summaryrefslogtreecommitdiffstats
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to '')
-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.phtml15
-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.php346
-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.php113
-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.less109
-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.php195
-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.php196
-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.php139
-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.php62
-rw-r--r--modules/monitoring/application/views/helpers/EscapeComment.php38
-rw-r--r--modules/monitoring/application/views/helpers/HostFlags.php33
-rw-r--r--modules/monitoring/application/views/helpers/IconImage.php64
-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.php116
-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.php33
-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.phtml70
-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.phtml145
-rw-r--r--modules/monitoring/configuration.php431
-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.php74
-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.php57
-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.php40
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimestarthistoryQuery.php204
-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.php283
-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.php40
-rw-r--r--modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimestarthistoryQuery.php202
-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.php286
-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.php348
-rw-r--r--modules/monitoring/library/Monitoring/BackendStep.php206
-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.php324
-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.php159
-rw-r--r--modules/monitoring/library/Monitoring/Object/Acknowledgement.php215
-rw-r--r--modules/monitoring/library/Monitoring/Object/Host.php204
-rw-r--r--modules/monitoring/library/Monitoring/Object/HostList.php133
-rw-r--r--modules/monitoring/library/Monitoring/Object/Macro.php82
-rw-r--r--modules/monitoring/library/Monitoring/Object/MonitoredObject.php930
-rw-r--r--modules/monitoring/library/Monitoring/Object/ObjectList.php293
-rw-r--r--modules/monitoring/library/Monitoring/Object/Service.php219
-rw-r--r--modules/monitoring/library/Monitoring/Object/ServiceList.php184
-rw-r--r--modules/monitoring/library/Monitoring/Plugin.php12
-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.php35
-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.php337
-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.php167
-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.php270
-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/module.less1919
-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.php185
-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.php423
-rw-r--r--modules/setup/application/forms/AuthBackendPage.php273
-rw-r--r--modules/setup/application/forms/AuthenticationPage.php69
-rw-r--r--modules/setup/application/forms/DatabaseCreationPage.php208
-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.phtml153
-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.php64
-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.php131
-rw-r--r--modules/setup/library/Setup/Steps/ResourceStep.php199
-rw-r--r--modules/setup/library/Setup/Steps/UserGroupStep.php213
-rw-r--r--modules/setup/library/Setup/Utils/DbTool.php943
-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.php752
-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.php232
-rw-r--r--modules/translation/library/Translation/Cli/TranslationCommand.php73
-rw-r--r--modules/translation/library/Translation/Util/GettextTranslationHelper.php442
-rw-r--r--modules/translation/module.info7
470 files changed, 56850 insertions, 0 deletions
diff --git a/modules/doc/application/controllers/IcingawebController.php b/modules/doc/application/controllers/IcingawebController.php
new file mode 100644
index 0000000..e841c41
--- /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..47dfb1c
--- /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($_SERER['HTTP_IF_NONE_MATCH'])) {
+ $ifNoneMatch = explode(', ', stripslashes($_SERVER['HTTP_IF_NONE_MATCH']));
+ foreach ($ifNoneMatch as $tag) {
+ if ($tag === $etag) {
+ $match = true;
+ break;
+ }
+ }
+ } elseif (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
+ $lastModifiedSince = stripslashes($_SERVER['HTTP_IF_MODIFIED_SINCE']);
+ if ($lastModifiedSince === $lastModified) {
+ $match = true;
+ }
+ }
+
+ $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..6ae2b14
--- /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..c84a983
--- /dev/null
+++ b/modules/doc/application/views/scripts/style/font.phtml
@@ -0,0 +1,15 @@
+<div class="controls">
+<?= $this->tabs ?>
+<h1>Icinga Web 2 Icons</h1>
+</div>
+
+<div class="content">
+<?php foreach ($this->font->glyphs as $icon): ?>
+<!-- TODO: move CSS away //-->
+<div style="width: 33%; font-size: 1.5em; height: 2em; float: left;" class="<?=
+ $this->font->css_prefix_text . $icon->css
+?>">
+<?= $this->escape($icon->css) ?> <span style="font-size: 0.6em">(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..de2d4d4
--- /dev/null
+++ b/modules/doc/library/Doc/Renderer/DocSectionRenderer.php
@@ -0,0 +1,346 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Doc\Renderer;
+
+require_once 'Parsedown/Parsedown.php';
+
+use DOMDocument;
+use DOMXPath;
+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 \Icinga\Web\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 \Icinga\Module\Doc\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 \Icinga\Web\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 \Icinga\Module\Doc\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 \Icinga\Web\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..fd2c903
--- /dev/null
+++ b/modules/doc/library/Doc/Search/DocSearchIterator.php
@@ -0,0 +1,113 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Doc\Search;
+
+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
+ {
+ $section = $this->current();
+ /** @var $section \Icinga\Module\Doc\DocSection */
+ $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..ac596db
--- /dev/null
+++ b/modules/doc/module.info
@@ -0,0 +1,4 @@
+Module: doc
+Version: 2.11.4
+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..007c5c5
--- /dev/null
+++ b/modules/doc/public/css/module.less
@@ -0,0 +1,109 @@
+/*! 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;
+}
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..06fb2a8
--- /dev/null
+++ b/modules/migrate/application/clicommands/NavigationCommand.php
@@ -0,0 +1,195 @@
+<?php
+
+/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
+
+namespace Icinga\Module\Migrate\Clicommands;
+
+use Icinga\Application\Config;
+use Icinga\Application\Icinga;
+use Icinga\Application\Logger;
+use Icinga\Cli\Command;
+use Icinga\Data\ConfigObject;
+use Icinga\Exception\NotReadableError;
+use Icinga\Exception\NotWritableError;
+use Icinga\Module\Icingadb\Compat\UrlMigrator;
+use Icinga\Util\DirectoryIterator;
+use Icinga\Web\Request;
+use ipl\Web\Filter\QueryString;
+use ipl\Web\Url;
+
+class NavigationCommand extends Command
+{
+ /**
+ * Migrate local user monitoring navigation items to the Icinga DB Web actions
+ *
+ * USAGE
+ *
+ * icingacli migrate navigation [options]
+ *
+ * OPTIONS:
+ *
+ * --user=<username> Migrate monitoring navigation items only for
+ * the given user. (Default *)
+ *
+ * --delete Remove the legacy files after successfully
+ * migrated the navigation items.
+ */
+ public function indexAction()
+ {
+ $moduleManager = Icinga::app()->getModuleManager();
+ if (! $moduleManager->hasEnabled('icingadb')) {
+ Logger::error('Icinga DB module is not enabled. Please verify that the module is installed and enabled.');
+ return;
+ }
+
+ $preferencesPath = Config::resolvePath('preferences');
+ $sharedNavigation = Config::resolvePath('navigation');
+ if (! file_exists($preferencesPath) && ! file_exists($sharedNavigation)) {
+ Logger::info('There are no local user navigation items to migrate');
+ return;
+ }
+
+ $rc = 0;
+ $user = $this->params->get('user');
+ $directories = new DirectoryIterator($preferencesPath);
+
+ foreach ($directories as $directory) {
+ $username = $user;
+ if ($username !== null && $directories->key() !== $username) {
+ continue;
+ }
+
+ if ($username === null) {
+ $username = $directories->key();
+ }
+
+ $hostActions = $this->readFromIni($directory . '/host-actions.ini', $rc);
+ $serviceActions = $this->readFromIni($directory . '/service-actions.ini', $rc);
+
+ Logger::info('Migrating monitoring navigation items for user "%s" to the Icinga DB Web actions', $username);
+
+ if (! $hostActions->isEmpty()) {
+ $this->migrateNavigationItems($hostActions, $directory . '/icingadb-host-actions.ini', $rc);
+ }
+
+ if (! $serviceActions->isEmpty()) {
+ $this->migrateNavigationItems($serviceActions, $directory . '/icingadb-service-actions.ini', $rc);
+ }
+ }
+
+ // Start migrating shared navigation items
+ $hostActions = $this->readFromIni($sharedNavigation . '/host-actions.ini', $rc);
+ $serviceActions = $this->readFromIni($sharedNavigation . '/service-actions.ini', $rc);
+
+ Logger::info('Migrating shared monitoring navigation items to the Icinga DB Web actions');
+
+ if (! $hostActions->isEmpty()) {
+ $this->migrateNavigationItems($hostActions, $sharedNavigation . '/icingadb-host-actions.ini', $rc);
+ }
+
+ if (! $serviceActions->isEmpty()) {
+ $this->migrateNavigationItems($serviceActions, $sharedNavigation . '/icingadb-service-actions.ini', $rc);
+ }
+
+ if ($rc > 0) {
+ Logger::error('Failed to migrate some monitoring navigation items');
+ exit($rc);
+ }
+
+ Logger::info('Successfully migrated all local user monitoring navigation items');
+ }
+
+ /**
+ * Migrate the given config to the given new config path
+ *
+ * @param Config $config
+ * @param string $path
+ * @param int $rc
+ */
+ private function migrateNavigationItems($config, $path, &$rc)
+ {
+ $deleteLegacyFiles = $this->params->get('delete');
+ $newConfig = $this->readFromIni($path, $rc);
+ $counter = 1;
+
+ /** @var ConfigObject $configObject */
+ foreach ($config->getConfigObject() as $configObject) {
+ // Change the config type from "host-action" to icingadb's new action
+ if (strpos($path, 'icingadb-host-actions') !== false) {
+ $configObject->type = 'icingadb-host-action';
+ } else {
+ $configObject->type = 'icingadb-service-action';
+ }
+
+ $urlString = $configObject->get('url');
+ if ($urlString !== null) {
+ $url = Url::fromPath($urlString, [], new Request());
+
+ try {
+ $urlString = UrlMigrator::transformUrl($url)->getAbsoluteUrl();
+ $configObject->url = rawurldecode($urlString);
+ } catch (\InvalidArgumentException $err) {
+ // Do nothing
+ }
+ }
+
+ $legacyFilter = $configObject->get('filter');
+ if ($legacyFilter !== null) {
+ $filter = QueryString::parse($legacyFilter);
+ $filter = UrlMigrator::transformFilter($filter);
+ if ($filter !== false) {
+ $configObject->filter = rawurldecode(QueryString::render($filter));
+ } else {
+ unset($configObject->filter);
+ }
+ }
+
+ $section = $config->key();
+ while ($newConfig->hasSection($section)) {
+ $section = $config->key() . $counter++;
+ }
+
+ $newConfig->setSection($section, $configObject);
+ }
+
+ try {
+ if (! $newConfig->isEmpty()) {
+ $newConfig->saveIni();
+
+ // Remove the legacy file only if explicitly requested
+ if ($deleteLegacyFiles) {
+ unlink($config->getConfigFile());
+ }
+ }
+ } catch (NotWritableError $error) {
+ Logger::error('%s: %s', $error->getMessage(), $error->getPrevious()->getMessage());
+ $rc = 256;
+ }
+ }
+
+ /**
+ * Get the navigation items config from the given ini path
+ *
+ * @param string $path Absolute path of the ini file
+ * @param int $rc The return code used to exit the action
+ *
+ * @return Config
+ */
+ private function readFromIni($path, &$rc)
+ {
+ try {
+ $config = Config::fromIni($path);
+ } catch (NotReadableError $error) {
+ if ($error->getPrevious() !== null) {
+ Logger::error('%s: %s', $error->getMessage(), $error->getPrevious()->getMessage());
+ } else {
+ Logger::error($error->getMessage());
+ }
+
+ $config = new Config();
+ $rc = 128;
+ }
+
+ return $config;
+ }
+}
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..6eb2911
--- /dev/null
+++ b/modules/migrate/module.info
@@ -0,0 +1,5 @@
+Module: migrate
+Version: 2.11.4
+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..08ab1bc
--- /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..48dd580
--- /dev/null
+++ b/modules/monitoring/application/controllers/HealthController.php
@@ -0,0 +1,196 @@
+<?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) {
+ return $this->render('not-running', true, null);
+ }
+ $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..9219df8
--- /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 (! empty($acknowledgedObjects)) {
+ $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..0ccff99
--- /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()
+ {
+ $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..6c65592
--- /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 (! empty($acknowledgedObjects)) {
+ $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..ab46071
--- /dev/null
+++ b/modules/monitoring/application/forms/Command/Object/ProcessCheckResultCommandForm.php
@@ -0,0 +1,139 @@
+<?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)
+ {
+ 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..db1511c
--- /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', 1)) {
+ $filters[] = $this->stateChangeFilter();
+ }
+ if ($this->getValue('comment', 1)) {
+ $filters[] = $this->commentFilter();
+ }
+ if ($this->getValue('notification', 1)) {
+ $filters[] = $this->notificationFilter();
+ }
+ if ($this->getValue('downtime', 1)) {
+ $filters[] = $this->downtimeFilter();
+ }
+ if ($this->getValue('flapping', 1)) {
+ $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..3a7c10d
--- /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') === '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', 'cnt_critical_hard');
+ $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..8bfdc3a
--- /dev/null
+++ b/modules/monitoring/application/views/helpers/Customvar.php
@@ -0,0 +1,62 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+class Zend_View_Helper_Customvar extends Zend_View_Helper_Abstract
+{
+ /**
+ * 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..be85a22
--- /dev/null
+++ b/modules/monitoring/application/views/helpers/EscapeComment.php
@@ -0,0 +1,38 @@
+<?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) {
+ require_once 'HTMLPurifier/Bootstrap.php';
+ require_once 'HTMLPurifier.php';
+ require_once 'HTMLPurifier.autoload.php';
+
+ $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..81d8ebc
--- /dev/null
+++ b/modules/monitoring/application/views/helpers/HostFlags.php
@@ -0,0 +1,33 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+class Zend_View_Helper_HostFlags extends Zend_View_Helper_Abstract
+{
+ 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..3c0ca43
--- /dev/null
+++ b/modules/monitoring/application/views/helpers/IconImage.php
@@ -0,0 +1,64 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+use Icinga\Module\Monitoring\Object\Macro;
+
+/**
+ * Generate icons to describe a given hosts state
+ */
+class Zend_View_Helper_IconImage extends Zend_View_Helper_Abstract
+{
+ /**
+ * 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..8509e5b
--- /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..e7bc72b
--- /dev/null
+++ b/modules/monitoring/application/views/helpers/Perfdata.php
@@ -0,0 +1,116 @@
+<?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;
+
+class Zend_View_Helper_Perfdata extends Zend_View_Helper_Abstract
+{
+ /**
+ * 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..bcd3d9e
--- /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 style="font-size: 0.75em"'
+ );
+
+ /** @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..47a351c
--- /dev/null
+++ b/modules/monitoring/application/views/helpers/ServiceFlags.php
@@ -0,0 +1,33 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+class Zend_View_Helper_ServiceFlags extends Zend_View_Helper_Abstract
+{
+ 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..8809c53
--- /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 style="width: 33.5em;">
+<?php foreach (array_reverse($grids) as $key => $grid): ?>
+ <div style=" <?= $this->orientation === 'horizontal' ? '' : 'display: inline-block; vertical-align: top; top; margin: 0.5em;' ?>">
+ <?= $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..aa448ae
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/show/contact.phtml
@@ -0,0 +1,70 @@
+<?php $contactHelper = $this->getHelper('ContactFlags') ?>
+<div class="controls">
+ <?php if (! $this->compact): ?>
+ <?= $this->tabs; ?>
+ <?php endif ?>
+ <h1><?= $this->translate('Contact details') ?></h1>
+ <div class="circular" style="background-image: url('<?=
+ $this->href('static/gravatar', array('email' => $contact->contact_email))
+ ?>';width:120px;height:120px;)"></div>
+
+<?php if (! $contact): ?>
+ <?= $this->translate('No such contact') ?>: <?= $contactName ?>
+</div>
+<?php return; endif ?>
+
+ <table class="name-value-table">
+ <tbody>
+ <tr>
+ <th style="width: 20%"></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..af3b406
--- /dev/null
+++ b/modules/monitoring/application/views/scripts/timeline/index.phtml
@@ -0,0 +1,145 @@
+<?php
+use Icinga\Web\Url;
+use Icinga\Util\Color;
+
+$groupInfo = $timeline->getGroupInfo();
+$firstRow = ! $beingExtended;
+
+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
+$circleWidth = $timeline->calculateCircleWidth($timeInfo[1][$groupName], 2);
+$extrapolatedCircleWidth = $timeline->getExtrapolatedCircleWidth($timeInfo[1][$groupName], 2);
+?>
+<?php if ($firstRow && $extrapolatedCircleWidth !== $circleWidth): ?>
+ <div class="circle-box" style="width: <?= $extrapolatedCircleWidth; ?>;">
+ <div class="outer-circle extrapolated <?= $timeInfo[1][$groupName]->getClass() ?>" style="<?= sprintf(
+ 'width: %2$s; height: %2$s; margin-top: -%1$Fem;',
+ (float) substr($extrapolatedCircleWidth, 0, -2) / 2,
+ $extrapolatedCircleWidth
+ ); ?>">
+<?php else: ?>
+ <div class="circle-box" style="width: <?= $circleWidth; ?>;">
+ <div class="outer-circle" style="<?= sprintf(
+ 'width: %2$s; height: %2$s; margin-top: -%1$Fem;',
+ (float) substr($circleWidth, 0, -2) / 2,
+ $circleWidth
+ ); ?>">
+<?php endif ?>
+ <?= $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
+ ),
+ 'class' => 'inner-circle ' . $timeInfo[1][$groupName]->getClass(),
+ 'style' => sprintf(
+ 'width: %2$s; height: %2$s; margin-top: -%1$Fem; margin-left: -%1$Fem;',
+ (float) substr($circleWidth, 0, -2) / 2,
+ (string) $circleWidth
+ )
+ )
+ ); ?>
+ </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 ?>
diff --git a/modules/monitoring/configuration.php b/modules/monitoring/configuration.php
new file mode 100644
index 0000000..663db93
--- /dev/null
+++ b/modules/monitoring/configuration.php
@@ -0,0 +1,431 @@
+<?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('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..09779b6
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/AllcontactsQuery.php
@@ -0,0 +1,74 @@
+<?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 $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..297b20a
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/EventgridQuery.php
@@ -0,0 +1,57 @@
+<?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_hard' => "SUM(CASE WHEN sth.state = 3 AND sth.type = 'hard_state' 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..77d91e5
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimeendhistoryQuery.php
@@ -0,0 +1,40 @@
+<?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'");
+ $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..54ac6a1
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostdowntimestarthistoryQuery.php
@@ -0,0 +1,204 @@
+<?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->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..284468e
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/HostnotificationQuery.php
@@ -0,0 +1,283 @@
+<?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()
+ {
+ 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..5d79143
--- /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..bd7a077
--- /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->columMap[$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..2d592c8
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimeendhistoryQuery.php
@@ -0,0 +1,40 @@
+<?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'");
+ $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..f22e265
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicedowntimestarthistoryQuery.php
@@ -0,0 +1,202 @@
+<?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->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..d3fccf0
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/ServicenotificationQuery.php
@@ -0,0 +1,286 @@
+<?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()
+ {
+ 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..cf59cf3
--- /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..5400957
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Backend/MonitoringBackend.php
@@ -0,0 +1,348 @@
+<?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\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 Icinga\Data\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..e94625f
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/BackendStep.php
@@ -0,0 +1,206 @@
+<?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>';
+
+ 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..8370314
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Command/Renderer/IcingaApiCommandRenderer.php
@@ -0,0 +1,324 @@
+<?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
+ *
+ * @return array
+ */
+ 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..aa47547
--- /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
+ *
+ * @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..5426bb9
--- /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
+ *
+ * @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..6440fe5
--- /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..e80c6f0
--- /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..700bfd5
--- /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, $object) {
+ $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..51ead8a
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/MonitoringWizard.php
@@ -0,0 +1,159 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring;
+
+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 $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..dfb25ed
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Object/Host.php
@@ -0,0 +1,204 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Object;
+
+use Icinga\Data\Filter\FilterEqual;
+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 \Icinga\Module\Monitoring\DataView\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('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..3f67154
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Object/Macro.php
@@ -0,0 +1,82 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Object;
+
+use Exception;
+use Icinga\Application\Logger;
+
+/**
+ * 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..36b922a
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Object/ObjectList.php
@@ -0,0 +1,293 @@
+<?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\Filterable;
+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
+ */
+ 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 type
+ */
+ 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) {
+ $status = 2;
+ unset($features[$status]);
+ 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..95c00fc
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Object/Service.php
@@ -0,0 +1,219 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring\Object;
+
+use Icinga\Data\Filter\FilterEqual;
+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 \Icinga\Module\Monitoring\DataView\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('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.php b/modules/monitoring/library/Monitoring/Plugin.php
new file mode 100644
index 0000000..e8e1f5d
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Plugin.php
@@ -0,0 +1,12 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Monitoring;
+
+use Icinga\Application\Cli;
+
+require_once ICINGA_LIBDIR . '/Icinga/Application/Cli.php';
+
+class Plugin extends Cli
+{
+}
diff --git a/modules/monitoring/library/Monitoring/Plugin/Perfdata.php b/modules/monitoring/library/Monitoring/Plugin/Perfdata.php
new file mode 100644
index 0000000..476354a
--- /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 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..fd1818f
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/ProvidedHook/X509/Sni.php
@@ -0,0 +1,35 @@
+<?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)
+ {
+ $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..128b64b
--- /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($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..08c7a2c
--- /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 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..014ac43
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Web/Controller/MonitoredObjectController.php
@@ -0,0 +1,337 @@
+<?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\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;
+ $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..e06526e
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Web/Navigation/Renderer/MonitoringBadgeNavigationItemRenderer.php
@@ -0,0 +1,167 @@
+<?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) {
+ $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..4cbdad5
--- /dev/null
+++ b/modules/monitoring/library/Monitoring/Web/Widget/CustomVarTable.php
@@ -0,0 +1,270 @@
+<?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;
+
+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);
+
+ $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..82c520d
--- /dev/null
+++ b/modules/monitoring/module.info
@@ -0,0 +1,5 @@
+Module: monitoring
+Version: 2.11.4
+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/module.less b/modules/monitoring/public/css/module.less
new file mode 100644
index 0000000..f97031c
--- /dev/null
+++ b/modules/monitoring/public/css/module.less
@@ -0,0 +1,1919 @@
+/*! 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;
+
+ .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..130d797
--- /dev/null
+++ b/modules/setup/application/clicommands/ConfigCommand.php
@@ -0,0 +1,185 @@
+<?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\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.'));
+ }
+ 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..3252ec1
--- /dev/null
+++ b/modules/setup/application/forms/AdminAccountPage.php
@@ -0,0 +1,423 @@
+<?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\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();
+ 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';
+ }
+
+ 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.'
+ )
+ )
+ );
+ $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'))) {
+ $query->getQuery()->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'))) {
+ $query->getQuery()->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..4280c64
--- /dev/null
+++ b/modules/setup/application/forms/AuthBackendPage.php
@@ -0,0 +1,273 @@
+<?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();
+ }
+
+ 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..8660a21
--- /dev/null
+++ b/modules/setup/application/forms/DatabaseCreationPage.php
@@ -0,0 +1,208 @@
+<?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')
+ )
+ );
+
+ 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..b3f1784
--- /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..b2b3bda
--- /dev/null
+++ b/modules/setup/application/views/scripts/index/index.phtml
@@ -0,0 +1,153 @@
+<?php
+
+use Icinga\Web\Notification;
+
+$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);
+
+$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); }
+))));
+
+?>
+<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" style="width: 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" style="width: 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" style="width: 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" style="width: 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): ?>
+ <div class="line left<?= $stateClass; ?>" style="float: left; width: <?= sprintf(
+ '%.2F',
+ 100 - (count($configPagesLeft) - 1) * $lineWidth
+ ); ?>%; margin-right: 0"></div>
+ <?php elseif ($page === $lastPage): ?>
+ <div class="line<?= $stateClass; ?>" style="float: left; width: <?= $lineWidth; ?>%; margin-right: -0.1em;"></div>
+ <?php else: ?>
+ <div class="line<?= $stateClass; ?>" style="float: left; width: <?= $lineWidth; ?>%;"></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): ?>
+ <div class="line<?= $stateClass; ?>" style="float: left; width: <?= $lineWidth; ?>%; margin-left: -0.1em;"></div>
+ <?php elseif ($page === $lastPage): ?>
+ <div class="line right<?= $stateClass; ?>" style="float: left; width: <?= sprintf(
+ '%.2F',
+ 100 - (count($configPagesRight) - 1) * $lineWidth
+ ); ?>%; margin-left: 0;"></div>
+ <?php else: ?>
+ <div class="line<?= $stateClass; ?>" style="float: left; width: <?= $lineWidth; ?>%;"></div>
+ <?php endif ?>
+ <?php endforeach ?>
+ </td>
+ </tr></tbody></table>
+ <?php if ($maxProgress > 2 || $currentPos > 2): ?>
+ <?= $this->restartForm ?>
+ <?php endif ?>
+ </div>
+ <div class="step" style="width: 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>
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..dc5ba1c
--- /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..fd16405
--- /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 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..672fad4
--- /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..cc9392a
--- /dev/null
+++ b/modules/setup/library/Setup/RequirementsRenderer.php
@@ -0,0 +1,64 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Setup;
+
+use RecursiveIteratorIterator;
+
+class RequirementsRenderer extends RecursiveIteratorIterator
+{
+ public function beginIteration(): void
+ {
+ $this->tags[] = '<ul class="requirements">';
+ }
+
+ public function endIteration(): void
+ {
+ $this->tags[] = '</ul>';
+ }
+
+ public function beginChildren(): void
+ {
+ $this->tags[] = '<li>';
+ $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..1d0797d
--- /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..2c928f6
--- /dev/null
+++ b/modules/setup/library/Setup/Steps/GeneralConfigStep.php
@@ -0,0 +1,131 @@
+<?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'];
+
+ 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..d9daf3b
--- /dev/null
+++ b/modules/setup/library/Setup/Steps/ResourceStep.php
@@ -0,0 +1,199 @@
+<?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>';
+ }
+
+ 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>';
+ }
+
+ 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..5cf203e
--- /dev/null
+++ b/modules/setup/library/Setup/Utils/DbTool.php
@@ -0,0 +1,943 @@
+<?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_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,
+ 'SELECT' => 24,
+ 'INSERT' => 24,
+ 'UPDATE' => 24,
+ 'DELETE' => 8,
+ 'TRUNCATE' => 8,
+ 'REFERENCES' => 24,
+ 'TRIGGER' => 8,
+ 'CREATE' => 12,
+ '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();
+ $tablePrivileges = array();
+ foreach (array_intersect($privileges, array_keys($this->pgsqlGrantContexts)) as $privilege) {
+ if (! empty($context) && $this->pgsqlGrantContexts[$privilege] & static::TABLE_LEVEL) {
+ $tablePrivileges[] = $privilege;
+ } elseif ($this->pgsqlGrantContexts[$privilege] & static::DATABASE_LEVEL) {
+ $dbPrivileges[] = $privilege;
+ }
+ }
+
+ if (! empty($dbPrivileges)) {
+ $this->exec(sprintf(
+ 'GRANT %s ON DATABASE %s TO %s',
+ join(',', $dbPrivileges),
+ $this->config['dbname'],
+ $username
+ ));
+ }
+
+ if (! empty($tablePrivileges)) {
+ foreach ($context as $table) {
+ $this->exec(sprintf(
+ 'GRANT %s ON TABLE %s TO %s',
+ join(',', $tablePrivileges),
+ $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;
+ if ($this->dbFromConfig) {
+ $dbPrivileges = array();
+ $tablePrivileges = array();
+ foreach (array_intersect($privileges, array_keys($this->pgsqlGrantContexts)) as $privilege) {
+ if (! empty($context) && $this->pgsqlGrantContexts[$privilege] & static::TABLE_LEVEL) {
+ $tablePrivileges[] = $privilege;
+ }
+ if ($this->pgsqlGrantContexts[$privilege] & static::DATABASE_LEVEL) {
+ $dbPrivileges[] = $privilege;
+ }
+ }
+
+ if (! empty($dbPrivileges)) {
+ $dbExclusivesGranted = true;
+ foreach ($dbPrivileges as $dbPrivilege) {
+ $query = $this->query(
+ 'SELECT has_database_privilege(:user, :dbname, :privilege) AS db_privilege_granted',
+ array(
+ ':user' => $username !== null ? $username : $this->config['username'],
+ ':dbname' => $this->config['dbname'],
+ ':privilege' => $dbPrivilege . ($requireGrants ? ' WITH GRANT OPTION' : '')
+ )
+ );
+ if (! $query->fetchObject()->db_privilege_granted) {
+ $privilegesGranted = false;
+ if (! in_array($dbPrivilege, $tablePrivileges)) {
+ $dbExclusivesGranted = false;
+ }
+ }
+ }
+
+ if ($privilegesGranted) {
+ // Do not check privileges twice if they are already granted at database level
+ $tablePrivileges = array_diff($tablePrivileges, $dbPrivileges);
+ } elseif ($dbExclusivesGranted) {
+ $privilegesGranted = true;
+ }
+ }
+
+ if ($privilegesGranted && !empty($tablePrivileges)) {
+ foreach (array_intersect($context, $this->listTables()) as $table) {
+ foreach ($tablePrivileges as $tablePrivilege) {
+ $query = $this->query(
+ 'SELECT has_table_privilege(:user, :table, :privilege) AS table_privilege_granted',
+ array(
+ ':user' => $username !== null ? $username : $this->config['username'],
+ ':table' => $table,
+ ':privilege' => $tablePrivilege . ($requireGrants ? ' WITH GRANT OPTION' : '')
+ )
+ );
+ $privilegesGranted &= $query->fetchObject()->table_privilege_granted;
+ }
+ }
+ }
+ } 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 (array_search('CREATE', $privileges, true) !== false) {
+ $query = $this->query(
+ 'select rolcreatedb from pg_roles where rolname = :user',
+ array(':user' => $username !== null ? $username : $this->config['username'])
+ );
+ $privilegesGranted &= $query->fetchColumn() !== false;
+ }
+ }
+
+ if (array_search('CREATEROLE', $privileges, true) !== false) {
+ $query = $this->query(
+ 'select rolcreaterole from pg_roles where rolname = :user',
+ array(':user' => $username !== null ? $username : $this->config['username'])
+ );
+ $privilegesGranted &= $query->fetchColumn() !== false;
+ }
+
+ if (array_search('SUPER', $privileges, true) !== false) {
+ $query = $this->query(
+ 'select rolsuper from pg_roles where rolname = :user',
+ array(':user' => $username !== null ? $username : $this->config['username'])
+ );
+ $privilegesGranted &= $query->fetchColumn() !== false;
+ }
+
+ return (bool) $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..f25be55
--- /dev/null
+++ b/modules/setup/library/Setup/WebWizard.php
@@ -0,0 +1,752 @@
+<?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 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',
+ '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'
+ );
+
+ /**
+ * 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) {
+ $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') {
+ $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') {
+ $page->setResourceConfig($this->getPageData('setup_ldap_resource'));
+ $page->setBackendConfig($this->getPageData('setup_authentication_backend'));
+ } elseif ($page->getName() === 'setup_admin_account') {
+ $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') {
+ $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') {
+ $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());
+ }
+ }
+
+ $setup->addStep(new EnableModuleStep(array_keys($this->getPage('setup_modules')->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.9.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.11.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..2251ba3
--- /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..e3570bd
--- /dev/null
+++ b/modules/setup/module.info
@@ -0,0 +1,6 @@
+Module: setup
+Version: 2.11.4
+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..af01d5f
--- /dev/null
+++ b/modules/translation/library/Translation/Cli/ArrayToTextTableHelper.php
@@ -0,0 +1,232 @@
+<?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 array $rows The input array
+ * @param bool $head Show heading
+ * @param int $maxWidth Max Column Height (returns)
+ * @param int $maxHeight Max Row Width (chars)
+ */
+ 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..d1e6ac2
--- /dev/null
+++ b/modules/translation/library/Translation/Util/GettextTranslationHelper.php
@@ -0,0 +1,442 @@
+<?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
+ * @param array $blacklist A list of directories to omit
+ *
+ * @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..57a0dd2
--- /dev/null
+++ b/modules/translation/module.info
@@ -0,0 +1,7 @@
+Module: translation
+Version: 2.11.4
+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.