summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-14 13:15:40 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-14 13:15:40 +0000
commitb7fd908d538ed19fe41f03c0a3f93351d8da64e9 (patch)
tree46e14f318948cd4f5d7e874f83e7dfcc5d42fc64
parentInitial commit. (diff)
downloadicingaweb2-module-businessprocess-b7fd908d538ed19fe41f03c0a3f93351d8da64e9.tar.xz
icingaweb2-module-businessprocess-b7fd908d538ed19fe41f03c0a3f93351d8da64e9.zip
Adding upstream version 2.5.0.upstream/2.5.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.md37
-rw-r--r--.github/ISSUE_TEMPLATE/feature_request.md20
-rw-r--r--.github/workflows/L10n-update.yml20
-rw-r--r--.github/workflows/php.yml95
-rw-r--r--LICENSE339
-rw-r--r--README.md47
-rw-r--r--application/clicommands/CheckCommand.php23
-rw-r--r--application/clicommands/CleanupCommand.php106
-rw-r--r--application/clicommands/ProcessCommand.php227
-rw-r--r--application/controllers/HostController.php66
-rw-r--r--application/controllers/IndexController.php20
-rw-r--r--application/controllers/NodeController.php148
-rw-r--r--application/controllers/ProcessController.php780
-rw-r--r--application/controllers/ServiceController.php74
-rw-r--r--application/controllers/SuggestionsController.php372
-rw-r--r--application/forms/AddNodeForm.php412
-rw-r--r--application/forms/BpConfigForm.php236
-rw-r--r--application/forms/BpUploadForm.php207
-rw-r--r--application/forms/CleanupNodeForm.php61
-rw-r--r--application/forms/DeleteNodeForm.php125
-rw-r--r--application/forms/EditNodeForm.php315
-rw-r--r--application/forms/MoveNodeForm.php172
-rw-r--r--application/forms/ProcessForm.php158
-rw-r--r--application/forms/SimulationForm.php138
-rw-r--r--application/views/helpers/FormSimpleNote.php15
-rw-r--r--application/views/helpers/RenderStateBadges.php33
-rw-r--r--application/views/scripts/default.phtml2
-rw-r--r--application/views/scripts/host/show.phtml13
-rw-r--r--application/views/scripts/process/source.phtml25
-rw-r--r--application/views/scripts/service/show.phtml14
-rw-r--r--configuration.php64
-rw-r--r--doc/01-About.md19
-rw-r--r--doc/02-Installation.md24
-rw-r--r--doc/02-Installation.md.d/From-Source.md15
-rw-r--r--doc/03-Getting-Started.md77
-rw-r--r--doc/04-Create-your-first-process-node.md67
-rw-r--r--doc/05-Importing-Processes.md53
-rw-r--r--doc/06-Customize-Node-Order.md71
-rw-r--r--doc/07-State-Overrides.md45
-rw-r--r--doc/09-Operators.md43
-rw-r--r--doc/10-Monitoring.md49
-rw-r--r--doc/12-Web-Components-Breadcrumb.md69
-rw-r--r--doc/13-Web-Components-Tile-Renderer.md22
-rw-r--r--doc/14-Web-Components-Tree-Renderer.md13
-rw-r--r--doc/16-Add-To-Dashboard.md20
-rw-r--r--doc/21-Store-Config.md23
-rw-r--r--doc/22-Upload-Config.md26
-rw-r--r--doc/31-Permissions.md25
-rw-r--r--doc/81-History.md43
-rw-r--r--doc/screenshot/00_preview/0001_preview-tree-view.pngbin0 -> 59495 bytes
-rw-r--r--doc/screenshot/00_preview/0002_preview_tile_view.pngbin0 -> 22985 bytes
-rw-r--r--doc/screenshot/00_preview/0003_preview_businessprocesses_on_dashboard.png0
-rw-r--r--doc/screenshot/00_preview/0004_preview_tile_and_subtree.pngbin0 -> 27261 bytes
-rw-r--r--doc/screenshot/00_preview/0005_readme-preview.pngbin0 -> 150226 bytes
-rw-r--r--doc/screenshot/02_installation/101_menu-configuration-modules.pngbin0 -> 7272 bytes
-rw-r--r--doc/screenshot/02_installation/102_enable-module.pngbin0 -> 80770 bytes
-rw-r--r--doc/screenshot/03_getting-started/0201_empty-dashboard.pngbin0 -> 38430 bytes
-rw-r--r--doc/screenshot/03_getting-started/0202_create-new-configuration.pngbin0 -> 34234 bytes
-rw-r--r--doc/screenshot/03_getting-started/0203_create-new_name.pngbin0 -> 4367 bytes
-rw-r--r--doc/screenshot/03_getting-started/0204_create-new_title.pngbin0 -> 6917 bytes
-rw-r--r--doc/screenshot/03_getting-started/0205_create-new_description.pngbin0 -> 13943 bytes
-rw-r--r--doc/screenshot/03_getting-started/0206_create-new_backend.pngbin0 -> 9024 bytes
-rw-r--r--doc/screenshot/03_getting-started/0207_create-new_state-type.pngbin0 -> 7100 bytes
-rw-r--r--doc/screenshot/03_getting-started/0208_create-new_add-to-menu.pngbin0 -> 5854 bytes
-rw-r--r--doc/screenshot/03_getting-started/0209_new-empty-configuration.pngbin0 -> 19156 bytes
-rw-r--r--doc/screenshot/03_getting-started/0210_new-on-dashboard.pngbin0 -> 37480 bytes
-rw-r--r--doc/screenshot/04_first-root-node/0301_empty-config.pngbin0 -> 19156 bytes
-rw-r--r--doc/screenshot/04_first-root-node/0302_add-new-node.pngbin0 -> 27807 bytes
-rw-r--r--doc/screenshot/04_first-root-node/0303_node-title.pngbin0 -> 5428 bytes
-rw-r--r--doc/screenshot/04_first-root-node/0304_operator.pngbin0 -> 11318 bytes
-rw-r--r--doc/screenshot/04_first-root-node/0305_display.pngbin0 -> 7537 bytes
-rw-r--r--doc/screenshot/04_first-root-node/0306_info-url.pngbin0 -> 4088 bytes
-rw-r--r--doc/screenshot/04_first-root-node/0307_first-node-created.pngbin0 -> 28115 bytes
-rw-r--r--doc/screenshot/05_importing_nodes/0401_subprocesses_only.pngbin0 -> 68346 bytes
-rw-r--r--doc/screenshot/05_importing_nodes/0402_choose_existing_process.pngbin0 -> 32248 bytes
-rw-r--r--doc/screenshot/05_importing_nodes/0403_choose_configuration.pngbin0 -> 8035 bytes
-rw-r--r--doc/screenshot/05_importing_nodes/0404_choose_process.pngbin0 -> 8112 bytes
-rw-r--r--doc/screenshot/05_importing_nodes/0405_import_successful.pngbin0 -> 32816 bytes
-rw-r--r--doc/screenshot/05_importing_nodes/0406_breadcrumb_integration.pngbin0 -> 12574 bytes
-rw-r--r--doc/screenshot/05_importing_nodes/0407_jump_to_original.pngbin0 -> 30339 bytes
-rw-r--r--doc/screenshot/06_customize_node_order/0501_tiles_grab_tile.pngbin0 -> 52954 bytes
-rw-r--r--doc/screenshot/06_customize_node_order/0502_tiles_drop_at_location.pngbin0 -> 52986 bytes
-rw-r--r--doc/screenshot/06_customize_node_order/0503_tree_grab_header.pngbin0 -> 53453 bytes
-rw-r--r--doc/screenshot/06_customize_node_order/0504_tree_drop_at_location.pngbin0 -> 61292 bytes
-rw-r--r--doc/screenshot/07_state_overrides/0701_override_config.pngbin0 -> 76256 bytes
-rw-r--r--doc/screenshot/07_state_overrides/0702_overridden_tile.pngbin0 -> 36046 bytes
-rw-r--r--doc/screenshot/07_state_overrides/0703_overridden_tree.pngbin0 -> 40178 bytes
-rw-r--r--doc/screenshot/09_operators/0901_and-operator.pngbin0 -> 31172 bytes
-rw-r--r--doc/screenshot/09_operators/0902_or-operator.pngbin0 -> 30888 bytes
-rw-r--r--doc/screenshot/09_operators/0903_or-operator-without-ok.pngbin0 -> 31421 bytes
-rw-r--r--doc/screenshot/09_operators/0904_min-operator.pngbin0 -> 30888 bytes
-rw-r--r--doc/screenshot/09_operators/0905_deg-operator.jpgbin0 -> 68934 bytes
-rw-r--r--doc/screenshot/09_operators/0906_xor-operator.pngbin0 -> 30888 bytes
-rw-r--r--doc/screenshot/09_operators/0907_xor-operator-not-ok.pngbin0 -> 30767 bytes
-rw-r--r--doc/screenshot/12_web-components_breadcrumb/1201_simple-breadcrumb.pngbin0 -> 17797 bytes
-rw-r--r--doc/screenshot/12_web-components_breadcrumb/1202_return-from-fullscreen.pngbin0 -> 7114 bytes
-rw-r--r--doc/screenshot/12_web-components_breadcrumb/1203_add-to-dashboard.pngbin0 -> 16510 bytes
-rw-r--r--doc/screenshot/12_web-components_breadcrumb/1204_unlocked_config.pngbin0 -> 21001 bytes
-rw-r--r--doc/screenshot/13_web-components-tile-renderer/1301_tile-view.pngbin0 -> 57393 bytes
-rw-r--r--doc/screenshot/13_web-components-tile-renderer/1302_tile-and-subtree.pngbin0 -> 88235 bytes
-rw-r--r--doc/screenshot/14_web-components-tree-renderer/1401_tree-view.pngbin0 -> 70406 bytes
-rw-r--r--doc/screenshot/16_dashboard/1601_add-to-dashboard-link.pngbin0 -> 16510 bytes
-rw-r--r--doc/screenshot/16_dashboard/1602_add_to_dashboard-form.pngbin0 -> 24578 bytes
-rw-r--r--doc/screenshot/16_dashboard/1603_businessprocesses_on_dashboard.pngbin0 -> 151132 bytes
-rw-r--r--doc/screenshot/21_store-config/2101_Pending-Changes.pngbin0 -> 11792 bytes
-rw-r--r--doc/screenshot/21_store-config/2102_Store-Config.pngbin0 -> 52360 bytes
-rw-r--r--doc/screenshot/21_store-config/2103_Show-Diff.pngbin0 -> 36463 bytes
-rw-r--r--doc/screenshot/22_upload-config/2201_go-to-upload.pngbin0 -> 32819 bytes
-rw-r--r--doc/screenshot/22_upload-config/2202_choose-file.pngbin0 -> 16146 bytes
-rw-r--r--doc/screenshot/22_upload-config/2203_syntax-error.pngbin0 -> 25406 bytes
-rw-r--r--doc/screenshot/22_upload-config/2204_duplicate-name.pngbin0 -> 40201 bytes
-rw-r--r--doc/screenshot/81_history/8101_bpaddon-overview.pngbin0 -> 77964 bytes
-rw-r--r--doc/screenshot/81_history/8102_bpaddon-detail.pngbin0 -> 46198 bytes
-rw-r--r--library/Businessprocess/BpConfig.php1117
-rw-r--r--library/Businessprocess/BpNode.php646
-rw-r--r--library/Businessprocess/Common/Sort.php158
-rw-r--r--library/Businessprocess/Director/ShipConfigFiles.php22
-rw-r--r--library/Businessprocess/Exception/ModificationError.php9
-rw-r--r--library/Businessprocess/Exception/NestingError.php9
-rw-r--r--library/Businessprocess/HostNode.php64
-rw-r--r--library/Businessprocess/IcingaDbObject.php94
-rw-r--r--library/Businessprocess/ImportedNode.php139
-rw-r--r--library/Businessprocess/Metadata.php264
-rw-r--r--library/Businessprocess/Modification/NodeAction.php179
-rw-r--r--library/Businessprocess/Modification/NodeAddChildrenAction.php74
-rw-r--r--library/Businessprocess/Modification/NodeApplyManualOrderAction.php35
-rw-r--r--library/Businessprocess/Modification/NodeCopyAction.php48
-rw-r--r--library/Businessprocess/Modification/NodeCreateAction.php129
-rw-r--r--library/Businessprocess/Modification/NodeModifyAction.php121
-rw-r--r--library/Businessprocess/Modification/NodeMoveAction.php227
-rw-r--r--library/Businessprocess/Modification/NodeRemoveAction.php125
-rw-r--r--library/Businessprocess/Modification/ProcessChanges.php294
-rw-r--r--library/Businessprocess/MonitoredNode.php19
-rw-r--r--library/Businessprocess/Monitoring/Backend/Ido/Query/CustomVarJoinTemplateOverride.php84
-rw-r--r--library/Businessprocess/Monitoring/Backend/Ido/Query/HostStatusQuery.php8
-rw-r--r--library/Businessprocess/Monitoring/Backend/Ido/Query/ServiceStatusQuery.php8
-rw-r--r--library/Businessprocess/Monitoring/DataView/HostStatus.php16
-rw-r--r--library/Businessprocess/Monitoring/DataView/ServiceStatus.php16
-rw-r--r--library/Businessprocess/MonitoringRestrictions.php65
-rw-r--r--library/Businessprocess/Node.php570
-rw-r--r--library/Businessprocess/ProvidedHook/Icingadb/HostActions.php23
-rw-r--r--library/Businessprocess/ProvidedHook/Icingadb/IcingadbSupport.php10
-rw-r--r--library/Businessprocess/ProvidedHook/Icingadb/ServiceActions.php25
-rw-r--r--library/Businessprocess/ProvidedHook/Icingadb/ServiceDetailExtension.php77
-rw-r--r--library/Businessprocess/ProvidedHook/Monitoring/DetailviewExtension.php83
-rw-r--r--library/Businessprocess/ProvidedHook/Monitoring/HostActions.php19
-rw-r--r--library/Businessprocess/ProvidedHook/Monitoring/ServiceActions.php24
-rw-r--r--library/Businessprocess/Renderer/Breadcrumb.php80
-rw-r--r--library/Businessprocess/Renderer/Renderer.php431
-rw-r--r--library/Businessprocess/Renderer/TileRenderer.php85
-rw-r--r--library/Businessprocess/Renderer/TileRenderer/NodeTile.php353
-rw-r--r--library/Businessprocess/Renderer/TreeRenderer.php380
-rw-r--r--library/Businessprocess/ServiceNode.php95
-rw-r--r--library/Businessprocess/Simulation.php185
-rw-r--r--library/Businessprocess/State/IcingaDbState.php191
-rw-r--r--library/Businessprocess/State/MonitoringState.php151
-rw-r--r--library/Businessprocess/Storage/ConfigDiff.php77
-rw-r--r--library/Businessprocess/Storage/LegacyConfigParser.php413
-rw-r--r--library/Businessprocess/Storage/LegacyConfigRenderer.php268
-rw-r--r--library/Businessprocess/Storage/LegacyStorage.php205
-rw-r--r--library/Businessprocess/Storage/Storage.php107
-rw-r--r--library/Businessprocess/Test/BaseTestCase.php76
-rw-r--r--library/Businessprocess/Test/Bootstrap.php29
-rw-r--r--library/Businessprocess/Web/Component/ActionBar.php15
-rw-r--r--library/Businessprocess/Web/Component/BpDashboardTile.php47
-rw-r--r--library/Businessprocess/Web/Component/Content.php14
-rw-r--r--library/Businessprocess/Web/Component/Controls.php14
-rw-r--r--library/Businessprocess/Web/Component/Dashboard.php140
-rw-r--r--library/Businessprocess/Web/Component/DashboardAction.php27
-rw-r--r--library/Businessprocess/Web/Component/RenderedProcessActionBar.php161
-rw-r--r--library/Businessprocess/Web/Component/Tabs.php9
-rw-r--r--library/Businessprocess/Web/Component/WtfTabs.php22
-rw-r--r--library/Businessprocess/Web/Controller.php262
-rw-r--r--library/Businessprocess/Web/FakeRequest.php26
-rw-r--r--library/Businessprocess/Web/Form/BpConfigBaseForm.php135
-rw-r--r--library/Businessprocess/Web/Form/CsrfToken.php53
-rw-r--r--library/Businessprocess/Web/Form/Element/Checkbox.php8
-rw-r--r--library/Businessprocess/Web/Form/Element/FormElement.php9
-rw-r--r--library/Businessprocess/Web/Form/Element/IplStateOverrides.php75
-rw-r--r--library/Businessprocess/Web/Form/Element/SimpleNote.php22
-rw-r--r--library/Businessprocess/Web/Form/FormLoader.php39
-rw-r--r--library/Businessprocess/Web/Form/QuickBaseForm.php166
-rw-r--r--library/Businessprocess/Web/Form/QuickForm.php514
-rw-r--r--library/Businessprocess/Web/Form/Validator/HostServiceTermValidator.php96
-rw-r--r--library/Businessprocess/Web/Navigation/Renderer/ProcessProblemsBadge.php59
-rw-r--r--library/Businessprocess/Web/Navigation/Renderer/ProcessesProblemsBadge.php53
-rw-r--r--library/Businessprocess/Web/Url.php32
-rw-r--r--module.info16
-rw-r--r--phpcs.xml25
-rw-r--r--phpstan-baseline.neon4476
-rw-r--r--phpstan.neon31
-rw-r--r--phpunit.xml28
-rw-r--r--public/css/module.less996
-rw-r--r--public/img/ack.gifbin0 -> 564 bytes
-rw-r--r--public/img/downtime.gifbin0 -> 601 bytes
-rw-r--r--public/img/help.gifbin0 -> 1057 bytes
-rw-r--r--public/img/icon_collapse.pngbin0 -> 178 bytes
-rw-r--r--public/img/icon_expand.pngbin0 -> 177 bytes
-rw-r--r--public/js/behavior/sortable.js47
-rw-r--r--public/js/module.js287
-rw-r--r--public/js/vendor/Sortable.js2349
-rw-r--r--public/js/vendor/jquery.fn.sortable.js76
-rw-r--r--run.php10
-rw-r--r--test/bootstrap.php16
-rw-r--r--test/config/authentication.ini0
-rw-r--r--test/config/config.ini0
-rw-r--r--test/config/modules/businessprocess/processes/also-with-semicolons.conf8
-rw-r--r--test/config/modules/businessprocess/processes/broken_wrong-operator.conf1
-rw-r--r--test/config/modules/businessprocess/processes/combined.conf1
-rw-r--r--test/config/modules/businessprocess/processes/simple_with-header.conf13
-rw-r--r--test/config/modules/businessprocess/processes/simple_without-header.conf6
-rw-r--r--test/config/modules/businessprocess/processes/with-semicolons.conf14
-rw-r--r--test/php/library/Businessprocess/BpConfigTest.php49
-rw-r--r--test/php/library/Businessprocess/BpNodeTest.php39
-rw-r--r--test/php/library/Businessprocess/HostNodeTest.php63
-rw-r--r--test/php/library/Businessprocess/MetadataTest.php32
-rw-r--r--test/php/library/Businessprocess/Operators/AndOperatorTest.php214
-rw-r--r--test/php/library/Businessprocess/Operators/DegradedOperatorTest.php159
-rw-r--r--test/php/library/Businessprocess/Operators/MinOperatorTest.php174
-rw-r--r--test/php/library/Businessprocess/Operators/NotOperatorTest.php151
-rw-r--r--test/php/library/Businessprocess/Operators/OrOperatorTest.php116
-rw-r--r--test/php/library/Businessprocess/ServiceNodeTest.php56
-rw-r--r--test/php/library/Businessprocess/SimulationTest.php47
-rw-r--r--test/php/library/Businessprocess/Storage/LegacyStorageTest.php175
-rw-r--r--test/php/library/Businessprocess/Web/Component/TabsTest.php17
-rw-r--r--test/phpunit-compat.php10
-rwxr-xr-xtest/setup_vendor.sh82
227 files changed, 24777 insertions, 0 deletions
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..4734b80
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,37 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+## Describe the bug
+A clear and concise description of what the issue is.
+
+## To Reproduce
+Provide a link to a live example, or an unambiguous set of steps to reproduce this issue. Include configuration, logs, etc. to reproduce, if relevant.
+
+1.
+2.
+3.
+4.
+
+## Expected behavior
+A clear and concise description of what you expected to happen.
+
+## Screenshots
+If applicable, add screenshots to help explain your problem.
+
+## Your Environment
+Include as many relevant details about the environment you experienced the problem in
+
+* Icinga Web 2 version and modules (System - About):
+* Web browser used:
+* Icinga 2 version used (`icinga2 --version`):
+* PHP version used (`php --version`):
+* Server operating system and version:
+
+## Additional context
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..a7621fd
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+## Is your feature request related to a problem? Please describe.
+A clear and concise description of what the problem is. Ex. I'm always using this feature but am missing [...]
+
+## Describe the solution you'd like
+A clear and concise description of what you want to happen.
+
+## Describe alternatives you've considered
+A clear and concise description of any alternative solutions or features you've considered.
+
+## Additional context
+Add any other context or screenshots about the feature request here.
diff --git a/.github/workflows/L10n-update.yml b/.github/workflows/L10n-update.yml
new file mode 100644
index 0000000..45599e8
--- /dev/null
+++ b/.github/workflows/L10n-update.yml
@@ -0,0 +1,20 @@
+name: L10n Update
+
+on:
+ push:
+ branches:
+ - main
+
+jobs:
+ trigger-update:
+ name: L10n Update Trigger
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Repository dispatch
+ uses: peter-evans/repository-dispatch@v1
+ with:
+ token: ${{ secrets.ICINGABOT_TOKEN }}
+ repository: Icinga/L10n
+ event-type: update
+ client-payload: '{"origin": "${{ github.repository }}", "commit": "${{ github.sha }}"}'
diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml
new file mode 100644
index 0000000..37078bb
--- /dev/null
+++ b/.github/workflows/php.yml
@@ -0,0 +1,95 @@
+name: PHP Tests
+
+on:
+ push:
+ branches:
+ - main
+ - release/*
+ pull_request:
+ branches:
+ - main
+
+jobs:
+ lint:
+ name: Static analysis for php ${{ matrix.php }} on ${{ matrix.os }}
+ runs-on: ${{ matrix.os }}
+
+ strategy:
+ fail-fast: false
+ matrix:
+ php: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2']
+ os: ['ubuntu-latest']
+
+ steps:
+ - name: Checkout code base
+ uses: actions/checkout@v3
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ tools: phpcs
+
+ - name: Setup dependencies
+ run: |
+ composer require -n --no-progress overtrue/phplint
+ git clone --depth 1 https://github.com/Icinga/icingaweb2.git vendor/icingaweb2
+ git clone --depth 1 https://github.com/Icinga/icingadb-web.git vendor/icingadb-web
+ git clone --depth 1 https://github.com/Icinga/icingaweb2-module-director.git vendor/icingaweb2-module-director
+ git clone --depth 1 -b snapshot/nightly https://github.com/Icinga/icinga-php-library.git vendor/icinga-php-library
+ git clone --depth 1 -b snapshot/nightly https://github.com/Icinga/icinga-php-thirdparty.git vendor/icinga-php-thirdparty
+
+ - name: PHP Lint
+ if: ${{ ! cancelled() }}
+ run: ./vendor/bin/phplint -n --exclude={^vendor/.*} -- .
+
+ - name: PHP CodeSniffer
+ if: ${{ ! cancelled() }}
+ run: phpcs
+
+ - name: PHPStan
+ if: ${{ ! cancelled() }}
+ uses: php-actions/phpstan@v3
+
+ test:
+ name: Unit tests with php ${{ matrix.php }} on ${{ matrix.os }}
+ runs-on: ${{ matrix.os }}
+
+ env:
+ phpunit-version: 8.5
+
+ strategy:
+ fail-fast: false
+ matrix:
+ php: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2']
+ os: ['ubuntu-latest']
+
+ steps:
+ - name: Checkout code base
+ uses: actions/checkout@v3
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ tools: phpunit:${{ matrix.phpunit-version || env.phpunit-version }}
+
+ - name: Setup Icinga Web
+ run: |
+ git clone --depth 1 https://github.com/Icinga/icingaweb2.git _icingaweb2
+ ln -s `pwd` _icingaweb2/modules/businessprocess
+
+ - name: Setup Libraries
+ run: |
+ mkdir _libraries
+ git clone --depth 1 -b snapshot/nightly https://github.com/Icinga/icinga-php-library.git _libraries/ipl
+ git clone --depth 1 -b snapshot/nightly https://github.com/Icinga/icinga-php-thirdparty.git _libraries/vendor
+
+ - name: Setup dependencies
+ run: composer require -d _icingaweb2 -n --no-progress mockery/mockery
+
+ - name: PHPUnit
+ env:
+ ICINGAWEB_LIBDIR: _libraries
+ ICINGAWEB_CONFIGDIR: test/config
+ run: phpunit --verbose --bootstrap _icingaweb2/test/php/bootstrap.php
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d159169
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..2caf704
--- /dev/null
+++ b/README.md
@@ -0,0 +1,47 @@
+# Icinga Business Process Modeling
+
+[![PHP Support](https://img.shields.io/badge/php-%3E%3D%207.2-777BB4?logo=PHP)](https://php.net/)
+[![Build Status](https://github.com/Icinga/icingaweb2-module-businessprocess/actions/workflows/php.yml/badge.svg)](https://github.com/Icinga/icingaweb2-module-businessprocess/actions/workflows/php.yml)
+[![Github Tag](https://img.shields.io/github/tag/Icinga/icingaweb2-module-businessprocess.svg)](https://github.com/Icinga/icingaweb2-module-businessprocess/releases/latest)
+
+![Icinga Logo](https://icinga.com/wp-content/uploads/2014/06/icinga_logo.png)
+
+If you want to visualize and monitor hierarchical business processes based on
+any or all objects monitored by Icinga, the Icinga Web 2 business process
+module is the way to go.
+
+![Preview](doc/screenshot/00_preview/0005_readme-preview.png)
+
+Want to create custom process-based dashboards? Trigger notifications at
+process or sub-process level? Provide a quick top-level view for thousands of
+components on a single screen? That's what this module has been designed for!
+
+You're running a huge cloud, want to get rid of the monitoring noise triggered
+by your auto-scaling platform but still want to have detailed information just
+a couple of clicks away in case you need them? You will love this little module!
+
+Documentation
+-------------
+
+### Basics
+* [Installation](doc/02-Installation.md)
+* [Getting Started](doc/03-Getting-Started.md)
+* [Create your first process node](doc/04-Create-your-first-process-node.md)
+* [Importing Processes](doc/05-Importing-Processes.md)
+* [Customize Node Order](doc/06-Customize-Node-Order.md)
+* [State Overrides](doc/07-State-Overrides.md)
+* [Operators](doc/09-Operators.md)
+* [Controlling Access](doc/31-Permissions.md)
+
+### Web Components
+* [Breadcrumb](doc/12-Web-Components-Breadcrumb.md)
+* [Tile Renderer](doc/13-Web-Components-Tile-Renderer.md)
+* [Tree Renderer](doc/14-Web-Components-Tree-Renderer.md)
+* [Show Processes on a Dashboard](doc/16-Add-To-Dashboard.md)
+
+### Storage
+* [Store your Configuration](doc/21-Store-Config.md)
+* [Upload an existing Configuration](doc/22-Upload-Config.md)
+
+### The Project
+* [Project History](doc/81-History.md)
diff --git a/application/clicommands/CheckCommand.php b/application/clicommands/CheckCommand.php
new file mode 100644
index 0000000..d1c561f
--- /dev/null
+++ b/application/clicommands/CheckCommand.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Clicommands;
+
+class CheckCommand extends ProcessCommand
+{
+ public function listActions()
+ {
+ return array('process');
+ }
+
+ /**
+ * 'check process' is DEPRECATED, please use 'process check' instead
+ *
+ * USAGE
+ *
+ * icingacli businessprocess check process [--config <name>] <process>
+ */
+ public function processAction()
+ {
+ $this->checkAction();
+ }
+}
diff --git a/application/clicommands/CleanupCommand.php b/application/clicommands/CleanupCommand.php
new file mode 100644
index 0000000..f0041c8
--- /dev/null
+++ b/application/clicommands/CleanupCommand.php
@@ -0,0 +1,106 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Clicommands;
+
+use Exception;
+use Icinga\Application\Logger;
+use Icinga\Application\Modules\Module;
+use Icinga\Cli\Command;
+use Icinga\Module\Businessprocess\Modification\NodeRemoveAction;
+use Icinga\Module\Businessprocess\ProvidedHook\Icingadb\IcingadbSupport;
+use Icinga\Module\Businessprocess\State\IcingaDbState;
+use Icinga\Module\Businessprocess\State\MonitoringState;
+use Icinga\Module\Businessprocess\Storage\LegacyStorage;
+
+class CleanupCommand extends Command
+{
+ /**
+ * @var LegacyStorage
+ */
+ protected $storage;
+
+ protected $defaultActionName = 'cleanup';
+
+ public function init()
+ {
+ $this->storage = LegacyStorage::getInstance();
+ }
+
+ /**
+ * Cleanup all missing monitoring nodes from the specified config name
+ * If no config name is specified, the missing nodes are cleaned from all available configs.
+ * Invalid config files and file names are ignored
+ *
+ * USAGE
+ *
+ * icingacli businessprocess cleanup [<config-name>]
+ *
+ * OPTIONS
+ *
+ * <config-name>
+ */
+ public function cleanupAction(): void
+ {
+ $configNames = (array) $this->params->shift() ?: $this->storage->listAllProcessNames();
+ $foundMissingNode = false;
+ foreach ($configNames as $configName) {
+ if (! $this->storage->hasProcess($configName)) {
+ continue;
+ }
+
+ try {
+ $bp = $this->storage->loadProcess($configName);
+ } catch (Exception $e) {
+ Logger::error(
+ 'Failed to scan the %s.conf file for missing nodes. Faulty config found.',
+ $configName
+ );
+
+ continue;
+ }
+
+ if (Module::exists('icingadb')
+ && (! $bp->hasBackendName() && IcingadbSupport::useIcingaDbAsBackend())
+ ) {
+ IcingaDbState::apply($bp);
+ } else {
+ MonitoringState::apply($bp);
+ }
+
+ $removedNodes = [];
+ foreach (array_keys($bp->getMissingChildren()) as $missingNode) {
+ $node = $bp->getNode($missingNode);
+ $remove = new NodeRemoveAction($node);
+
+ try {
+ if ($remove->appliesTo($bp)) {
+ $remove->applyTo($bp);
+ $removedNodes[] = $node->getName();
+ $this->storage->storeProcess($bp);
+ $bp->clearAppliedChanges();
+
+ $foundMissingNode = true;
+ }
+ } catch (Exception $e) {
+ Logger::error(sprintf('(%s.conf) %s', $configName, $e->getMessage()));
+
+ continue;
+ }
+ }
+
+ if (! empty($removedNodes)) {
+ echo sprintf(
+ 'Removed following %d missing node(s) from %s.conf successfully:',
+ count($removedNodes),
+ $configName
+ );
+
+ echo "\n" . implode("\n", $removedNodes) . "\n\n";
+ }
+ }
+
+ if (! $foundMissingNode) {
+ echo "No missing node found.\n";
+ }
+ }
+}
diff --git a/application/clicommands/ProcessCommand.php b/application/clicommands/ProcessCommand.php
new file mode 100644
index 0000000..018c1e3
--- /dev/null
+++ b/application/clicommands/ProcessCommand.php
@@ -0,0 +1,227 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Clicommands;
+
+use Exception;
+use Icinga\Application\Logger;
+use Icinga\Application\Modules\Module;
+use Icinga\Cli\Command;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\HostNode;
+use Icinga\Module\Businessprocess\Node;
+use Icinga\Module\Businessprocess\ProvidedHook\Icingadb\IcingadbSupport;
+use Icinga\Module\Businessprocess\State\IcingaDbState;
+use Icinga\Module\Businessprocess\State\MonitoringState;
+use Icinga\Module\Businessprocess\Storage\LegacyStorage;
+
+class ProcessCommand extends Command
+{
+ /**
+ * @var LegacyStorage
+ */
+ protected $storage;
+
+ protected $hostColors = array(
+ 0 => array('black', 'lightgreen'),
+ 1 => array('lightgray', 'lightred'),
+ 2 => array('black', 'brown'),
+ 99 => array('black', 'lightgray'),
+ );
+
+ protected $serviceColors = array(
+ 0 => array('black', 'lightgreen'),
+ 1 => array('black', 'yellow'),
+ 2 => array('lightgray', 'lightred'),
+ 3 => array('black', 'lightpurple'),
+ 99 => array('black', 'lightgray'),
+ );
+
+ public function init()
+ {
+ $this->storage = LegacyStorage::getInstance();
+ }
+
+ /**
+ * List all available Business Process Configurations
+ *
+ * ...or their BusinessProcess Nodes in case a Configuration name is given
+ *
+ * USAGE
+ *
+ * icingacli businessprocess list processes [<config-name>] [options]
+ *
+ * OPTIONS
+ *
+ * <config-name>
+ * --no-title Show only names and no related title
+ */
+ public function listAction()
+ {
+ if ($config = $this->params->shift()) {
+ $this->listBpNames($this->storage->loadProcess($config));
+ } else {
+ $this->listConfigNames(! (bool) $this->params->shift('no-title'));
+ }
+ }
+
+ protected function listConfigNames($withTitle)
+ {
+ foreach ($this->storage->listProcesses() as $key => $title) {
+ if ($withTitle) {
+ echo $title . "\n";
+ } else {
+ echo $key . "\n";
+ }
+ }
+ }
+
+ /**
+ * Check a specific process
+ *
+ * USAGE
+ *
+ * icingacli businessprocess process check <process> [options]
+ *
+ * OPTIONS
+ *
+ * --config <configname> Name of the config that contains <process>
+ * --details Show problem details as a tree
+ * --colors Show colored output
+ * --state-type <type> Define which state type to look at. Could be
+ * either soft or hard, overrides an eventually
+ * configured default
+ * --blame Show problem details as a tree reduced to the
+ * nodes which have the same state as the business
+ * process
+ * --root-cause Used in combination with --blame. Only shows
+ * the path of the nodes which are responsible for
+ * the state of the business process
+ * --downtime-is-ok Treat hosts/services in downtime always as
+ * UP/OK.
+ * --ack-is-ok Treat acknowledged hosts/services always as
+ * UP/OK.
+ */
+ public function checkAction()
+ {
+ $nodeName = $this->params->shift();
+ if (! $nodeName) {
+ Logger::error('A process name is required');
+ exit(1);
+ }
+
+ $name = $this->params->get('config');
+ try {
+ if ($name === null) {
+ $name = $this->getFirstProcessName();
+ }
+
+ $bp = $this->storage->loadProcess($name);
+ } catch (Exception $err) {
+ Logger::error("Can't access configuration '%s': %s", $name, $err->getMessage());
+
+ exit(3);
+ }
+
+ if (null !== ($stateType = $this->params->get('state-type'))) {
+ if ($stateType === 'soft') {
+ $bp->useSoftStates();
+ }
+ if ($stateType === 'hard') {
+ $bp->useHardStates();
+ }
+ }
+
+ try {
+ /** @var BpNode $node */
+ $node = $bp->getNode($nodeName);
+ if (Module::exists('icingadb')
+ && (! $bp->hasBackendName() && IcingadbSupport::useIcingaDbAsBackend())
+ ) {
+ IcingaDbState::apply($bp);
+ } else {
+ MonitoringState::apply($bp);
+ }
+
+ if ($bp->hasErrors()) {
+ Logger::error("Checking Business Process '%s' failed: %s\n", $name, $bp->getErrors());
+
+ exit(3);
+ }
+ } catch (Exception $err) {
+ Logger::error("Checking Business Process '%s' failed: %s", $name, $err);
+
+ exit(3);
+ }
+
+ if ($this->params->shift('ack-is-ok')) {
+ Node::setAckIsOk();
+ }
+
+ if ($this->params->shift('downtime-is-ok')) {
+ Node::setDowntimeIsOk();
+ }
+
+ printf("Business Process %s: %s\n", $node->getStateName(), $node->getAlias());
+ if ($this->params->shift('details')) {
+ echo $this->renderProblemTree($node->getProblemTree(), $this->params->shift('colors'));
+ }
+ if ($this->params->shift('blame')) {
+ echo $this->renderProblemTree(
+ $node->getProblemTreeBlame($this->params->shift('root-cause')),
+ $this->params->shift('colors')
+ );
+ }
+
+ exit($node->getState());
+ }
+
+ protected function listBpNames(BpConfig $config)
+ {
+ foreach ($config->listBpNodes() as $title) {
+ echo $title . "\n";
+ }
+ }
+
+ protected function renderProblemTree($tree, $useColors = false, $depth = 0, BpNode $parent = null)
+ {
+ $output = '';
+
+ foreach ($tree as $name => $subtree) {
+ /** @var Node $node */
+ $node = $subtree['node'];
+ $state = $parent !== null ? $parent->getChildState($node) : $node->getState();
+
+ if ($node instanceof HostNode) {
+ $colors = $this->hostColors[$state];
+ } else {
+ $colors = $this->serviceColors[$state];
+ }
+
+ $state = sprintf('[%s]', $node->getStateName($state));
+ if ($useColors) {
+ $state = $this->screen->colorize($state, $colors[0], $colors[1]);
+ }
+
+ $output .= sprintf(
+ "%s%s %s %s\n",
+ str_repeat(' ', $depth),
+ $node instanceof BpNode ? $node->getOperator() : '-',
+ $state,
+ $node->getAlias()
+ );
+
+ if ($node instanceof BpNode) {
+ $output .= $this->renderProblemTree($subtree['children'], $useColors, $depth + 1, $node);
+ }
+ }
+
+ return $output;
+ }
+
+ protected function getFirstProcessName()
+ {
+ $list = $this->storage->listProcessNames();
+ return key($list);
+ }
+}
diff --git a/application/controllers/HostController.php b/application/controllers/HostController.php
new file mode 100644
index 0000000..e22edde
--- /dev/null
+++ b/application/controllers/HostController.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Controllers;
+
+use Icinga\Application\Modules\Module;
+use Icinga\Module\Businessprocess\IcingaDbObject;
+use Icinga\Module\Businessprocess\ProvidedHook\Icingadb\IcingadbSupport;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Module\Monitoring\Controller;
+use Icinga\Module\Monitoring\DataView\DataView;
+use Icinga\Web\Url;
+use ipl\Stdlib\Filter;
+
+class HostController extends Controller
+{
+ /**
+ * True if business process prefers to use icingadb as backend for it's nodes
+ *
+ * @var bool
+ */
+ protected $isIcingadbPreferred;
+
+ protected function moduleInit()
+ {
+ $this->isIcingadbPreferred = Module::exists('icingadb')
+ && ! $this->params->has('backend')
+ && IcingadbSupport::useIcingaDbAsBackend();
+
+ if (! $this->isIcingadbPreferred) {
+ parent::moduleInit();
+ }
+ }
+
+ public function showAction()
+ {
+ if ($this->isIcingadbPreferred) {
+ $hostName = $this->params->shift('host');
+
+ $query = Host::on(IcingaDbObject::fetchDb());
+ IcingaDbObject::applyIcingaDbRestrictions($query);
+
+ $query->filter(Filter::equal('host.name', $hostName));
+
+ $host = $query->first();
+
+ $this->params->add('name', $hostName);
+
+ if ($host !== null) {
+ $this->redirectNow(Url::fromPath('icingadb/host')->setParams($this->params));
+ }
+ } else {
+ $hostName = $this->params->get('host');
+
+ $query = $this->backend->select()
+ ->from('hoststatus', array('host_name'))
+ ->where('host_name', $hostName);
+
+ $this->applyRestriction('monitoring/filter/objects', $query);
+ if ($query->fetchRow() !== false) {
+ $this->redirectNow(Url::fromPath('monitoring/host/show')->setParams($this->params));
+ }
+ }
+
+ $this->view->host = $hostName;
+ }
+}
diff --git a/application/controllers/IndexController.php b/application/controllers/IndexController.php
new file mode 100644
index 0000000..60ddc70
--- /dev/null
+++ b/application/controllers/IndexController.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Controllers;
+
+use Icinga\Module\Businessprocess\Web\Controller;
+use Icinga\Module\Businessprocess\Web\Component\Dashboard;
+
+class IndexController extends Controller
+{
+ /**
+ * Show an overview page
+ */
+ public function indexAction()
+ {
+ $this->setTitle($this->translate('Business Process Overview'));
+ $this->controls()->add($this->overviewTab());
+ $this->content()->add(Dashboard::create($this->Auth(), $this->storage()));
+ $this->setAutorefreshInterval(15);
+ }
+}
diff --git a/application/controllers/NodeController.php b/application/controllers/NodeController.php
new file mode 100644
index 0000000..e5c657f
--- /dev/null
+++ b/application/controllers/NodeController.php
@@ -0,0 +1,148 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Controllers;
+
+use Exception;
+use Icinga\Application\Modules\Module;
+use Icinga\Module\Businessprocess\ProvidedHook\Icingadb\IcingadbSupport;
+use Icinga\Module\Businessprocess\Renderer\Breadcrumb;
+use Icinga\Module\Businessprocess\Renderer\TileRenderer;
+use Icinga\Module\Businessprocess\Simulation;
+use Icinga\Module\Businessprocess\State\IcingaDbState;
+use Icinga\Module\Businessprocess\State\MonitoringState;
+use Icinga\Module\Businessprocess\Web\Controller;
+use Icinga\Module\Businessprocess\Web\Url;
+use ipl\Html\Html;
+use ipl\Web\Widget\Link;
+
+class NodeController extends Controller
+{
+ public function impactAction()
+ {
+ $this->setAutorefreshInterval(10);
+ $content = $this->content();
+ $this->controls()->add(
+ $this->singleTab($this->translate('Node Impact'))
+ );
+ $name = $this->params->get('name');
+ $this->addTitle($this->translate('Business Impact (%s)'), $name);
+
+ $brokenFiles = [];
+ $simulation = Simulation::fromSession($this->session());
+ foreach ($this->storage()->listProcessNames() as $configName) {
+ try {
+ $config = $this->storage()->loadProcess($configName);
+ } catch (Exception $e) {
+ $meta = $this->storage()->loadMetadata($configName);
+ $brokenFiles[$meta->get('Title')] = $configName;
+ continue;
+ }
+
+ $parents = [];
+ if ($config->hasNode($name)) {
+ foreach ($config->getNode($name)->getPaths() as $path) {
+ array_pop($path); // Remove the monitored node
+ $immediateParentName = array_pop($path); // The directly affected process
+ $parents[] = [$config->getNode($immediateParentName), $path];
+ }
+ }
+
+ $askedConfigs = [];
+ foreach ($config->getImportedNodes() as $importedNode) {
+ $importedConfig = $importedNode->getBpConfig();
+
+ if (isset($askedConfigs[$importedConfig->getName()])) {
+ continue;
+ } else {
+ $askedConfigs[$importedConfig->getName()] = true;
+ }
+
+ if ($importedConfig->hasNode($name)) {
+ $node = $importedConfig->getNode($name);
+ $nativePaths = $node->getPaths($config);
+
+ do {
+ $path = array_pop($nativePaths);
+ $importedNodePos = array_search($importedNode->getIdentifier(), $path, true);
+ if ($importedNodePos !== false) {
+ array_pop($path); // Remove the monitored node
+ $immediateParentName = array_pop($path); // The directly affected process
+ $importedPath = array_slice($path, $importedNodePos + 1);
+
+ // We may get multiple native paths. Though, only the right hand of the path
+ // is what we're interested in. The left part is not what is getting imported.
+ $antiDuplicator = join('|', $importedPath) . '|' . $immediateParentName;
+ if (isset($parents[$antiDuplicator])) {
+ continue;
+ }
+
+ foreach ($importedNode->getPaths($config) as $targetPath) {
+ if ($targetPath[count($targetPath) - 1] === $immediateParentName) {
+ array_pop($targetPath);
+ $parent = $importedNode;
+ } else {
+ $parent = $importedConfig->getNode($immediateParentName);
+ }
+
+ $parents[$antiDuplicator] = [$parent, array_merge($targetPath, $importedPath)];
+ }
+ }
+ } while (! empty($nativePaths));
+ }
+ }
+
+ if (empty($parents)) {
+ continue;
+ }
+
+ if (Module::exists('icingadb') &&
+ (! $config->getBackendName() && IcingadbSupport::useIcingaDbAsBackend())
+ ) {
+ IcingaDbState::apply($config);
+ } else {
+ MonitoringState::apply($config);
+ }
+ $config->applySimulation($simulation);
+
+ foreach ($parents as $parentAndPath) {
+ $renderer = (new TileRenderer($config, array_shift($parentAndPath)))
+ ->setUrl(Url::fromPath('businessprocess/process/show', ['config' => $configName]))
+ ->setPath(array_shift($parentAndPath));
+
+ $bc = Breadcrumb::create($renderer);
+ $bc->getAttributes()->set('data-base-target', '_next');
+ $content->add($bc);
+ }
+ }
+
+ if ($content->isEmpty()) {
+ $content->add($this->translate('No impact detected. Is this node part of a business process?'));
+ }
+
+ if (! empty($brokenFiles)) {
+ $elem = Html::tag(
+ 'ul',
+ ['class' => 'broken-files'],
+ tp(
+ 'The following business process has an invalid config file and therefore cannot be read:',
+ 'The following business processes have invalid config files and therefore cannot be read:',
+ count($brokenFiles)
+ )
+ );
+
+ foreach ($brokenFiles as $bpName => $fileName) {
+ $elem->addHtml(
+ Html::tag(
+ 'li',
+ new Link(
+ sprintf('%s (%s.conf)', $bpName, $fileName),
+ \ipl\Web\Url::fromPath('businessprocess/process/show', ['config' => $fileName])
+ )
+ )
+ );
+ }
+
+ $content->addHtml($elem);
+ }
+ }
+}
diff --git a/application/controllers/ProcessController.php b/application/controllers/ProcessController.php
new file mode 100644
index 0000000..208c91e
--- /dev/null
+++ b/application/controllers/ProcessController.php
@@ -0,0 +1,780 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Controllers;
+
+use Icinga\Application\Modules\Module;
+use Icinga\Date\DateFormatter;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\Forms\AddNodeForm;
+use Icinga\Module\Businessprocess\Forms\EditNodeForm;
+use Icinga\Module\Businessprocess\Node;
+use Icinga\Module\Businessprocess\ProvidedHook\Icingadb\IcingadbSupport;
+use Icinga\Module\Businessprocess\Renderer\Breadcrumb;
+use Icinga\Module\Businessprocess\Renderer\Renderer;
+use Icinga\Module\Businessprocess\Renderer\TileRenderer;
+use Icinga\Module\Businessprocess\Renderer\TreeRenderer;
+use Icinga\Module\Businessprocess\Simulation;
+use Icinga\Module\Businessprocess\State\IcingaDbState;
+use Icinga\Module\Businessprocess\State\MonitoringState;
+use Icinga\Module\Businessprocess\Storage\ConfigDiff;
+use Icinga\Module\Businessprocess\Storage\LegacyConfigRenderer;
+use Icinga\Module\Businessprocess\Web\Component\ActionBar;
+use Icinga\Module\Businessprocess\Web\Component\RenderedProcessActionBar;
+use Icinga\Module\Businessprocess\Web\Component\Tabs;
+use Icinga\Module\Businessprocess\Web\Controller;
+use Icinga\Util\Json;
+use Icinga\Web\Notification;
+use Icinga\Web\Url;
+use Icinga\Web\Widget\Tabextension\DashboardAction;
+use Icinga\Web\Widget\Tabextension\OutputFormat;
+use ipl\Html\Form;
+use ipl\Html\Html;
+use ipl\Html\HtmlElement;
+use ipl\Html\HtmlString;
+use ipl\Html\TemplateString;
+use ipl\Html\Text;
+use ipl\Web\Control\SortControl;
+use ipl\Web\FormElement\TermInput;
+use ipl\Web\Widget\Link;
+use ipl\Web\Widget\Icon;
+
+class ProcessController extends Controller
+{
+ /** @var Renderer */
+ protected $renderer;
+
+ /**
+ * Create a new Business Process Configuration
+ */
+ public function createAction()
+ {
+ $this->assertPermission('businessprocess/create');
+
+ $title = $this->translate('Create a new Business Process');
+ $this->setTitle($title);
+ $this->controls()
+ ->add($this->tabsForCreate()->activate('create'))
+ ->add(Html::tag('h1', null, $title));
+
+ $this->content()->add(
+ $this->loadForm('bpConfig')
+ ->setStorage($this->storage())
+ ->setSuccessUrl('businessprocess/process/show')
+ ->handleRequest()
+ );
+ }
+
+ /**
+ * Upload an existing Business Process Configuration
+ */
+ public function uploadAction()
+ {
+ $this->assertPermission('businessprocess/create');
+
+ $title = $this->translate('Upload a Business Process Config file');
+ $this->setTitle($title);
+ $this->controls()
+ ->add($this->tabsForCreate()->activate('upload'))
+ ->add(Html::tag('h1', null, $title));
+
+ $this->content()->add(
+ $this->loadForm('BpUpload')
+ ->setStorage($this->storage())
+ ->setSuccessUrl('businessprocess/process/show')
+ ->handleRequest()
+ );
+ }
+
+ /**
+ * Show a business process
+ */
+ public function showAction()
+ {
+ $bp = $this->loadModifiedBpConfig();
+ $node = $this->getNode($bp);
+
+ if (Module::exists('icingadb') &&
+ (! $bp->hasBackendName() && IcingadbSupport::useIcingaDbAsBackend())
+ ) {
+ IcingaDbState::apply($bp);
+ } else {
+ MonitoringState::apply($bp);
+ }
+
+ $this->handleSimulations($bp);
+
+ $this->setTitle($this->translate('Business Process "%s"'), $bp->getTitle());
+
+ $renderer = $this->prepareRenderer($bp, $node);
+
+ if (! $this->showFullscreen && ($node === null || ! $renderer->rendersImportedNode())) {
+ if ($this->params->get('unlocked')) {
+ $renderer->unlock();
+ }
+
+ if ($bp->isEmpty() && $renderer->isLocked()) {
+ $this->redirectNow($this->url()->with('unlocked', true));
+ }
+ }
+
+ $this->handleFormatRequest($bp, $node);
+
+ $this->prepareControls($bp, $renderer);
+
+ $this->tabs()->extend(new OutputFormat());
+
+ $this->content()->add($this->showHints($bp, $renderer));
+ $this->content()->add($this->showWarnings($bp));
+ $this->content()->add($this->showErrors($bp));
+ $this->content()->add($renderer);
+ $this->loadActionForm($bp, $node);
+ $this->setDynamicAutorefresh();
+ }
+
+ /**
+ * Create a sort control and apply its sort specification to the given renderer
+ *
+ * @param Renderer $renderer
+ * @param BpConfig $config
+ *
+ * @return SortControl
+ */
+ protected function createBpSortControl(Renderer $renderer, BpConfig $config): SortControl
+ {
+ $defaultSort = $this->session()->get('sort.default', $renderer->getDefaultSort());
+ $options = [
+ 'display_name asc' => $this->translate('Name'),
+ 'state desc' => $this->translate('State')
+ ];
+ if ($config->getMetadata()->isManuallyOrdered()) {
+ $options['manual asc'] = $this->translate('Manual');
+ } elseif ($defaultSort === 'manual desc') {
+ $defaultSort = $renderer->getDefaultSort();
+ }
+
+ $sortControl = SortControl::create($options)
+ ->setDefault($defaultSort)
+ ->setMethod('POST')
+ ->setAttribute('name', 'bp-sort-control')
+ ->on(Form::ON_SUCCESS, function (SortControl $sortControl) use ($renderer) {
+ $sort = $sortControl->getSort();
+ if ($sort === $renderer->getDefaultSort()) {
+ $this->session()->delete('sort.default');
+ $url = Url::fromRequest()->without($sortControl->getSortParam());
+ } else {
+ $this->session()->set('sort.default', $sort);
+ $url = Url::fromRequest()->with($sortControl->getSortParam(), $sort);
+ }
+
+ $this->redirectNow($url);
+ })->handleRequest($this->getServerRequest());
+
+ $renderer->setSort($sortControl->getSort());
+ $this->params->shift($sortControl->getSortParam());
+
+ return $sortControl;
+ }
+
+ protected function prepareControls($bp, $renderer)
+ {
+ $controls = $this->controls();
+
+ if ($this->showFullscreen) {
+ $controls->getAttributes()->add('class', 'want-fullscreen');
+ $controls->add(Html::tag(
+ 'a',
+ [
+ 'href' => $this->url()->without('showFullscreen')->without('view'),
+ 'title' => $this->translate('Leave full screen and switch back to normal mode')
+ ],
+ new Icon('down-left-and-up-right-to-center')
+ ));
+ }
+
+ if (! ($this->showFullscreen || $this->view->compact)) {
+ $controls->add($this->getProcessTabs($bp, $renderer));
+ $controls->getAttributes()->add('class', 'separated');
+ }
+
+ $controls->add(Breadcrumb::create(clone $renderer));
+ if (! $this->showFullscreen && ! $this->view->compact) {
+ $controls->add(
+ new RenderedProcessActionBar($bp, $renderer, $this->url())
+ );
+ }
+
+ $controls->addHtml($this->createBpSortControl($renderer, $bp));
+ }
+
+ protected function getNode(BpConfig $bp)
+ {
+ if ($nodeName = $this->params->get('node')) {
+ return $bp->getNode($nodeName);
+ } else {
+ return null;
+ }
+ }
+
+ protected function prepareRenderer($bp, $node)
+ {
+ if ($this->renderer === null) {
+ if ($this->params->get('mode') === 'tree') {
+ $renderer = new TreeRenderer($bp, $node);
+ } else {
+ $renderer = new TileRenderer($bp, $node);
+ }
+ $renderer->setUrl($this->url())
+ ->setPath($this->params->getValues('path'));
+
+ $this->renderer = $renderer;
+ }
+
+ return $this->renderer;
+ }
+
+ protected function getProcessTabs(BpConfig $bp, Renderer $renderer)
+ {
+ $tabs = $this->singleTab($bp->getTitle());
+ if ($renderer->isLocked()) {
+ $tabs->extend(new DashboardAction());
+ }
+
+ return $tabs;
+ }
+
+ protected function handleSimulations(BpConfig $bp)
+ {
+ $simulation = Simulation::fromSession($this->session());
+
+ if ($this->params->get('dismissSimulations')) {
+ Notification::success(
+ sprintf(
+ $this->translate('%d applied simulation(s) have been dropped'),
+ $simulation->count()
+ )
+ );
+ $simulation->clear();
+ $this->redirectNow($this->url()->without('dismissSimulations')->without('unlocked'));
+ }
+
+ $bp->applySimulation($simulation);
+ }
+
+ protected function loadActionForm(BpConfig $bp, Node $node = null)
+ {
+ $action = $this->params->get('action');
+ $form = null;
+ if ($this->showFullscreen) {
+ return;
+ }
+
+ $canEdit = $bp->getMetadata()->canModify();
+
+ if ($action === 'add' && $canEdit) {
+ $form = (new AddNodeForm())
+ ->setProcess($bp)
+ ->setParentNode($node)
+ ->setStorage($this->storage())
+ ->setSession($this->session())
+ ->on(AddNodeForm::ON_SUCCESS, function () {
+ $this->redirectNow(Url::fromRequest()->without('action'));
+ })
+ ->handleRequest($this->getServerRequest());
+
+ if ($form->hasElement('children')) {
+ /** @var TermInput $childrenElement */
+ $childrenElement = $form->getElement('children');
+ foreach ($childrenElement->prepareMultipartUpdate($this->getServerRequest()) as $update) {
+ if (! is_array($update)) {
+ $update = [$update];
+ }
+
+ $this->addPart(...$update);
+ }
+ }
+ } elseif ($action === 'cleanup' && $canEdit) {
+ $form = $this->loadForm('CleanupNode')
+ ->setSuccessUrl(Url::fromRequest()->without('action'))
+ ->setProcess($bp)
+ ->setSession($this->session())
+ ->handleRequest();
+ } elseif ($action === 'editmonitored' && $canEdit) {
+ $form = (new EditNodeForm())
+ ->setProcess($bp)
+ ->setNode($bp->getNode($this->params->get('editmonitorednode')))
+ ->setParentNode($node)
+ ->setSession($this->session())
+ ->on(EditNodeForm::ON_SUCCESS, function () {
+ $this->redirectNow(Url::fromRequest()->without(['action', 'editmonitorednode']));
+ })
+ ->handleRequest($this->getServerRequest());
+ } elseif ($action === 'delete' && $canEdit) {
+ $form = $this->loadForm('DeleteNode')
+ ->setSuccessUrl(Url::fromRequest()->without('action'))
+ ->setProcess($bp)
+ ->setNode($bp->getNode($this->params->get('deletenode')))
+ ->setParentNode($node)
+ ->setSession($this->session())
+ ->handleRequest();
+ } elseif ($action === 'edit' && $canEdit) {
+ $form = $this->loadForm('Process')
+ ->setSuccessUrl(Url::fromRequest()->without('action'))
+ ->setProcess($bp)
+ ->setNode($bp->getNode($this->params->get('editnode')))
+ ->setSession($this->session())
+ ->handleRequest();
+ } elseif ($action === 'simulation') {
+ $form = $this->loadForm('simulation')
+ ->setSuccessUrl(Url::fromRequest()->without('action'))
+ ->setNode($bp->getNode($this->params->get('simulationnode')))
+ ->setSimulation(Simulation::fromSession($this->session()))
+ ->handleRequest();
+ } elseif ($action === 'move') {
+ $successUrl = $this->url()->without(['action', 'movenode']);
+ if ($this->params->get('mode') === 'tree') {
+ // If the user moves a node from a subtree, the `node` param exists
+ $successUrl->getParams()->remove('node');
+ }
+
+ if ($this->session()->get('sort.default')) {
+ // If there's a default sort specification in the session, it can only be `display_name desc`,
+ // as otherwise the user wouldn't be able to trigger this action. So it's safe to just define
+ // descending manual order now.
+ $successUrl->getParams()->add(SortControl::DEFAULT_SORT_PARAM, 'manual desc');
+ }
+
+ $form = $this->loadForm('MoveNode')
+ ->setSuccessUrl($successUrl)
+ ->setProcess($bp)
+ ->setParentNode($node)
+ ->setSession($this->session())
+ ->setNode($bp->getNode($this->params->get('movenode')))
+ ->handleRequest();
+ }
+
+ if ($form) {
+ $this->content()->prepend(HtmlString::create((string) $form));
+ }
+ }
+
+ protected function setDynamicAutorefresh()
+ {
+ if (! $this->isXhr()) {
+ // This will trigger the very first XHR refresh immediately on page
+ // load. Please not that this may hammer the server in case we would
+ // decide to use autorefreshInterval for HTML meta-refreshes also.
+ $this->setAutorefreshInterval(1);
+ return;
+ }
+
+ if ($this->params->has('action')) {
+ if ($this->params->get('action') !== 'add') {
+ // The new add form uses the term input, which doesn't support value persistence across refreshes
+ $this->setAutorefreshInterval(45);
+ }
+ } else {
+ $this->setAutorefreshInterval(10);
+ }
+ }
+
+ protected function showWarnings(BpConfig $bp)
+ {
+ if ($bp->hasWarnings()) {
+ $ul = Html::tag('ul', array('class' => 'warning'));
+ foreach ($bp->getWarnings() as $warning) {
+ $ul->add(Html::tag('li')->setContent($warning));
+ }
+
+ return $ul;
+ } else {
+ return null;
+ }
+ }
+
+ protected function showErrors(BpConfig $bp)
+ {
+ if ($bp->hasWarnings()) {
+ $ul = Html::tag('ul', array('class' => 'error'));
+ foreach ($bp->getErrors() as $msg) {
+ $ul->add(Html::tag('li')->setContent($msg));
+ }
+
+ return $ul;
+ } else {
+ return null;
+ }
+ }
+
+ protected function showHints(BpConfig $bp, Renderer $renderer)
+ {
+ $ul = Html::tag('ul', ['class' => 'error']);
+ $this->prepareMissingNodeLinks($ul);
+ foreach ($bp->getErrors() as $error) {
+ $ul->addHtml(Html::tag('li', $error));
+ }
+
+ if ($bp->hasChanges()) {
+ $li = Html::tag('li')->setSeparator(' ');
+ $li->add(sprintf(
+ $this->translate('This process has %d pending change(s).'),
+ $bp->countChanges()
+ ))->add(Html::tag(
+ 'a',
+ [
+ 'href' => Url::fromPath('businessprocess/process/config')
+ ->setParams($this->getRequest()->getUrl()->getParams())
+ ],
+ $this->translate('Store')
+ ))->add(Html::tag(
+ 'a',
+ ['href' => $this->url()->with('dismissChanges', true)],
+ $this->translate('Dismiss')
+ ));
+ $ul->add($li);
+ }
+
+ if ($bp->hasSimulations()) {
+ $li = Html::tag('li')->setSeparator(' ');
+ $li->add(sprintf(
+ $this->translate('This process shows %d simulated state(s).'),
+ $bp->countSimulations()
+ ))->add(Html::tag(
+ 'a',
+ ['href' => $this->url()->with('dismissSimulations', true)],
+ $this->translate('Dismiss')
+ ));
+ $ul->add($li);
+ }
+
+ if (! $renderer->isLocked() && $renderer->appliesCustomSorting()) {
+ $ul->addHtml(Html::tag('li', null, [
+ Text::create($this->translate('Drag&Drop disabled. Custom sort order applied.')),
+ (new Form())
+ ->setAttribute('class', 'inline')
+ ->addElement('submitButton', SortControl::DEFAULT_SORT_PARAM, [
+ 'label' => $this->translate('Reset to default'),
+ 'value' => $renderer->getDefaultSort(),
+ 'class' => 'link-button'
+ ])
+ ->addElement('hidden', 'uid', ['value' => 'bp-sort-control'])
+ ])->setSeparator(' '));
+ }
+
+ if (! $ul->isEmpty()) {
+ return $ul;
+ } else {
+ return null;
+ }
+ }
+
+ protected function prepareMissingNodeLinks(HtmlElement $ul): void
+ {
+ $missing = array_keys($this->bp->getMissingChildren());
+ if (! empty($missing)) {
+ $missingLinkedNodes = null;
+ foreach ($this->bp->getImportedNodes() as $process) {
+ if ($process->hasMissingChildren()) {
+ $missingLinkedNodes = array_keys($process->getMissingChildren());
+ $link = Url::fromPath('businessprocess/process/show')
+ ->addParams(['config' => $process->getConfigName()]);
+
+ $ul->addHtml(Html::tag(
+ 'li',
+ [
+ TemplateString::create(
+ tp(
+ 'Linked node %s has one missing child node: {{#link}}Show{{/link}}',
+ 'Linked node %s has %d missing child nodes: {{#link}}Show{{/link}}',
+ count($missingLinkedNodes)
+ ),
+ $process->getAlias(),
+ count($missingLinkedNodes),
+ ['link' => new Link(null, (string) $link)]
+ )
+ ]
+ ));
+ }
+ }
+
+ if (! empty($missingLinkedNodes)) {
+ return;
+ }
+
+ $count = count($missing);
+ if ($count > 10) {
+ $missing = array_slice($missing, 0, 10);
+ $missing[] = '...';
+ }
+
+ $link = Url::fromPath('businessprocess/process/show')
+ ->addParams(['config' => $this->bp->getName(), 'action' => 'cleanup']);
+
+ $ul->addHtml(Html::tag(
+ 'li',
+ [
+ TemplateString::create(
+ tp(
+ '{{#link}}Cleanup{{/link}} one missing node: %2$s',
+ '{{#link}}Cleanup{{/link}} %d missing nodes: %s',
+ count($missing)
+ ),
+ ['link' => new Link(null, (string) $link)],
+ $count,
+ implode(', ', $missing)
+ )
+ ]
+ ));
+ }
+ }
+
+ /**
+ * Show the source code for a process
+ */
+ public function sourceAction()
+ {
+ $this->assertPermission('businessprocess/modify');
+
+ $bp = $this->loadModifiedBpConfig();
+ $this->view->showDiff = $showDiff = (bool) $this->params->get('showDiff', false);
+
+ $this->view->source = LegacyConfigRenderer::renderConfig($bp);
+ if ($this->view->showDiff) {
+ $this->view->diff = ConfigDiff::create(
+ $this->storage()->getSource($this->view->configName),
+ $this->view->source
+ );
+ $title = sprintf(
+ $this->translate('%s: Source Code Differences'),
+ $bp->getTitle()
+ );
+ } else {
+ $title = sprintf(
+ $this->translate('%s: Source Code'),
+ $bp->getTitle()
+ );
+ }
+
+ $this->setTitle($title);
+ $this->controls()
+ ->add($this->tabsForConfig($bp)->activate('source'))
+ ->add(Html::tag('h1', null, $title))
+ ->add($this->createConfigActionBar($bp, $showDiff));
+
+ $this->setViewScript('process/source');
+ }
+
+ /**
+ * Download a process configuration file
+ */
+ public function downloadAction()
+ {
+ $this->assertPermission('businessprocess/modify');
+
+ $config = $this->loadModifiedBpConfig();
+ $response = $this->getResponse();
+ $response->setHeader(
+ 'Content-Disposition',
+ sprintf(
+ 'attachment; filename="%s.conf";',
+ $config->getName()
+ )
+ );
+ $response->setHeader('Content-Type', 'text/plain');
+
+ echo LegacyConfigRenderer::renderConfig($config);
+ $this->doNotRender();
+ }
+
+ /**
+ * Modify a business process configuration
+ */
+ public function configAction()
+ {
+ $this->assertPermission('businessprocess/modify');
+
+ $bp = $this->loadModifiedBpConfig();
+
+ $title = sprintf(
+ $this->translate('%s: Configuration'),
+ $bp->getTitle()
+ );
+ $this->setTitle($title);
+ $this->controls()
+ ->add($this->tabsForConfig($bp)->activate('config'))
+ ->add(Html::tag('h1', null, $title))
+ ->add($this->createConfigActionBar($bp));
+
+ $url = Url::fromPath('businessprocess/process/show')
+ ->setParams($this->getRequest()->getUrl()->getParams());
+ $this->content()->add(
+ $this->loadForm('bpConfig')
+ ->setProcess($bp)
+ ->setStorage($this->storage())
+ ->setSuccessUrl($url)
+ ->handleRequest()
+ );
+ }
+
+ protected function createConfigActionBar(BpConfig $config, $showDiff = false)
+ {
+ $actionBar = new ActionBar();
+
+ if ($showDiff) {
+ $params = array('config' => $config->getName());
+ $actionBar->add(Html::tag(
+ 'a',
+ [
+ 'href' => Url::fromPath('businessprocess/process/source', $params),
+ 'title' => $this->translate('Show source code')
+ ],
+ [
+ new Icon('file-lines'),
+ $this->translate('Source'),
+ ]
+ ));
+ } else {
+ $params = array(
+ 'config' => $config->getName(),
+ 'showDiff' => true
+ );
+
+ $actionBar->add(Html::tag(
+ 'a',
+ [
+ 'href' => Url::fromPath('businessprocess/process/source', $params),
+ 'title' => $this->translate('Highlight changes')
+ ],
+ [
+ new Icon('shuffle'),
+ $this->translate('Diff')
+ ]
+ ));
+ }
+
+ $actionBar->add(Html::tag(
+ 'a',
+ [
+ 'href' => Url::fromPath('businessprocess/process/download', ['config' => $config->getName()]),
+ 'target' => '_blank',
+ 'title' => $this->translate('Download process configuration')
+ ],
+ [
+ new Icon('download'),
+ $this->translate('Download')
+ ]
+ ));
+
+ return $actionBar;
+ }
+
+ protected function tabsForShow()
+ {
+ return $this->tabs()->add('show', array(
+ 'label' => $this->translate('Business Process'),
+ 'url' => $this->url()
+ ));
+ }
+
+ /**
+ * @return Tabs
+ */
+ protected function tabsForCreate()
+ {
+ return $this->tabs()->add('create', array(
+ 'label' => $this->translate('Create'),
+ 'url' => 'businessprocess/process/create'
+ ))->add('upload', array(
+ 'label' => $this->translate('Upload'),
+ 'url' => 'businessprocess/process/upload'
+ ));
+ }
+
+ protected function tabsForConfig(BpConfig $config)
+ {
+ $params = array(
+ 'config' => $config->getName()
+ );
+
+ $tabs = $this->tabs()->add('config', array(
+ 'label' => $this->translate('Process Configuration'),
+ 'url' =>Url::fromPath('businessprocess/process/config', $params)
+ ));
+
+ if ($this->params->get('showDiff')) {
+ $params['showDiff'] = true;
+ }
+
+ $tabs->add('source', array(
+ 'label' => $this->translate('Source'),
+ 'url' =>Url::fromPath('businessprocess/process/source', $params)
+ ));
+
+ return $tabs;
+ }
+
+ protected function handleFormatRequest(BpConfig $bp, BpNode $node = null)
+ {
+ $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'));
+ }
+
+ switch ($desiredFormat) {
+ 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($node !== null ? $node->toArray() : $bp->toArray()))
+ ->sendResponse();
+ exit;
+ case 'csv':
+ $csv = fopen('php://temp', 'w');
+
+ fputcsv($csv, ['Path', 'Name', 'State', 'Since', 'In_Downtime']);
+
+ foreach ($node !== null ? $node->toArray(null, true) : $bp->toArray(true) as $node) {
+ $data = [$node['path'], $node['name']];
+
+ if (isset($node['state'])) {
+ $data[] = $node['state'];
+ }
+
+ if (isset($node['since'])) {
+ $data[] = DateFormatter::formatDateTime($node['since']);
+ }
+
+ if (isset($node['in_downtime'])) {
+ $data[] = $node['in_downtime'];
+ }
+
+ fputcsv($csv, $data);
+ }
+
+ $response = $this->getResponse();
+ $response
+ ->setHeader('Content-Type', 'text/csv')
+ ->setHeader('Cache-Control', 'no-store')
+ ->setHeader(
+ 'Content-Disposition',
+ 'attachment; filename=' . $this->getRequest()->getActionName() . '.csv'
+ )
+ ->sendHeaders();
+
+ rewind($csv);
+
+ fpassthru($csv);
+
+ exit;
+ }
+ }
+}
diff --git a/application/controllers/ServiceController.php b/application/controllers/ServiceController.php
new file mode 100644
index 0000000..671d00c
--- /dev/null
+++ b/application/controllers/ServiceController.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Controllers;
+
+use Icinga\Application\Modules\Module;
+use Icinga\Module\Businessprocess\IcingaDbObject;
+use Icinga\Module\Businessprocess\ProvidedHook\Icingadb\IcingadbSupport;
+use Icinga\Module\Icingadb\Model\Service;
+use Icinga\Module\Monitoring\Controller;
+use Icinga\Module\Monitoring\DataView\DataView;
+use Icinga\Web\Url;
+use ipl\Stdlib\Filter;
+
+class ServiceController extends Controller
+{
+ /**
+ * True if business process prefers to use icingadb as backend for it's nodes
+ *
+ * @var bool
+ */
+ protected $isIcingadbPreferred;
+
+ protected function moduleInit()
+ {
+ $this->isIcingadbPreferred = Module::exists('icingadb')
+ && ! $this->params->has('backend')
+ && IcingadbSupport::useIcingaDbAsBackend();
+
+ if (! $this->isIcingadbPreferred) {
+ parent::moduleInit();
+ }
+ }
+
+ public function showAction()
+ {
+ if ($this->isIcingadbPreferred) {
+ $hostName = $this->params->shift('host');
+ $serviceName = $this->params->shift('service');
+
+ $query = Service::on(IcingaDbObject::fetchDb());
+ IcingaDbObject::applyIcingaDbRestrictions($query);
+
+ $query->filter(Filter::all(
+ Filter::equal('service.name', $serviceName),
+ Filter::equal('host.name', $hostName)
+ ));
+
+ $service = $query->first();
+
+ $this->params->add('name', $serviceName);
+ $this->params->add('host.name', $hostName);
+
+ if ($service !== null) {
+ $this->redirectNow(Url::fromPath('icingadb/service')->setParams($this->params));
+ }
+ } else {
+ $hostName = $this->params->get('host');
+ $serviceName = $this->params->get('service');
+
+ $query = $this->backend->select()
+ ->from('servicestatus', array('service_description'))
+ ->where('host_name', $hostName)
+ ->where('service_description', $serviceName);
+
+ $this->applyRestriction('monitoring/filter/objects', $query);
+ if ($query->fetchRow() !== false) {
+ $this->redirectNow(Url::fromPath('monitoring/service/show')->setParams($this->params));
+ }
+ }
+
+ $this->view->host = $hostName;
+ $this->view->service = $serviceName;
+ }
+}
diff --git a/application/controllers/SuggestionsController.php b/application/controllers/SuggestionsController.php
new file mode 100644
index 0000000..9fa0331
--- /dev/null
+++ b/application/controllers/SuggestionsController.php
@@ -0,0 +1,372 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Controllers;
+
+use Exception;
+use Icinga\Data\Filter\Filter as LegacyFilter;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\HostNode;
+use Icinga\Module\Businessprocess\IcingaDbObject;
+use Icinga\Module\Businessprocess\ImportedNode;
+use Icinga\Module\Businessprocess\Monitoring\DataView\HostStatus;
+use Icinga\Module\Businessprocess\Monitoring\DataView\ServiceStatus;
+use Icinga\Module\Businessprocess\MonitoringRestrictions;
+use Icinga\Module\Businessprocess\ServiceNode;
+use Icinga\Module\Businessprocess\Web\Controller;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Module\Icingadb\Model\Service;
+use ipl\Stdlib\Filter;
+use ipl\Web\FormElement\TermInput\TermSuggestions;
+
+class SuggestionsController extends Controller
+{
+ public function processAction()
+ {
+ $ignoreList = [];
+ $forConfig = null;
+ $forParent = null;
+ if ($this->params->has('config')) {
+ $forConfig = $this->loadModifiedBpConfig();
+
+ $parentName = $this->params->get('node');
+ if ($parentName) {
+ $forParent = $forConfig->getBpNode($parentName);
+
+ $collectParents = function ($node) use ($ignoreList, &$collectParents) {
+ foreach ($node->getParents() as $parent) {
+ $ignoreList[$parent->getName()] = true;
+
+ if ($parent->hasParents()) {
+ $collectParents($parent);
+ }
+ }
+ };
+
+ $ignoreList[$parentName] = true;
+ if ($forParent->hasParents()) {
+ $collectParents($forParent);
+ }
+
+ foreach ($forParent->getChildNames() as $name) {
+ $ignoreList[$name] = true;
+ }
+ }
+ }
+
+ $suggestions = new TermSuggestions((function () use ($forConfig, $forParent, $ignoreList, &$suggestions) {
+ foreach ($this->storage()->listProcessNames() as $config) {
+ $differentConfig = false;
+ if ($forConfig === null || $config !== $forConfig->getName()) {
+ if ($forConfig !== null && $forParent === null) {
+ continue;
+ }
+
+ try {
+ $bp = $this->storage()->loadProcess($config);
+ } catch (Exception $_) {
+ continue;
+ }
+
+ $differentConfig = true;
+ } else {
+ $bp = $forConfig;
+ }
+
+ foreach ($bp->getBpNodes() as $bpNode) {
+ /** @var BpNode $bpNode */
+ if ($bpNode instanceof ImportedNode) {
+ continue;
+ }
+
+ $search = $bpNode->getName();
+ if ($differentConfig) {
+ $search = "@$config:$search";
+ }
+
+ if (in_array($search, $suggestions->getExcludeTerms(), true)
+ || isset($ignoreList[$search])
+ || ($forParent
+ ? $forParent->hasChild($search)
+ : ($forConfig && $forConfig->hasRootNode($search))
+ )
+ ) {
+ continue;
+ }
+
+ if ($suggestions->matchSearch($bpNode->getName())
+ || (! $bpNode->hasAlias() || $suggestions->matchSearch($bpNode->getAlias()))
+ || $bpNode->getName() === $suggestions->getOriginalSearchValue()
+ || $bpNode->getAlias() === $suggestions->getOriginalSearchValue()
+ ) {
+ yield [
+ 'search' => $search,
+ 'label' => $bpNode->getAlias() ?? $bpNode->getName(),
+ 'config' => $config
+ ];
+ }
+ }
+ }
+ })());
+ $suggestions->setGroupingCallback(function (array $data) {
+ return $this->storage()->loadMetadata($data['config'])->getTitle();
+ });
+
+ $this->getDocument()->addHtml($suggestions->forRequest($this->getServerRequest()));
+ }
+
+ public function icingadbHostAction()
+ {
+ $excludes = Filter::none();
+ $forConfig = null;
+ if ($this->params->has('config')) {
+ $forConfig = $this->loadModifiedBpConfig();
+
+ if ($this->params->has('node')) {
+ $nodeName = $this->params->get('node');
+ $node = $forConfig->getBpNode($nodeName);
+
+ foreach ($node->getChildren() as $child) {
+ if ($child instanceof HostNode) {
+ $excludes->add(Filter::equal('host.name', $child->getHostname()));
+ }
+ }
+ }
+ }
+
+ $suggestions = new TermSuggestions((function () use ($forConfig, $excludes, &$suggestions) {
+ foreach ($suggestions->getExcludeTerms() as $excludeTerm) {
+ [$hostName, $_] = BpConfig::splitNodeName($excludeTerm);
+ $excludes->add(Filter::equal('host.name', $hostName));
+ }
+
+ $hosts = Host::on($forConfig->getBackend())
+ ->columns(['host.name', 'host.display_name'])
+ ->limit(50);
+ IcingaDbObject::applyIcingaDbRestrictions($hosts);
+ $hosts->filter(Filter::all(
+ $excludes,
+ Filter::any(
+ Filter::like('host.name', $suggestions->getSearchTerm()),
+ Filter::equal('host.name', $suggestions->getOriginalSearchValue()),
+ Filter::like('host.display_name', $suggestions->getSearchTerm()),
+ Filter::equal('host.display_name', $suggestions->getOriginalSearchValue()),
+ Filter::like('host.address', $suggestions->getSearchTerm()),
+ Filter::equal('host.address', $suggestions->getOriginalSearchValue()),
+ Filter::like('host.address6', $suggestions->getSearchTerm()),
+ Filter::equal('host.address6', $suggestions->getOriginalSearchValue()),
+ Filter::like('host.customvar_flat.flatvalue', $suggestions->getSearchTerm()),
+ Filter::equal('host.customvar_flat.flatvalue', $suggestions->getOriginalSearchValue()),
+ Filter::like('hostgroup.name', $suggestions->getSearchTerm()),
+ Filter::equal('hostgroup.name', $suggestions->getOriginalSearchValue())
+ )
+ ));
+ foreach ($hosts as $host) {
+ yield [
+ 'search' => BpConfig::joinNodeName($host->name, 'Hoststatus'),
+ 'label' => $host->display_name,
+ 'class' => 'host'
+ ];
+ }
+ })());
+
+ $this->getDocument()->addHtml($suggestions->forRequest($this->getServerRequest()));
+ }
+
+ public function icingadbServiceAction()
+ {
+ $excludes = Filter::none();
+ $forConfig = null;
+ if ($this->params->has('config')) {
+ $forConfig = $this->loadModifiedBpConfig();
+
+ if ($this->params->has('node')) {
+ $nodeName = $this->params->get('node');
+ $node = $forConfig->getBpNode($nodeName);
+
+ foreach ($node->getChildren() as $child) {
+ if ($child instanceof ServiceNode) {
+ $excludes->add(Filter::all(
+ Filter::equal('host.name', $child->getHostname()),
+ Filter::equal('service.name', $child->getServiceDescription())
+ ));
+ }
+ }
+ }
+ }
+
+ $suggestions = new TermSuggestions((function () use ($forConfig, $excludes, &$suggestions) {
+ foreach ($suggestions->getExcludeTerms() as $excludeTerm) {
+ [$hostName, $serviceName] = BpConfig::splitNodeName($excludeTerm);
+ if ($serviceName !== null && $serviceName !== 'Hoststatus') {
+ $excludes->add(Filter::all(
+ Filter::equal('host.name', $hostName),
+ Filter::equal('service.name', $serviceName)
+ ));
+ }
+ }
+
+ $services = Service::on($forConfig->getBackend())
+ ->columns(['host.name', 'host.display_name', 'service.name', 'service.display_name'])
+ ->limit(50);
+ IcingaDbObject::applyIcingaDbRestrictions($services);
+ $services->filter(Filter::all(
+ $excludes,
+ Filter::any(
+ Filter::like('host.name', $suggestions->getSearchTerm()),
+ Filter::equal('host.name', $suggestions->getOriginalSearchValue()),
+ Filter::like('host.display_name', $suggestions->getSearchTerm()),
+ Filter::equal('host.display_name', $suggestions->getOriginalSearchValue()),
+ Filter::like('service.name', $suggestions->getSearchTerm()),
+ Filter::equal('service.name', $suggestions->getOriginalSearchValue()),
+ Filter::like('service.display_name', $suggestions->getSearchTerm()),
+ Filter::equal('service.display_name', $suggestions->getOriginalSearchValue()),
+ Filter::like('service.customvar_flat.flatvalue', $suggestions->getSearchTerm()),
+ Filter::equal('service.customvar_flat.flatvalue', $suggestions->getOriginalSearchValue()),
+ Filter::like('servicegroup.name', $suggestions->getSearchTerm()),
+ Filter::equal('servicegroup.name', $suggestions->getOriginalSearchValue())
+ )
+ ));
+ foreach ($services as $service) {
+ yield [
+ 'class' => 'service',
+ 'search' => BpConfig::joinNodeName($service->host->name, $service->name),
+ 'label' => sprintf(
+ $this->translate('%s on %s', '<service> on <host>'),
+ $service->display_name,
+ $service->host->display_name
+ )
+ ];
+ }
+ })());
+
+ $this->getDocument()->addHtml($suggestions->forRequest($this->getServerRequest()));
+ }
+
+ public function monitoringHostAction()
+ {
+ $excludes = LegacyFilter::matchAny();
+ $forConfig = null;
+ if ($this->params->has('config')) {
+ $forConfig = $this->loadModifiedBpConfig();
+
+ if ($this->params->has('node')) {
+ $nodeName = $this->params->get('node');
+ $node = $forConfig->getBpNode($nodeName);
+
+ foreach ($node->getChildren() as $child) {
+ if ($child instanceof HostNode) {
+ $excludes->addFilter(LegacyFilter::where('host_name', $child->getHostname()));
+ }
+ }
+ }
+ }
+
+ $suggestions = new TermSuggestions((function () use ($forConfig, $excludes, &$suggestions) {
+ foreach ($suggestions->getExcludeTerms() as $excludeTerm) {
+ [$hostName, $_] = BpConfig::splitNodeName($excludeTerm);
+ $excludes->addFilter(LegacyFilter::where('host_name', $hostName));
+ }
+
+ $hosts = (new HostStatus($forConfig->getBackend()->select(), ['host_name', 'host_display_name']))
+ ->limit(50)
+ ->applyFilter(MonitoringRestrictions::getRestriction('monitoring/filter/objects'))
+ ->applyFilter(LegacyFilter::matchAny(
+ LegacyFilter::where('host_name', $suggestions->getSearchTerm()),
+ LegacyFilter::where('host_display_name', $suggestions->getSearchTerm()),
+ LegacyFilter::where('host_address', $suggestions->getSearchTerm()),
+ LegacyFilter::where('host_address6', $suggestions->getSearchTerm()),
+ LegacyFilter::where('_host_%', $suggestions->getSearchTerm()),
+ // This also forces a group by on the query, needed anyway due to the custom var filter
+ // above, which may return multiple rows because of the wildcard in the name filter.
+ LegacyFilter::where('hostgroup_name', $suggestions->getSearchTerm()),
+ LegacyFilter::where('hostgroup_alias', $suggestions->getSearchTerm())
+ ));
+ if (! $excludes->isEmpty()) {
+ $hosts->applyFilter(LegacyFilter::not($excludes));
+ }
+
+ foreach ($hosts as $row) {
+ yield [
+ 'search' => BpConfig::joinNodeName($row->host_name, 'Hoststatus'),
+ 'label' => $row->host_display_name,
+ 'class' => 'host'
+ ];
+ }
+ })());
+
+ $this->getDocument()->addHtml($suggestions->forRequest($this->getServerRequest()));
+ }
+
+ public function monitoringServiceAction()
+ {
+ $excludes = LegacyFilter::matchAny();
+ $forConfig = null;
+ if ($this->params->has('config')) {
+ $forConfig = $this->loadModifiedBpConfig();
+
+ if ($this->params->has('node')) {
+ $nodeName = $this->params->get('node');
+ $node = $forConfig->getBpNode($nodeName);
+
+ foreach ($node->getChildren() as $child) {
+ if ($child instanceof ServiceNode) {
+ $excludes->addFilter(LegacyFilter::matchAll(
+ LegacyFilter::where('host_name', $child->getHostname()),
+ LegacyFilter::where('service_description', $child->getServiceDescription())
+ ));
+ }
+ }
+ }
+ }
+
+ $suggestions = new TermSuggestions((function () use ($forConfig, $excludes, &$suggestions) {
+ foreach ($suggestions->getExcludeTerms() as $excludeTerm) {
+ [$hostName, $serviceName] = BpConfig::splitNodeName($excludeTerm);
+ if ($serviceName !== null && $serviceName !== 'Hoststatus') {
+ $excludes->addFilter(LegacyFilter::matchAll(
+ LegacyFilter::where('host_name', $hostName),
+ LegacyFilter::where('service_description', $serviceName)
+ ));
+ }
+ }
+
+ $services = (new ServiceStatus($forConfig->getBackend()->select(), [
+ 'host_name',
+ 'host_display_name',
+ 'service_description',
+ 'service_display_name'
+ ]))
+ ->limit(50)
+ ->applyFilter(MonitoringRestrictions::getRestriction('monitoring/filter/objects'))
+ ->applyFilter(LegacyFilter::matchAny(
+ LegacyFilter::where('host_name', $suggestions->getSearchTerm()),
+ LegacyFilter::where('host_display_name', $suggestions->getSearchTerm()),
+ LegacyFilter::where('service_description', $suggestions->getSearchTerm()),
+ LegacyFilter::where('service_display_name', $suggestions->getSearchTerm()),
+ LegacyFilter::where('_service_%', $suggestions->getSearchTerm()),
+ // This also forces a group by on the query, needed anyway due to the custom var filter
+ // above, which may return multiple rows because of the wildcard in the name filter.
+ LegacyFilter::where('servicegroup_name', $suggestions->getSearchTerm()),
+ LegacyFilter::where('servicegroup_alias', $suggestions->getSearchTerm())
+ ));
+ if (! $excludes->isEmpty()) {
+ $services->applyFilter(LegacyFilter::not($excludes));
+ }
+
+ foreach ($services as $row) {
+ yield [
+ 'class' => 'service',
+ 'search' => BpConfig::joinNodeName($row->host_name, $row->service_description),
+ 'label' => sprintf(
+ $this->translate('%s on %s', '<service> on <host>'),
+ $row->service_display_name,
+ $row->host_display_name
+ )
+ ];
+ }
+ })());
+
+ $this->getDocument()->addHtml($suggestions->forRequest($this->getServerRequest()));
+ }
+}
diff --git a/application/forms/AddNodeForm.php b/application/forms/AddNodeForm.php
new file mode 100644
index 0000000..3840d8a
--- /dev/null
+++ b/application/forms/AddNodeForm.php
@@ -0,0 +1,412 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Forms;
+
+use Exception;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\Common\Sort;
+use Icinga\Module\Businessprocess\Modification\ProcessChanges;
+use Icinga\Module\Businessprocess\Node;
+use Icinga\Module\Businessprocess\Storage\Storage;
+use Icinga\Module\Businessprocess\Web\Form\Element\IplStateOverrides;
+use Icinga\Module\Businessprocess\Web\Form\Validator\HostServiceTermValidator;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+use Icinga\Web\Session\SessionNamespace;
+use ipl\Html\HtmlElement;
+use ipl\Html\Text;
+use ipl\I18n\Translation;
+use ipl\Stdlib\Str;
+use ipl\Web\Compat\CompatForm;
+use ipl\Web\FormElement\TermInput;
+use ipl\Web\Url;
+
+class AddNodeForm extends CompatForm
+{
+ use Sort;
+ use Translation;
+
+ /** @var Storage */
+ protected $storage;
+
+ /** @var ?BpConfig */
+ protected $bp;
+
+ /** @var ?BpNode */
+ protected $parent;
+
+ /** @var SessionNamespace */
+ protected $session;
+
+ /**
+ * Set the storage to use
+ *
+ * @param Storage $storage
+ *
+ * @return $this
+ */
+ public function setStorage(Storage $storage): self
+ {
+ $this->storage = $storage;
+
+ return $this;
+ }
+
+ /**
+ * Set the affected configuration
+ *
+ * @param BpConfig $bp
+ *
+ * @return $this
+ */
+ public function setProcess(BpConfig $bp): self
+ {
+ $this->bp = $bp;
+
+ return $this;
+ }
+
+ /**
+ * Set the affected sub-process
+ *
+ * @param ?BpNode $node
+ *
+ * @return $this
+ */
+ public function setParentNode(BpNode $node = null): self
+ {
+ $this->parent = $node;
+
+ return $this;
+ }
+
+ /**
+ * Set the user's session
+ *
+ * @param SessionNamespace $session
+ *
+ * @return $this
+ */
+ public function setSession(SessionNamespace $session): self
+ {
+ $this->session = $session;
+
+ return $this;
+ }
+
+ protected function assemble()
+ {
+ if ($this->parent !== null) {
+ $title = sprintf($this->translate('Add a node to %s'), $this->parent->getAlias());
+ $nodeTypes = [
+ 'host' => $this->translate('Host'),
+ 'service' => $this->translate('Service'),
+ 'process' => $this->translate('Existing Process'),
+ 'new-process' => $this->translate('New Process')
+ ];
+ } else {
+ $title = $this->translate('Add a new root node');
+ if (! $this->bp->isEmpty()) {
+ $nodeTypes = [
+ 'process' => $this->translate('Existing Process'),
+ 'new-process' => $this->translate('New Process')
+ ];
+ } else {
+ $nodeTypes = [];
+ }
+ }
+
+ $this->addHtml(new HtmlElement('h2', null, Text::create($title)));
+
+ if (! empty($nodeTypes)) {
+ $this->addElement('select', 'node_type', [
+ 'label' => $this->translate('Node type'),
+ 'options' => array_merge(
+ ['' => ' - ' . $this->translate('Please choose') . ' - '],
+ $nodeTypes
+ ),
+ 'disabledOptions' => [''],
+ 'class' => 'autosubmit',
+ 'required' => true,
+ 'ignore' => true
+ ]);
+
+ $nodeType = $this->getPopulatedValue('node_type');
+ } else {
+ $nodeType = 'new-process';
+ }
+
+ if ($nodeType === 'new-process') {
+ $this->assembleNewProcessElements();
+ } elseif ($nodeType === 'process') {
+ $this->assembleExistingProcessElements();
+ } elseif ($nodeType === 'host') {
+ $this->assembleHostElements();
+ } elseif ($nodeType === 'service') {
+ $this->assembleServiceElements();
+ }
+
+ $this->addElement('submit', 'submit', [
+ 'label' => $this->translate('Add Process')
+ ]);
+ }
+
+ protected function assembleNewProcessElements(): void
+ {
+ $this->addElement('text', 'name', [
+ 'required' => true,
+ 'ignore' => true,
+ 'label' => $this->translate('ID'),
+ 'description' => $this->translate('This is the unique identifier of this process'),
+ 'validators' => [
+ 'callback' => function ($value, $validator) {
+ if ($this->parent !== null ? $this->parent->hasChild($value) : $this->bp->hasRootNode($value)) {
+ $validator->addMessage(
+ sprintf($this->translate('%s is already defined in this process'), $value)
+ );
+
+ return false;
+ }
+
+ return true;
+ }
+ ]
+ ]);
+
+ $this->addElement('text', 'alias', [
+ 'label' => $this->translate('Display Name'),
+ 'description' => $this->translate(
+ 'Usually this name will be shown for this node. Equals ID if not given'
+ ),
+ ]);
+
+ $this->addElement('select', 'operator', [
+ 'required' => true,
+ 'label' => $this->translate('Operator'),
+ 'multiOptions' => Node::getOperators()
+ ]);
+
+ $display = 1;
+ if (! $this->bp->isEmpty() && $this->bp->getMetadata()->isManuallyOrdered()) {
+ $rootNodes = self::applyManualSorting($this->bp->getRootNodes());
+ $display = end($rootNodes)->getDisplay() + 1;
+ }
+ $this->addElement('select', 'display', [
+ 'required' => true,
+ 'label' => $this->translate('Visualization'),
+ 'description' => $this->translate('Where to show this process'),
+ 'value' => $this->parent !== null ? '0' : "$display",
+ 'multiOptions' => [
+ "$display" => $this->translate('Toplevel Process'),
+ '0' => $this->translate('Subprocess only'),
+ ]
+ ]);
+
+ $this->addElement('text', 'infoUrl', [
+ 'label' => $this->translate('Info URL'),
+ 'description' => $this->translate('URL pointing to more information about this node')
+ ]);
+ }
+
+ protected function assembleExistingProcessElements(): void
+ {
+ $termValidator = function (array $terms) {
+ foreach ($terms as $term) {
+ /** @var TermInput\ValidatedTerm $term */
+ $nodeName = $term->getSearchValue();
+ if ($nodeName[0] === '@') {
+ if ($this->parent === null) {
+ $term->setMessage($this->translate('Imported nodes cannot be used as root nodes'));
+ } elseif (strpos($nodeName, ':') === false) {
+ $term->setMessage($this->translate('Missing node name'));
+ } else {
+ [$config, $nodeName] = Str::trimSplit(substr($nodeName, 1), ':', 2);
+ if (! $this->storage->hasProcess($config)) {
+ $term->setMessage($this->translate('Config does not exist or access has been denied'));
+ } else {
+ try {
+ $bp = $this->storage->loadProcess($config);
+ } catch (Exception $e) {
+ $term->setMessage(
+ sprintf($this->translate('Cannot load config: %s'), $e->getMessage())
+ );
+ }
+
+ if (isset($bp)) {
+ if (! $bp->hasNode($nodeName)) {
+ $term->setMessage($this->translate('No node with this name found in config'));
+ } else {
+ $term->setLabel($bp->getNode($nodeName)->getAlias());
+ }
+ }
+ }
+ }
+ } elseif (! $this->bp->hasNode($nodeName)) {
+ $term->setMessage($this->translate('No node with this name found in config'));
+ } else {
+ $term->setLabel($this->bp->getNode($nodeName)->getAlias());
+ }
+
+ if ($this->parent !== null && $this->parent->hasChild($term->getSearchValue())) {
+ $term->setMessage($this->translate('Already defined in this process'));
+ }
+
+ if ($this->parent !== null && $term->getSearchValue() === $this->parent->getName()) {
+ $term->setMessage($this->translate('Results in a parent/child loop'));
+ }
+ }
+ };
+
+ $this->addElement(
+ (new TermInput('children'))
+ ->setRequired()
+ ->setVerticalTermDirection()
+ ->setLabel($this->translate('Process Nodes'))
+ ->setSuggestionUrl(Url::fromPath('businessprocess/suggestions/process', [
+ 'node' => isset($this->parent) ? $this->parent->getName() : null,
+ 'config' => $this->bp->getName(),
+ 'showCompact' => true,
+ '_disableLayout' => true
+ ]))
+ ->on(TermInput::ON_ENRICH, $termValidator)
+ ->on(TermInput::ON_ADD, $termValidator)
+ ->on(TermInput::ON_PASTE, $termValidator)
+ ->on(TermInput::ON_SAVE, $termValidator)
+ );
+ }
+
+ protected function assembleHostElements(): void
+ {
+ if ($this->bp->getBackend() instanceof MonitoringBackend) {
+ $suggestionsPath = 'businessprocess/suggestions/monitoring-host';
+ } else {
+ $suggestionsPath = 'businessprocess/suggestions/icingadb-host';
+ }
+
+ $this->addElement($this->createChildrenElementForObjects(
+ $this->translate('Hosts'),
+ $suggestionsPath
+ ));
+
+ $this->addElement('checkbox', 'host_override', [
+ 'ignore' => true,
+ 'class' => 'autosubmit',
+ 'label' => $this->translate('Override Host State')
+ ]);
+ if ($this->getPopulatedValue('host_override') === 'y') {
+ $this->addElement(new IplStateOverrides('stateOverrides', [
+ 'label' => $this->translate('State Overrides'),
+ 'options' => [
+ 0 => $this->translate('UP'),
+ 1 => $this->translate('DOWN'),
+ 99 => $this->translate('PENDING')
+ ]
+ ]));
+ }
+ }
+
+ protected function assembleServiceElements(): void
+ {
+ if ($this->bp->getBackend() instanceof MonitoringBackend) {
+ $suggestionsPath = 'businessprocess/suggestions/monitoring-service';
+ } else {
+ $suggestionsPath = 'businessprocess/suggestions/icingadb-service';
+ }
+
+ $this->addElement($this->createChildrenElementForObjects(
+ $this->translate('Services'),
+ $suggestionsPath
+ ));
+
+ $this->addElement('checkbox', 'service_override', [
+ 'ignore' => true,
+ 'class' => 'autosubmit',
+ 'label' => $this->translate('Override Service State')
+ ]);
+ if ($this->getPopulatedValue('service_override') === 'y') {
+ $this->addElement(new IplStateOverrides('stateOverrides', [
+ 'label' => $this->translate('State Overrides'),
+ 'options' => [
+ 0 => $this->translate('OK'),
+ 1 => $this->translate('WARNING'),
+ 2 => $this->translate('CRITICAL'),
+ 3 => $this->translate('UNKNOWN'),
+ 99 => $this->translate('PENDING'),
+ ]
+ ]));
+ }
+ }
+
+ protected function createChildrenElementForObjects(string $label, string $suggestionsPath): TermInput
+ {
+ $termValidator = function (array $terms) {
+ (new HostServiceTermValidator())
+ ->setParent($this->parent)
+ ->isValid($terms);
+ };
+
+ return (new TermInput('children'))
+ ->setRequired()
+ ->setLabel($label)
+ ->setVerticalTermDirection()
+ ->setSuggestionUrl(Url::fromPath($suggestionsPath, [
+ 'node' => isset($this->parent) ? $this->parent->getName() : null,
+ 'config' => $this->bp->getName(),
+ 'showCompact' => true,
+ '_disableLayout' => true
+ ]))
+ ->on(TermInput::ON_ENRICH, $termValidator)
+ ->on(TermInput::ON_ADD, $termValidator)
+ ->on(TermInput::ON_PASTE, $termValidator)
+ ->on(TermInput::ON_SAVE, $termValidator);
+ }
+
+ protected function onSuccess()
+ {
+ $changes = ProcessChanges::construct($this->bp, $this->session);
+
+ $nodeType = $this->getValue('node_type');
+ if (! $nodeType || $nodeType === 'new-process') {
+ $properties = $this->getValues();
+ if (! $properties['alias']) {
+ unset($properties['alias']);
+ }
+
+ if ($this->parent !== null) {
+ $properties['parentName'] = $this->parent->getName();
+ }
+
+ $changes->createNode(BpConfig::escapeName($this->getValue('name')), $properties);
+ } else {
+ /** @var TermInput $term */
+ $term = $this->getElement('children');
+ $children = array_unique(array_map(function ($term) {
+ return $term->getSearchValue();
+ }, $term->getTerms()));
+
+ if ($nodeType === 'host' || $nodeType === 'service') {
+ $stateOverrides = $this->getValue('stateOverrides');
+ if (! empty($stateOverrides)) {
+ $childOverrides = [];
+ foreach ($children as $nodeName) {
+ $childOverrides[$nodeName] = $stateOverrides;
+ }
+
+ $changes->modifyNode($this->parent, [
+ 'stateOverrides' => array_merge($this->parent->getStateOverrides(), $childOverrides)
+ ]);
+ }
+ }
+
+ if ($this->parent !== null) {
+ $changes->addChildrenToNode($children, $this->parent);
+ } else {
+ foreach ($children as $nodeName) {
+ $changes->copyNode($nodeName);
+ }
+ }
+ }
+
+ unset($changes);
+ }
+}
diff --git a/application/forms/BpConfigForm.php b/application/forms/BpConfigForm.php
new file mode 100644
index 0000000..8a0bc95
--- /dev/null
+++ b/application/forms/BpConfigForm.php
@@ -0,0 +1,236 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Forms;
+
+use Icinga\Authentication\Auth;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\Web\Form\BpConfigBaseForm;
+
+class BpConfigForm extends BpConfigBaseForm
+{
+ protected $deleteButtonName;
+
+ public function setup()
+ {
+ $this->addElement('text', 'name', array(
+ 'label' => $this->translate('ID'),
+ 'required' => true,
+ 'validators' => array(
+ array(
+ 'validator' => 'StringLength',
+ 'options' => array(
+ 'min' => 2,
+ 'max' => 40
+ )
+ ),
+ [
+ 'validator' => 'Regex',
+ 'options' => [
+ 'pattern' => '/^[a-zA-Z0-9](?:[\w\h._-]*)?\w$/',
+ 'messages' => [
+ 'regexNotMatch' => $this->translate(
+ 'Id must only consist of alphanumeric characters.'
+ . ' Underscore at the beginning and space, dot and hyphen at the beginning'
+ . ' and end are not allowed.'
+ )
+ ]
+ ]
+ ]
+ ),
+ 'description' => $this->translate(
+ 'This is the unique identifier of this process'
+ ),
+ ));
+
+ $this->addElement('text', 'Title', array(
+ 'label' => $this->translate('Display Name'),
+ 'description' => $this->translate(
+ 'Usually this name will be shown for this process. Equals ID'
+ . ' if not given'
+ ),
+ ));
+
+ $this->addElement('textarea', 'Description', array(
+ 'label' => $this->translate('Description'),
+ 'description' => $this->translate(
+ 'A slightly more detailed description for this process, about 100-150 characters long'
+ ),
+ 'rows' => 4,
+ ));
+
+ if (! empty($this->listAvailableBackends())) {
+ $this->addElement('select', 'Backend', array(
+ 'label' => $this->translate('Backend'),
+ 'description' => $this->translate(
+ 'Icinga Web Monitoring Backend where current object states for'
+ . ' this process should be retrieved from'
+ ),
+ 'multiOptions' => array(
+ null => $this->translate('Use the configured default backend'),
+ ) + $this->listAvailableBackends()
+ ));
+ }
+
+ $this->addElement('select', 'Statetype', array(
+ 'label' => $this->translate('State Type'),
+ 'required' => true,
+ 'description' => $this->translate(
+ 'Whether this process should be based on Icinga hard or soft states'
+ ),
+ 'multiOptions' => array(
+ 'soft' => $this->translate('Use SOFT states'),
+ 'hard' => $this->translate('Use HARD states'),
+ )
+ ));
+
+ $this->addElement('select', 'AddToMenu', array(
+ 'label' => $this->translate('Add to menu'),
+ 'required' => true,
+ 'description' => $this->translate(
+ 'Whether this process should be linked in the main Icinga Web 2 menu'
+ ),
+ 'multiOptions' => array(
+ 'yes' => $this->translate('Yes'),
+ 'no' => $this->translate('No'),
+ )
+ ));
+
+ $this->addElement('text', 'AllowedUsers', array(
+ 'label' => $this->translate('Allowed Users'),
+ 'description' => $this->translate(
+ 'Allowed Users (comma-separated)'
+ ),
+ ));
+
+ $this->addElement('text', 'AllowedGroups', array(
+ 'label' => $this->translate('Allowed Groups'),
+ 'description' => $this->translate(
+ 'Allowed Groups (comma-separated)'
+ ),
+ ));
+
+ $this->addElement('text', 'AllowedRoles', array(
+ 'label' => $this->translate('Allowed Roles'),
+ 'description' => $this->translate(
+ 'Allowed Roles (comma-separated)'
+ ),
+ ));
+
+ if ($this->bp === null) {
+ $this->setSubmitLabel(
+ $this->translate('Add')
+ );
+ } else {
+ $config = $this->bp;
+
+ $meta = $config->getMetadata();
+ foreach ($meta->getProperties() as $k => $v) {
+ if ($el = $this->getElement($k)) {
+ $el->setValue($v);
+ }
+ }
+ $this->getElement('name')
+ ->setValue($config->getName())
+ ->setAttrib('readonly', true);
+
+ $this->setSubmitLabel(
+ $this->translate('Store')
+ );
+
+ $label = $this->translate('Delete');
+ $el = $this->createElement('submit', $label, array(
+ 'data-base-target' => '_main'
+ ))->setLabel($label)->setDecorators(array('ViewHelper'));
+ $this->deleteButtonName = $el->getName();
+ $this->addElement($el);
+ }
+ }
+
+ protected function onSetup()
+ {
+ $this->getElement($this->getSubmitLabel())->setAttrib('data-base-target', '_main');
+ }
+
+ protected function onRequest()
+ {
+ $name = $this->getValue('name');
+
+ if ($this->shouldBeDeleted()) {
+ if ($this->bp->isReferenced()) {
+ $this->addError(sprintf(
+ $this->translate('Process "%s" cannot be deleted as it has been referenced in other processes'),
+ $name
+ ));
+ } else {
+ $this->bp->clearAppliedChanges();
+ $this->storage->deleteProcess($name);
+ $this->setSuccessUrl('businessprocess');
+ $this->redirectOnSuccess(sprintf('Process %s has been deleted', $name));
+ }
+ }
+ }
+
+ public function onSuccess()
+ {
+ $name = $this->getValue('name');
+
+ if ($this->bp === null) {
+ if ($this->storage->hasProcess($name)) {
+ $this->addError(sprintf(
+ $this->translate('A process named "%s" already exists'),
+ $name
+ ));
+
+ return;
+ }
+
+ // New config
+ $config = new BpConfig();
+ $config->setName($name);
+
+ if (! $this->prepareMetadata($config)) {
+ return;
+ }
+
+ $this->setSuccessUrl(
+ $this->getSuccessUrl()->setParams(
+ array('config' => $name, 'unlocked' => true)
+ )
+ );
+ $this->setSuccessMessage(sprintf('Process %s has been created', $name));
+ } else {
+ $config = $this->bp;
+ $this->setSuccessMessage(sprintf('Process %s has been stored', $name));
+ }
+ $meta = $config->getMetadata();
+ foreach ($this->getValues() as $key => $value) {
+ if (! in_array($key, ['Title', 'Description', 'Backend'], true)
+ && ($value === null || $value === '')) {
+ continue;
+ }
+
+ if ($meta->hasKey($key)) {
+ $meta->set($key, $value);
+ }
+ }
+
+ $this->storage->storeProcess($config);
+ $config->clearAppliedChanges();
+ parent::onSuccess();
+ }
+
+ public function hasDeleteButton()
+ {
+ return $this->deleteButtonName !== null;
+ }
+
+ public function shouldBeDeleted()
+ {
+ if (! $this->hasDeleteButton()) {
+ return false;
+ }
+
+ $name = $this->deleteButtonName;
+ return $this->getSentValue($name) === $this->getElement($name)->getLabel();
+ }
+}
diff --git a/application/forms/BpUploadForm.php b/application/forms/BpUploadForm.php
new file mode 100644
index 0000000..a746740
--- /dev/null
+++ b/application/forms/BpUploadForm.php
@@ -0,0 +1,207 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Forms;
+
+use Exception;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\Storage\LegacyConfigParser;
+use Icinga\Module\Businessprocess\Web\Form\BpConfigBaseForm;
+use Icinga\Web\Notification;
+
+class BpUploadForm extends BpConfigBaseForm
+{
+ protected $node;
+
+ protected $objectList = array();
+
+ protected $processList = array();
+
+ protected $deleteButtonName;
+
+ private $sourceCode;
+
+ /** @var BpConfig */
+ private $uploadedConfig;
+
+ public function setup()
+ {
+ $this->showUpload();
+ if ($this->hasSource()) {
+ $this->showDetails();
+ }
+ }
+
+ protected function showDetails()
+ {
+ $this->addElement('text', 'name', array(
+ 'label' => $this->translate('Name'),
+ 'required' => true,
+ 'description' => $this->translate(
+ 'This is the unique identifier of this process'
+ ),
+ 'validators' => array(
+ array(
+ 'validator' => 'StringLength',
+ 'options' => array(
+ 'min' => 2,
+ 'max' => 40
+ )
+ ),
+ [
+ 'validator' => 'Regex',
+ 'options' => [
+ 'pattern' => '/^[a-zA-Z0-9](?:[\w\h._-]*)?\w$/',
+ 'messages' => [
+ 'regexNotMatch' => $this->translate(
+ 'Id must only consist of alphanumeric characters.'
+ . ' Underscore at the beginning and space, dot and hyphen at the beginning'
+ . ' and end are not allowed.'
+ )
+ ]
+ ]
+ ]
+ ),
+ ));
+
+ $this->addElement('textarea', 'source', array(
+ 'label' => $this->translate('Source'),
+ 'description' => $this->translate(
+ 'Business process source code'
+ ),
+ 'value' => $this->sourceCode,
+ 'class' => 'preformatted smaller',
+ 'rows' => 7,
+ ));
+
+ $this->getUploadedConfig();
+
+ $this->setSubmitLabel(
+ $this->translate('Store')
+ );
+ }
+
+ public function getUploadedConfig()
+ {
+ if ($this->uploadedConfig === null) {
+ $this->uploadedConfig = $this->parseSubmittedSourceCode();
+ }
+
+ return $this->uploadedConfig;
+ }
+
+ protected function parseSubmittedSourceCode()
+ {
+ $code = $this->getSentValue('source');
+ $name = $this->getSentValue('name', '<new config>');
+ if (empty($code)) {
+ $code = $this->sourceCode;
+ }
+
+ try {
+ $config = LegacyConfigParser::parseString($name, $code);
+
+ if ($config->hasErrors()) {
+ foreach ($config->getErrors() as $error) {
+ $this->addError($error);
+ }
+ }
+ } catch (Exception $e) {
+ $this->addError($e->getMessage());
+ return null;
+ }
+
+ return $config;
+ }
+
+ protected function hasSource()
+ {
+ if ($this->hasBeenSent() && $source = $this->getSentValue('source')) {
+ $this->sourceCode = $source;
+ } else {
+ $this->processUploadedSource();
+ }
+
+ if (empty($this->sourceCode)) {
+ return false;
+ } else {
+ $this->removeElement('uploaded_file');
+ return true;
+ }
+ }
+
+ protected function showUpload()
+ {
+ $this->setAttrib('enctype', 'multipart/form-data');
+
+ $this->addElement('file', 'uploaded_file', array(
+ 'label' => $this->translate('File'),
+ 'destination' => $this->getTempDir(),
+ 'required' => true,
+ ));
+
+ /** @var \Zend_Form_Element_File $el */
+ $el = $this->getElement('uploaded_file');
+ $el->setValueDisabled(true);
+
+ $this->setSubmitLabel(
+ $this->translate('Next')
+ );
+ }
+
+ protected function getTempDir()
+ {
+ return sys_get_temp_dir();
+ }
+
+ protected function processUploadedSource()
+ {
+ /** @var ?\Zend_Form_Element_File $el */
+ $el = $this->getElement('uploaded_file');
+
+ if ($el && $this->hasBeenSent()) {
+ $tmpdir = $this->getTempDir();
+ $tmpfile = tempnam($tmpdir, 'bpupload_');
+
+ // TODO: race condition, try to do this without unlinking here
+ unlink($tmpfile);
+
+ $el->addFilter('Rename', $tmpfile);
+ if ($el->receive()) {
+ $this->sourceCode = file_get_contents($tmpfile);
+ unlink($tmpfile);
+ } else {
+ foreach ($el->file->getMessages() as $error) {
+ $this->addError($error);
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ public function onSuccess()
+ {
+ $config = $this->getUploadedConfig();
+ $name = $config->getName();
+
+ if ($this->storage->hasProcess($name)) {
+ $this->addError(sprintf(
+ $this->translate('A process named "%s" already exists'),
+ $name
+ ));
+
+ return;
+ }
+
+ if (! $this->prepareMetadata($config)) {
+ return;
+ }
+
+ $this->storage->storeProcess($config);
+ Notification::success(sprintf('Process %s has been stored', $name));
+
+ $this->getSuccessUrl()->setParam('config', $name);
+
+ parent::onSuccess();
+ }
+}
diff --git a/application/forms/CleanupNodeForm.php b/application/forms/CleanupNodeForm.php
new file mode 100644
index 0000000..c6e5398
--- /dev/null
+++ b/application/forms/CleanupNodeForm.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Forms;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\Modification\ProcessChanges;
+use Icinga\Module\Businessprocess\Web\Form\BpConfigBaseForm;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+use Icinga\Web\Session\SessionNamespace;
+use ipl\Html\Html;
+use ipl\Sql\Connection as IcingaDbConnection;
+
+class CleanupNodeForm extends BpConfigBaseForm
+{
+ /** @var MonitoringBackend|IcingaDbConnection */
+ protected $backend;
+
+ /** @var BpConfig */
+ protected $bp;
+
+ /** @var SessionNamespace */
+ protected $session;
+
+ public function setup()
+ {
+ $this->addHtml(Html::tag('h2', $this->translate('Cleanup missing nodes')));
+
+ $this->addElement('checkbox', 'cleanup_all', [
+ 'class' => 'autosubmit',
+ 'label' => $this->translate('Cleanup all missing nodes'),
+ 'description' => $this->translate('Remove all missing nodes from config')
+ ]);
+
+ if ($this->getSentValue('cleanup_all') !== '1') {
+ $this->addElement('multiselect', 'nodes', [
+ 'label' => $this->translate('Select nodes to cleanup'),
+ 'required' => true,
+ 'size' => 8,
+ 'multiOptions' => $this->bp->getMissingChildren()
+ ]);
+ }
+ }
+
+ public function onSuccess()
+ {
+ $changes = ProcessChanges::construct($this->bp, $this->session);
+
+ $nodesToCleanup = $this->getValue('cleanup_all') === '1'
+ ? array_keys($this->bp->getMissingChildren())
+ : $this->getValue('nodes');
+
+ foreach ($nodesToCleanup as $nodeName) {
+ $node = $this->bp->getNode($nodeName);
+ $changes->deleteNode($node);
+ }
+
+ unset($changes);
+
+ parent::onSuccess();
+ }
+}
diff --git a/application/forms/DeleteNodeForm.php b/application/forms/DeleteNodeForm.php
new file mode 100644
index 0000000..dba0710
--- /dev/null
+++ b/application/forms/DeleteNodeForm.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Forms;
+
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\Modification\ProcessChanges;
+use Icinga\Module\Businessprocess\Node;
+use Icinga\Module\Businessprocess\Web\Form\BpConfigBaseForm;
+use Icinga\Web\View;
+
+class DeleteNodeForm extends BpConfigBaseForm
+{
+ /** @var Node */
+ protected $node;
+
+ /** @var ?BpNode */
+ protected $parentNode;
+
+ public function setup()
+ {
+ $node = $this->node;
+ $nodeName = $node->getAlias() ?? $node->getName();
+
+ /** @var View $view */
+ $view = $this->getView();
+ $this->addHtml(
+ '<h2>' . $view->escape(
+ sprintf($this->translate('Delete "%s"'), $nodeName)
+ ) . '</h2>'
+ );
+
+ $biLink = $view->qlink(
+ $nodeName,
+ 'businessprocess/node/impact',
+ array('name' => $node->getName()),
+ array('data-base-target' => '_next')
+ );
+ $this->addHtml(
+ '<p>' . sprintf(
+ $view->escape(
+ $this->translate('Unsure? Show business impact of "%s"')
+ ),
+ $biLink
+ ) . '</p>'
+ );
+
+ if ($this->parentNode) {
+ $yesMsg = sprintf(
+ $this->translate('Delete from %s'),
+ $this->parentNode->getAlias()
+ );
+ } else {
+ $yesMsg = sprintf(
+ $this->translate('Delete root node "%s"'),
+ $nodeName
+ );
+ }
+
+ $this->addElement('select', 'confirm', array(
+ 'label' => $this->translate('Are you sure?'),
+ 'required' => true,
+ 'description' => $this->translate(
+ 'Do you really want to delete this node?'
+ ),
+ 'multiOptions' => $this->optionalEnum(array(
+ 'no' => $this->translate('No'),
+ 'yes' => $yesMsg,
+ 'all' => sprintf($this->translate('Delete all occurrences of %s'), $nodeName),
+ ))
+ ));
+ }
+
+ /**
+ * @param Node $node
+ * @return $this
+ */
+ public function setNode(Node $node)
+ {
+ $this->node = $node;
+ return $this;
+ }
+
+ /**
+ * @param BpNode|null $node
+ * @return $this
+ */
+ public function setParentNode(BpNode $node = null)
+ {
+ $this->parentNode = $node;
+ return $this;
+ }
+
+ public function onSuccess()
+ {
+ $changes = ProcessChanges::construct($this->bp, $this->session);
+
+ $confirm = $this->getValue('confirm');
+ switch ($confirm) {
+ case 'yes':
+ $changes->deleteNode($this->node, $this->parentNode === null ? null : $this->parentNode->getName());
+ break;
+ case 'all':
+ $changes->deleteNode($this->node);
+ break;
+ case 'no':
+ $this->setSuccessMessage($this->translate('Well, maybe next time'));
+ }
+
+ switch ($confirm) {
+ case 'yes':
+ case 'all':
+ if ($this->successUrl === null) {
+ $this->successUrl = clone $this->getRequest()->getUrl();
+ }
+
+ $this->successUrl->getParams()->remove(array('action', 'deletenode'));
+ }
+
+ // Trigger session desctruction to make sure it get's stored.
+ // TODO: figure out why this is necessary, might be an unclean shutdown on redirect
+ unset($changes);
+
+ parent::onSuccess();
+ }
+}
diff --git a/application/forms/EditNodeForm.php b/application/forms/EditNodeForm.php
new file mode 100644
index 0000000..bd1592b
--- /dev/null
+++ b/application/forms/EditNodeForm.php
@@ -0,0 +1,315 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Forms;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\Modification\ProcessChanges;
+use Icinga\Module\Businessprocess\Node;
+use Icinga\Module\Businessprocess\ServiceNode;
+use Icinga\Module\Businessprocess\Web\Form\Element\IplStateOverrides;
+use Icinga\Module\Businessprocess\Web\Form\Validator\HostServiceTermValidator;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+use Icinga\Web\Session\SessionNamespace;
+use ipl\Html\Attributes;
+use ipl\Html\FormattedString;
+use ipl\Html\HtmlElement;
+use ipl\Html\ValidHtml;
+use ipl\I18n\Translation;
+use ipl\Web\Compat\CompatForm;
+use ipl\Web\FormElement\TermInput\ValidatedTerm;
+use ipl\Web\Url;
+
+class EditNodeForm extends CompatForm
+{
+ use Translation;
+
+ /** @var ?BpConfig */
+ protected $bp;
+
+ /** @var ?Node */
+ protected $node;
+
+ /** @var ?BpNode */
+ protected $parent;
+
+ /** @var SessionNamespace */
+ protected $session;
+
+ /**
+ * Set the affected configuration
+ *
+ * @param BpConfig $bp
+ *
+ * @return $this
+ */
+ public function setProcess(BpConfig $bp): self
+ {
+ $this->bp = $bp;
+
+ return $this;
+ }
+
+ /**
+ * Set the affected node
+ *
+ * @param Node $node
+ *
+ * @return $this
+ */
+ public function setNode(Node $node): self
+ {
+ $this->node = $node;
+
+ $this->populate([
+ 'node-search' => $node->getName(),
+ 'node-label' => $node->getAlias()
+ ]);
+
+ return $this;
+ }
+
+ /**
+ * Set the affected sub-process
+ *
+ * @param ?BpNode $node
+ *
+ * @return $this
+ */
+ public function setParentNode(BpNode $node = null): self
+ {
+ $this->parent = $node;
+
+ if ($this->node !== null) {
+ $stateOverrides = $this->parent->getStateOverrides($this->node->getName());
+ if (! empty($stateOverrides)) {
+ $this->populate([
+ 'overrideStates' => 'y',
+ 'stateOverrides' => $stateOverrides
+ ]);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Set the user's session
+ *
+ * @param SessionNamespace $session
+ *
+ * @return $this
+ */
+ public function setSession(SessionNamespace $session): self
+ {
+ $this->session = $session;
+
+ return $this;
+ }
+
+ /**
+ * Identify and return the node the user has chosen
+ *
+ * @return Node
+ */
+ protected function identifyChosenNode(): Node
+ {
+ $userInput = $this->getPopulatedValue('node');
+ $nodeName = $this->getPopulatedValue('node-search');
+ $nodeLabel = $this->getPopulatedValue('node-label');
+
+ if ($nodeName && $userInput === $nodeLabel) {
+ // User accepted a suggestion and didn't change it manually
+ $node = $this->bp->getNode($nodeName);
+ } elseif ($userInput && (! $nodeLabel || $userInput !== $nodeLabel)) {
+ // User didn't choose a suggestion or changed it manually
+ $node = $this->bp->getNode(BpConfig::joinNodeName($userInput, 'Hoststatus'));
+ } else {
+ // If the search and user input are both empty, it can only be the initial value
+ $node = $this->node;
+ }
+
+ return $node;
+ }
+
+ protected function assemble()
+ {
+ $this->addHtml(new HtmlElement('h2', null, FormattedString::create(
+ $this->translate('Modify "%s"'),
+ $this->node->getAlias() ?? $this->node->getName()
+ )));
+
+ if ($this->node instanceof ServiceNode) {
+ $this->assembleServiceElements();
+ } else {
+ $this->assembleHostElements();
+ }
+
+ $this->addElement('submit', 'btn_submit', [
+ 'label' => $this->translate('Save Changes')
+ ]);
+ }
+
+ protected function assembleServiceElements(): void
+ {
+ if ($this->bp->getBackend() instanceof MonitoringBackend) {
+ $suggestionsPath = 'businessprocess/suggestions/monitoring-service';
+ } else {
+ $suggestionsPath = 'businessprocess/suggestions/icingadb-service';
+ }
+
+ $node = $this->identifyChosenNode();
+
+ $this->addHtml($this->createSearchInput(
+ $this->translate('Service'),
+ $node->getAlias() ?? $node->getName(),
+ $suggestionsPath
+ ));
+
+ $this->addElement('checkbox', 'overrideStates', [
+ 'ignore' => true,
+ 'class' => 'autosubmit',
+ 'label' => $this->translate('Override Service State')
+ ]);
+ if ($this->getPopulatedValue('overrideStates') === 'y') {
+ $this->addElement(new IplStateOverrides('stateOverrides', [
+ 'label' => $this->translate('State Overrides'),
+ 'options' => [
+ 0 => $this->translate('OK'),
+ 1 => $this->translate('WARNING'),
+ 2 => $this->translate('CRITICAL'),
+ 3 => $this->translate('UNKNOWN'),
+ 99 => $this->translate('PENDING'),
+ ]
+ ]));
+ }
+ }
+
+ protected function assembleHostElements(): void
+ {
+ if ($this->bp->getBackend() instanceof MonitoringBackend) {
+ $suggestionsPath = 'businessprocess/suggestions/monitoring-host';
+ } else {
+ $suggestionsPath = 'businessprocess/suggestions/icingadb-host';
+ }
+
+ $node = $this->identifyChosenNode();
+
+ $this->addHtml($this->createSearchInput(
+ $this->translate('Host'),
+ $node->getAlias() ?? $node->getName(),
+ $suggestionsPath
+ ));
+
+ $this->addElement('checkbox', 'overrideStates', [
+ 'ignore' => true,
+ 'class' => 'autosubmit',
+ 'label' => $this->translate('Override Host State')
+ ]);
+ if ($this->getPopulatedValue('overrideStates') === 'y') {
+ $this->addElement(new IplStateOverrides('stateOverrides', [
+ 'label' => $this->translate('State Overrides'),
+ 'options' => [
+ 0 => $this->translate('UP'),
+ 1 => $this->translate('DOWN'),
+ 99 => $this->translate('PENDING')
+ ]
+ ]));
+ }
+ }
+
+ protected function createSearchInput(string $label, string $value, string $suggestionsPath): ValidHtml
+ {
+ $userInput = $this->createElement('text', 'node', [
+ 'ignore' => true,
+ 'required' => true,
+ 'autocomplete' => 'off',
+ 'label' => $label,
+ 'value' => $value,
+ 'data-enrichment-type' => 'completion',
+ 'data-term-suggestions' => '#node-suggestions',
+ 'data-suggest-url' => Url::fromPath($suggestionsPath, [
+ 'node' => isset($this->parent) ? $this->parent->getName() : null,
+ 'config' => $this->bp->getName(),
+ 'showCompact' => true,
+ '_disableLayout' => true
+ ]),
+ 'validators' => ['callback' => function ($_, $validator) {
+ $newName = $this->identifyChosenNode()->getName();
+ if ($newName === $this->node->getName()) {
+ return true;
+ }
+
+ $term = new ValidatedTerm($newName);
+
+ (new HostServiceTermValidator())
+ ->setParent($this->parent)
+ ->isValid($term);
+
+ if (! $term->isValid()) {
+ $validator->addMessage($term->getMessage());
+ return false;
+ }
+
+ return true;
+ }]
+ ]);
+
+ $fieldset = new HtmlElement('fieldset');
+
+ $searchInput = $this->createElement('hidden', 'node-search', ['ignore' => true]);
+ $this->registerElement($searchInput);
+ $fieldset->addHtml($searchInput);
+
+ $labelInput = $this->createElement('hidden', 'node-label', ['ignore' => true]);
+ $this->registerElement($labelInput);
+ $fieldset->addHtml($labelInput);
+
+ $this->registerElement($userInput);
+ $this->decorate($userInput);
+
+ $fieldset->addHtml(
+ $userInput,
+ new HtmlElement('div', Attributes::create([
+ 'id' => 'node-suggestions',
+ 'class' => 'search-suggestions'
+ ]))
+ );
+
+ return $fieldset;
+ }
+
+ protected function onSuccess()
+ {
+ $changes = ProcessChanges::construct($this->bp, $this->session);
+
+ $children = $this->parent->getChildNames();
+ $previousPos = array_search($this->node->getName(), $children, true);
+ $node = $this->identifyChosenNode();
+ $nodeName = $node->getName();
+
+ $changes->deleteNode($this->node, $this->parent->getName());
+ $changes->addChildrenToNode([$nodeName], $this->parent);
+
+ $stateOverrides = $this->getValue('stateOverrides');
+ if (! empty($stateOverrides)) {
+ $changes->modifyNode($this->parent, [
+ 'stateOverrides' => array_merge($this->parent->getStateOverrides(), [
+ $nodeName => $stateOverrides
+ ])
+ ]);
+ }
+
+ if ($this->bp->getMetadata()->isManuallyOrdered() && ($newPos = count($children) - 1) > $previousPos) {
+ $changes->moveNode(
+ $node,
+ $newPos,
+ $previousPos,
+ $this->parent->getName(),
+ $this->parent->getName()
+ );
+ }
+
+ unset($changes);
+ }
+}
diff --git a/application/forms/MoveNodeForm.php b/application/forms/MoveNodeForm.php
new file mode 100644
index 0000000..81d15c7
--- /dev/null
+++ b/application/forms/MoveNodeForm.php
@@ -0,0 +1,172 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Forms;
+
+use Icinga\Application\Icinga;
+use Icinga\Application\Web;
+use Icinga\Exception\Http\HttpException;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\Exception\ModificationError;
+use Icinga\Module\Businessprocess\Modification\ProcessChanges;
+use Icinga\Module\Businessprocess\Node;
+use Icinga\Module\Businessprocess\Web\Form\BpConfigBaseForm;
+use Icinga\Module\Businessprocess\Web\Form\CsrfToken;
+use Icinga\Web\Session;
+use Icinga\Web\Session\SessionNamespace;
+
+class MoveNodeForm extends BpConfigBaseForm
+{
+ /** @var BpConfig */
+ protected $bp;
+
+ /** @var Node */
+ protected $node;
+
+ /** @var BpNode */
+ protected $parentNode;
+
+ /** @var SessionNamespace */
+ protected $session;
+
+ public function __construct($options = null)
+ {
+ parent::__construct($options);
+
+ // Zend's plugin loader reverses the order of added prefix paths thus trying our paths first before trying
+ // Zend paths
+ $this->addPrefixPaths(array(
+ array(
+ 'prefix' => 'Icinga\\Web\\Form\\Element\\',
+ 'path' => Icinga::app()->getLibraryDir('Icinga/Web/Form/Element'),
+ 'type' => static::ELEMENT
+ ),
+ array(
+ 'prefix' => 'Icinga\\Web\\Form\\Decorator\\',
+ 'path' => Icinga::app()->getLibraryDir('Icinga/Web/Form/Decorator'),
+ 'type' => static::DECORATOR
+ )
+ ));
+ }
+
+ public function setup()
+ {
+ $this->addElement(
+ 'text',
+ 'parent',
+ [
+ 'allowEmpty' => true,
+ 'filters' => ['Null'],
+ 'validators' => [
+ ['Callback', true, [
+ 'callback' => function ($name) {
+ return empty($name) || $this->bp->hasBpNode($name);
+ },
+ 'messages' => [
+ 'callbackValue' => $this->translate('No process found with name %value%')
+ ]
+ ]]
+ ]
+ ]
+ );
+ $this->addElement(
+ 'number',
+ 'from',
+ [
+ 'required' => true,
+ 'min' => 0
+ ]
+ );
+ $this->addElement(
+ 'number',
+ 'to',
+ [
+ 'required' => true,
+ 'min' => 0
+ ]
+ );
+ $this->addElement(
+ 'hidden',
+ 'csrfToken',
+ [
+ 'required' => true
+ ]
+ );
+
+ $this->setSubmitLabel('movenode');
+ }
+
+ /**
+ * @param Node $node
+ * @return $this
+ */
+ public function setNode(Node $node)
+ {
+ $this->node = $node;
+ return $this;
+ }
+
+ /**
+ * @param BpNode|null $node
+ * @return $this
+ */
+ public function setParentNode(BpNode $node = null)
+ {
+ $this->parentNode = $node;
+ return $this;
+ }
+
+ public function onSuccess()
+ {
+ if (! CsrfToken::isValid($this->getValue('csrfToken'))) {
+ throw new HttpException(403, 'nope');
+ }
+
+ $changes = ProcessChanges::construct($this->bp, $this->session);
+ if (! $this->bp->getMetadata()->isManuallyOrdered()) {
+ $changes->applyManualOrder();
+ }
+
+ try {
+ $changes->moveNode(
+ $this->node,
+ $this->getValue('from'),
+ $this->getValue('to'),
+ $this->getValue('parent'),
+ $this->parentNode !== null ? $this->parentNode->getName() : null
+ );
+ } catch (ModificationError $e) {
+ $this->notifyError($e->getMessage());
+ /** @var Web $app */
+ $app = Icinga::app();
+ $app->getResponse()
+ // Web 2's JS forces a content update for non-200s. Our own JS
+ // can't prevent this, hence we're not making this a 400 :(
+ //->setHttpResponseCode(400)
+ ->setHeader('X-Icinga-Container', 'ignore')
+ ->sendResponse();
+ exit;
+ }
+
+ // Trigger session destruction to make sure it get's stored.
+ unset($changes);
+
+ $this->notifySuccess($this->getSuccessMessage($this->translate('Node order updated')));
+
+ $response = $this->getRequest()->getResponse()
+ ->setHeader('X-Icinga-Container', 'ignore')
+ ->setHeader('X-Icinga-Extra-Updates', implode(';', [
+ $this->getRequest()->getHeader('X-Icinga-Container'),
+ $this->getSuccessUrl()->getAbsoluteUrl()
+ ]));
+
+ Session::getSession()->write();
+ $response->sendResponse();
+ exit;
+ }
+
+ public function hasBeenSent()
+ {
+ return true; // This form has no id
+ }
+}
diff --git a/application/forms/ProcessForm.php b/application/forms/ProcessForm.php
new file mode 100644
index 0000000..126fe9b
--- /dev/null
+++ b/application/forms/ProcessForm.php
@@ -0,0 +1,158 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Forms;
+
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\Modification\ProcessChanges;
+use Icinga\Module\Businessprocess\Node;
+use Icinga\Module\Businessprocess\Web\Form\BpConfigBaseForm;
+use Icinga\Web\Notification;
+use Icinga\Web\View;
+
+class ProcessForm extends BpConfigBaseForm
+{
+ /** @var BpNode */
+ protected $node;
+
+ public function setup()
+ {
+ if ($this->node !== null) {
+ /** @var View $view */
+ $view = $this->getView();
+
+ $this->addHtml(
+ '<h2>' . $view->escape(
+ sprintf($this->translate('Modify "%s"'), $this->node->getAlias())
+ ) . '</h2>'
+ );
+ }
+
+ $this->addElement('text', 'name', [
+ 'label' => $this->translate('ID'),
+ 'value' => (string) $this->node,
+ 'required' => true,
+ 'readonly' => $this->node ? true : null,
+ 'description' => $this->translate('This is the unique identifier of this process')
+ ]);
+
+ $this->addElement('text', 'alias', array(
+ 'label' => $this->translate('Display Name'),
+ 'description' => $this->translate(
+ 'Usually this name will be shown for this node. Equals ID'
+ . ' if not given'
+ ),
+ ));
+
+ $this->addElement('select', 'operator', array(
+ 'label' => $this->translate('Operator'),
+ 'required' => true,
+ 'multiOptions' => Node::getOperators()
+ ));
+
+ if ($this->node !== null) {
+ $display = $this->node->getDisplay() ?: 1;
+ } else {
+ $display = 1;
+ }
+ $this->addElement('select', 'display', array(
+ 'label' => $this->translate('Visualization'),
+ 'required' => true,
+ 'description' => $this->translate(
+ 'Where to show this process'
+ ),
+ 'multiOptions' => array(
+ "$display" => $this->translate('Toplevel Process'),
+ '0' => $this->translate('Subprocess only'),
+ )
+ ));
+
+ $this->addElement('text', 'url', array(
+ 'label' => $this->translate('Info URL'),
+ 'description' => $this->translate(
+ 'URL pointing to more information about this node'
+ )
+ ));
+
+ if ($node = $this->node) {
+ if ($node->hasAlias()) {
+ $this->getElement('alias')->setValue($node->getAlias());
+ }
+ $this->getElement('operator')->setValue($node->getOperator());
+ $this->getElement('display')->setValue($node->getDisplay());
+ if ($node->hasInfoUrl()) {
+ $this->getElement('url')->setValue($node->getInfoUrl());
+ }
+ }
+ }
+
+ /**
+ * @param BpNode $node
+ * @return $this
+ */
+ public function setNode(BpNode $node)
+ {
+ $this->node = $node;
+ return $this;
+ }
+
+ public function onSuccess()
+ {
+ $changes = ProcessChanges::construct($this->bp, $this->session);
+
+ $modifications = array();
+ $alias = $this->getValue('alias');
+ $operator = $this->getValue('operator');
+ $display = $this->getValue('display');
+ $url = $this->getValue('url');
+ if (empty($url)) {
+ $url = null;
+ }
+ if (empty($alias)) {
+ $alias = null;
+ }
+ // TODO: rename
+
+ if ($node = $this->node) {
+ if ($display !== $node->getDisplay()) {
+ $modifications['display'] = $display;
+ }
+ if ($operator !== $node->getOperator()) {
+ $modifications['operator'] = $operator;
+ }
+ if ($url !== $node->getInfoUrl()) {
+ $modifications['infoUrl'] = $url;
+ }
+ if ($alias !== $node->getAlias()) {
+ $modifications['alias'] = $alias;
+ }
+ } else {
+ $modifications = array(
+ 'display' => $display,
+ 'operator' => $operator,
+ 'infoUrl' => $url,
+ 'alias' => $alias,
+ );
+ }
+
+ if (! empty($modifications)) {
+ if ($this->node === null) {
+ $changes->createNode($this->getValue('name'), $modifications);
+ } else {
+ $changes->modifyNode($this->node, $modifications);
+ }
+
+ Notification::success(
+ sprintf(
+ 'Process %s has been modified',
+ $this->bp->getName()
+ )
+ );
+ }
+
+ // Trigger session destruction to make sure it get's stored.
+ // TODO: figure out why this is necessary, might be an unclean shutdown on redirect
+ unset($changes);
+
+ parent::onSuccess();
+ }
+}
diff --git a/application/forms/SimulationForm.php b/application/forms/SimulationForm.php
new file mode 100644
index 0000000..04a0f56
--- /dev/null
+++ b/application/forms/SimulationForm.php
@@ -0,0 +1,138 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Forms;
+
+use Icinga\Module\Businessprocess\MonitoredNode;
+use Icinga\Module\Businessprocess\Simulation;
+use Icinga\Module\Businessprocess\Web\Form\BpConfigBaseForm;
+use Icinga\Web\View;
+
+class SimulationForm extends BpConfigBaseForm
+{
+ /** @var MonitoredNode */
+ protected $node;
+
+ /** @var ?MonitoredNode */
+ protected $simulatedNode;
+
+ /** @var Simulation */
+ protected $simulation;
+
+ public function setup()
+ {
+ $states = $this->enumStateNames();
+
+ // TODO: Fetch state from object
+ if ($this->simulatedNode) {
+ $simulatedState = $this->simulatedNode->getState();
+ $states[$simulatedState] = sprintf(
+ '%s (%s)',
+ $this->node->getStateName($simulatedState),
+ $this->translate('Current simulation')
+ );
+ $node = $this->simulatedNode;
+ $hasSimulation = true;
+ } else {
+ $hasSimulation = false;
+ $node = $this->node;
+ }
+
+ /** @var View $view */
+ $view = $this->getView();
+ if ($hasSimulation) {
+ $title = $this->translate('Modify simulation for %s');
+ } else {
+ $title = $this->translate('Add simulation for %s');
+ }
+ $this->addHtml(
+ '<h2>'
+ . $view->escape(sprintf($title, $node->getAlias() ?? $node->getName()))
+ . '</h2>'
+ );
+
+ $this->addElement('select', 'state', array(
+ 'label' => $this->translate('State'),
+ 'multiOptions' => $states,
+ 'class' => 'autosubmit',
+ 'value' => $this->simulatedNode ? $node->getState() : null,
+ ));
+
+ $sentState = $this->getSentValue('state');
+ if (in_array($sentState, array('0', '99'))) {
+ return;
+ }
+
+ if ($hasSimulation || ($sentState !== null && ctype_digit($sentState))) {
+ $this->addElement('checkbox', 'acknowledged', array(
+ 'label' => $this->translate('Acknowledged'),
+ 'value' => $node->isAcknowledged(),
+ ));
+
+ $this->addElement('checkbox', 'in_downtime', array(
+ 'label' => $this->translate('In downtime'),
+ 'value' => $node->isInDowntime(),
+ ));
+ }
+
+ $this->setSubmitLabel($this->translate('Apply'));
+ }
+
+ public function setNode($node)
+ {
+ $this->node = $node;
+ return $this;
+ }
+
+ public function setSimulation(Simulation $simulation)
+ {
+ $this->simulation = $simulation;
+
+ $name = $this->node->getName();
+ if ($simulation->hasNode($name)) {
+ $this->simulatedNode = clone($this->node);
+ $s = $simulation->getNode($name);
+ $this->simulatedNode->setState($s->state)
+ ->setAck($s->acknowledged)
+ ->setDowntime($s->in_downtime)
+ ->setMissing(false);
+ }
+
+ return $this;
+ }
+
+ public function onSuccess()
+ {
+ $nodeName = $this->node->getName();
+ $state = $this->getValue('state');
+
+ if ($state !== null && ctype_digit($state)) {
+ $this->notifySuccess($this->translate('Simulation has been set'));
+ $this->simulation->set($nodeName, (object) array(
+ 'state' => $this->getValue('state'),
+ 'acknowledged' => $this->getValue('acknowledged'),
+ 'in_downtime' => $this->getValue('in_downtime'),
+ ));
+ } else {
+ if ($this->simulation->remove($nodeName)) {
+ $this->notifySuccess($this->translate('Simulation has been removed'));
+ }
+ }
+
+ parent::onSuccess();
+ }
+
+ /**
+ * @return array
+ */
+ protected function enumStateNames()
+ {
+ $states = array(
+ null => sprintf(
+ $this->translate('Use current state (%s)'),
+ $this->translate($this->node->getStateName())
+ )
+ ) + $this->node->enumStateNames();
+
+ return $states;
+ }
+}
diff --git a/application/views/helpers/FormSimpleNote.php b/application/views/helpers/FormSimpleNote.php
new file mode 100644
index 0000000..d8315f4
--- /dev/null
+++ b/application/views/helpers/FormSimpleNote.php
@@ -0,0 +1,15 @@
+<?php
+
+// Avoid complaints about missing namespace and invalid class name
+// @codingStandardsIgnoreStart
+class Zend_View_Helper_FormSimpleNote extends Zend_View_Helper_FormElement
+{
+ // @codingStandardsIgnoreEnd
+
+ public function formSimpleNote($name, $value = null)
+ {
+ $info = $this->_getInfo($name, $value);
+ extract($info); // name, value, attribs, options, listsep, disable
+ return $value;
+ }
+}
diff --git a/application/views/helpers/RenderStateBadges.php b/application/views/helpers/RenderStateBadges.php
new file mode 100644
index 0000000..70633aa
--- /dev/null
+++ b/application/views/helpers/RenderStateBadges.php
@@ -0,0 +1,33 @@
+<?php
+
+/**
+ * @deprecated
+ * @codingStandardsIgnoreStart
+ */
+class Zend_View_Helper_RenderStateBadges extends Zend_View_Helper_Abstract
+{
+ // @codingStandardsIgnoreEnd
+ public function renderStateBadges($summary)
+ {
+ $html = '';
+
+ foreach ($summary as $state => $cnt) {
+ if ($cnt === 0
+ || $state === 'OK'
+ || $state === 'UP'
+ ) {
+ continue;
+ }
+
+ $html .= '<span class="badge badge-' . strtolower($state)
+ . '" title="' . mt('monitoring', $state) . '">'
+ . $cnt . '</span>';
+ }
+
+ if ($html !== '') {
+ $html = '<div class="badges">' . $html . '</div>';
+ }
+
+ return $html;
+ }
+}
diff --git a/application/views/scripts/default.phtml b/application/views/scripts/default.phtml
new file mode 100644
index 0000000..3e2cc59
--- /dev/null
+++ b/application/views/scripts/default.phtml
@@ -0,0 +1,2 @@
+<?= $this->controls->render() ?>
+<?= $this->content->render() ?>
diff --git a/application/views/scripts/host/show.phtml b/application/views/scripts/host/show.phtml
new file mode 100644
index 0000000..413baf2
--- /dev/null
+++ b/application/views/scripts/host/show.phtml
@@ -0,0 +1,13 @@
+<?php
+/** @var \Icinga\Web\View $this */
+/** @var \Icinga\Web\Widget\Tabs $tabs */
+/** @var string $host */
+?>
+<div class="controls">
+ <?= $tabs->showOnlyCloseButton() ?>
+</div>
+<div class="content restricted">
+ <h1><?= $this->translate('Access Denied') ?></h1>
+ <p><?= sprintf($this->translate('You are lacking permission to access host "%s".'), $this->escape($host)) ?></p>
+ <a href="#" class="close-container-control action-link"><?= $this->icon('cancel') ?><?= $this->translate('Hide this message') ?></a>
+</div>
diff --git a/application/views/scripts/process/source.phtml b/application/views/scripts/process/source.phtml
new file mode 100644
index 0000000..d5ba6bb
--- /dev/null
+++ b/application/views/scripts/process/source.phtml
@@ -0,0 +1,25 @@
+<?= $this->controls->render() ?>
+
+<div class="content">
+<?php if ($this->showDiff): ?>
+<div class="diff">
+<?= $this->diff->render() ?>
+</div>
+<?php else: ?>
+<table class="sourcecode">
+<?php
+
+$cnt = 0;
+$lines = preg_split('~\r?\n~', $this->source);
+$len = ceil(log(count($lines), 10));
+$rowhtml = sprintf('<tr><th>%%0%dd: </th><td>%%s<br></td></tr>', $len);
+
+foreach ($lines as $line) {
+ $cnt++;
+ printf($rowhtml, $cnt, $this->escape($line));
+}
+
+?>
+</table>
+<?php endif ?>
+</div>
diff --git a/application/views/scripts/service/show.phtml b/application/views/scripts/service/show.phtml
new file mode 100644
index 0000000..205b3f7
--- /dev/null
+++ b/application/views/scripts/service/show.phtml
@@ -0,0 +1,14 @@
+<?php
+/** @var \Icinga\Web\View $this */
+/** @var \Icinga\Web\Widget\Tabs $tabs */
+/** @var string $host */
+/** @var string $service */
+?>
+<div class="controls">
+ <?= $tabs->showOnlyCloseButton() ?>
+</div>
+<div class="content restricted">
+ <h1><?= $this->escape($this->translate('Access Denied')) ?></h1>
+ <p><?= $this->escape(sprintf($this->translate('You are lacking permission to access service "%s" on host "%s"'), $service, $host)) ?></p>
+ <a href="#" class="close-container-control action-link"><?= $this->icon('cancel') ?><?= $this->translate('Hide this message') ?></a>
+</div>
diff --git a/configuration.php b/configuration.php
new file mode 100644
index 0000000..6ef510e
--- /dev/null
+++ b/configuration.php
@@ -0,0 +1,64 @@
+<?php
+
+use Icinga\Module\Businessprocess\Storage\LegacyStorage;
+use Icinga\Module\Businessprocess\Web\Navigation\Renderer\ProcessProblemsBadge;
+
+/** @var \Icinga\Application\Modules\Module $this */
+$section = $this->menuSection(N_('Business Processes'), array(
+ 'renderer' => 'ProcessesProblemsBadge',
+ 'url' => 'businessprocess',
+ 'icon' => 'sitemap',
+ 'priority' => 46
+));
+
+try {
+ $storage = LegacyStorage::getInstance();
+
+ $prio = 0;
+ foreach ($storage->listProcessNames() as $name) {
+ $meta = $storage->loadMetadata($name);
+ if ($meta->get('AddToMenu') === 'no') {
+ continue;
+ }
+ $prio++;
+
+ if ($prio > 5) {
+ $section->add(N_('Show all'), array(
+ 'url' => 'businessprocess',
+ 'priority' => $prio
+ ));
+
+ break;
+ }
+
+ $section->add($meta->getTitle(), array(
+ 'renderer' => (new ProcessProblemsBadge())->setBpConfigName($name),
+ 'url' => 'businessprocess/process/show',
+ 'urlParameters' => array('config' => $name),
+ 'priority' => $prio
+ ));
+ }
+} catch (Exception $e) {
+ // Well... there is not much we could do here
+}
+
+$this->providePermission(
+ 'businessprocess/showall',
+ $this->translate('Allow to see all available processes, regardless of configured restrictions')
+);
+$this->providePermission(
+ 'businessprocess/create',
+ $this->translate('Allow to create whole new process configuration (files)')
+);
+$this->providePermission(
+ 'businessprocess/modify',
+ $this->translate('Allow to modify process definitions, to add and remove nodes')
+);
+$this->provideRestriction(
+ 'businessprocess/prefix',
+ $this->translate('Restrict access to configurations with the given prefix')
+);
+
+$this->provideJsFile('vendor/Sortable.js');
+$this->provideJsFile('behavior/sortable.js');
+$this->provideJsFile('vendor/jquery.fn.sortable.js');
diff --git a/doc/01-About.md b/doc/01-About.md
new file mode 100644
index 0000000..44672b4
--- /dev/null
+++ b/doc/01-About.md
@@ -0,0 +1,19 @@
+# Icinga Business Process Modeling
+
+If you want to visualize and monitor hierarchical business processes based on
+objects monitored by Icinga, Icinga Business Process Modeling is the solution.
+
+[![Dashboard](screenshot/16_dashboard/1603_businessprocesses_on_dashboard.png)](16-Add-To-Dashboard.md)
+
+Want to create custom process-based dashboards? Trigger notifications at
+process or sub-process level? Provide a quick top-level view for thousands of
+components on a single screen? That's what this module has been designed for!
+
+You're running a huge cloud, want to get rid of the monitoring noise triggered
+by your auto-scaling platform but still want to have detailed information just
+a couple of clicks away in case you need them? You will love this little module!
+
+## Documentation
+
+* [Installation](02-Installation.md)
+* [Getting Started](03-Getting-Started.md)
diff --git a/doc/02-Installation.md b/doc/02-Installation.md
new file mode 100644
index 0000000..6d479b1
--- /dev/null
+++ b/doc/02-Installation.md
@@ -0,0 +1,24 @@
+<!-- {% if index %} -->
+# Installing Icinga Business Process Modeling
+
+The recommended way to install Icinga Business Process Modeling is to use prebuilt packages for
+all supported platforms from our official release repository.
+Please note that [Icinga Web](https://icinga.com/docs/icinga-web) is required to run Icinga
+Business Process Modeling and if it is not already set up, it is best to do this first.
+
+The following steps will guide you through installing and setting up Icinga Business Process Modeling.
+<!-- {% else %} -->
+<!-- {% if not icingaDocs %} -->
+
+## Installing the Package
+
+If the [repository](https://packages.icinga.com) is not configured yet, please add it first.
+Then use your distribution's package manager to install the `icinga-businessprocess` package
+or install [from source](02-Installation.md.d/From-Source.md).
+<!-- {% endif %} --><!-- {# end if not icingaDocs #} -->
+
+## Configuring Icinga Business Process Modeling
+
+That's it, Icinga Business Process Modeling is now ready to use.
+Please read more on [how to get started](03-Getting-Started.md).
+<!-- {% endif %} --><!-- {# end else if index #} -->
diff --git a/doc/02-Installation.md.d/From-Source.md b/doc/02-Installation.md.d/From-Source.md
new file mode 100644
index 0000000..9e4f6ec
--- /dev/null
+++ b/doc/02-Installation.md.d/From-Source.md
@@ -0,0 +1,15 @@
+# Installing Icinga Business Process Modeling from Source
+
+Please see the Icinga Web documentation on
+[how to install modules](https://icinga.com/docs/icinga-web/latest/doc/08-Modules/#installation) from source.
+Make sure you use `businessprocess` as the module name. The following requirements must also be met.
+
+## Requirements
+
+* PHP (≥7.2)
+* [Icinga Web](https://github.com/Icinga/icingaweb2) (≥2.9)
+* [Icinga DB Web](https://github.com/Icinga/icingadb-web) (≥1.0)
+* [Icinga PHP Library (ipl)](https://github.com/Icinga/icinga-php-library) (≥0.13.0)
+* [Icinga PHP Thirdparty](https://github.com/Icinga/icinga-php-thirdparty) (≥0.12.0)
+
+<!-- {% include "02-Installation.md" %} -->
diff --git a/doc/03-Getting-Started.md b/doc/03-Getting-Started.md
new file mode 100644
index 0000000..baacde0
--- /dev/null
+++ b/doc/03-Getting-Started.md
@@ -0,0 +1,77 @@
+# Getting Started
+
+Once you enable Icinga Business Process Modeling, it will pop up in your menu.
+If you click on it, it will show you a new Dashboard:
+
+![Empty Dashboard](screenshot/03_getting-started/0201_empty-dashboard.png)
+
+## A new Business Process configuration
+
+From here we choose to create a new *Business Process configuration*:
+
+![New Business Process](screenshot/03_getting-started/0202_create-new-configuration.png)
+
+Let's have a look at the single fields:
+
+### Configuration name
+
+![Configuration name](screenshot/03_getting-started/0203_create-new_name.png)
+
+The Business Process definition will be stored with this name. This is going to
+be used when referencing this process in URLs and in Check Commands.
+
+### Title
+
+![Configuration name](screenshot/03_getting-started/0204_create-new_title.png)
+
+You might optionally want to provide an additional title. In that case the title
+is shown in the GUI, while the name is still used as a reference. The title will
+default to the name.
+
+### Description
+
+![Description](screenshot/03_getting-started/0205_create-new_description.png)
+
+Provide a short description explaining within 100-150 character what this
+configuration provides. This will be shown on the Dashboard.
+
+### Backend
+
+![Backend](screenshot/03_getting-started/0206_create-new_backend.png)
+
+**Hint:** *Usually this should not be changed*
+
+Icinga Web 2 currently uses only one Monitoring Backend, but in theory you
+could configure multiple ones. They won't be usable in a meaningful way at the
+time of this writing. Still, you might want to use a different backend as a data
+provider for your Business Process.
+
+### State Type
+
+![State Type](screenshot/03_getting-started/0207_create-new_state-type.png)
+
+You can decide whether `SOFT` or `HARD` states should be the used as a base when
+calculating the state of a Business Process definition.
+
+### Add to menu
+
+Business Process configurations can be linked to the Icinga Web 2 menu. Only the
+first five configurations a user is allowed to see will be shown there:
+
+![Add to menu](screenshot/03_getting-started/0208_create-new_add-to-menu.png)
+
+That's all for now, click `Add` to store your new (still empty) Business Process
+configuration.
+
+## Empty configuration
+
+You are redirected to your newly created Business Process configuration:
+
+![Empty configuration](screenshot/03_getting-started/0209_new-empty-configuration.png)
+
+From here we can now add as many deeply nested Business Processes as we want.
+But let's first have a look at our Dashboard once again:
+
+![New on Dashboard](screenshot/03_getting-started/0210_new-on-dashboard.png)
+
+Now let's move on and [create your first Nodes](04-Create-your-first-process-node.md).
diff --git a/doc/04-Create-your-first-process-node.md b/doc/04-Create-your-first-process-node.md
new file mode 100644
index 0000000..ad3273d
--- /dev/null
+++ b/doc/04-Create-your-first-process-node.md
@@ -0,0 +1,67 @@
+# Create your first Business Process Node
+
+A *Business Process Node* consists of a *name*, *title*, an *operator* and one or
+more child nodes. It can be a Root Node, child node of other Business Process
+Nodes - or both.
+
+![Empty Config](screenshot/04_first-root-node/0301_empty-config.png)
+
+## Configuring our first node
+
+To create our first *Business Process Node* we click the *Add* button. This
+leads to the related configuration form:
+
+![Add new Node](screenshot/04_first-root-node/0302_add-new-node.png)
+
+First setting is the *Node name*, an identifier that must be unique throughout
+all Nodes that are going to be defined. This identifier will be used in every
+link and also in *Check Commands* referring this node from an Icinga *Service
+Check*.
+
+### Set a title
+
+As uniqueness sometimes leads to not-so-beautiful names, you are additionally
+allowed to specify a title. This is what the frontend is going to show:
+
+![Node Title](screenshot/04_first-root-node/0303_node-title.png)
+
+### Choose an operator
+
+Every Business Process requires an *Operator*. This operator defines it's
+behaviour, this specifies how it's very own state is going to be calculated:
+
+![Operator](screenshot/04_first-root-node/0304_operator.png)
+
+### Specify where to display
+
+The form suggests to create a *Toplevel Process*. It does so as we are about
+to create a new *root node*. We could alternatively also create a sub process.
+As we are currently not adding it to another Node, this would lead to an *Unbound
+Node* that could be linked later on.
+
+![Node Display](screenshot/04_first-root-node/0305_display.png)
+
+### Provide an optional Info URL
+
+One might also want to provide a link to additional information related to a
+specific process. This could be instructions with more technical details or
+hints telling what should happen if outage occurs. You might not want to do so
+for every single Node, but it might come in handy for your most important (top
+level?) nodes:
+
+![Node Info Url](screenshot/04_first-root-node/0306_info-url.png)
+
+That's it, your are ready to submit the form.
+
+### First Business Process Node ready
+
+You are now shown your first Business Process Node. A red bar reminds you that
+your pending changes have not been stored yet:
+
+![First Node created](screenshot/04_first-root-node/0307_first-node-created.png)
+
+You could now *Store the Configuration* or move on with adding additional nodes
+to complete your configuration.
+
+**Hint**: the blue arrow makes part of a breadcrumb showing your current position.
+ You might want to learn more about [breadcrumbs](12-Web-Components-Breadcrumb.md).
diff --git a/doc/05-Importing-Processes.md b/doc/05-Importing-Processes.md
new file mode 100644
index 0000000..3095bbf
--- /dev/null
+++ b/doc/05-Importing-Processes.md
@@ -0,0 +1,53 @@
+# Importing Processes
+
+To avoid redundancy and make complex *Business Process Configurations* easier
+to maintain it is possible to import processes from other configurations.
+
+In order to be able to import a process create a root node first. You cannot
+import processes into the root level.
+
+![Subprocesses Only](screenshot/05_importing_nodes/0401_subprocesses_only.png)
+
+## Importing a Process
+
+Once the related configuration form is open, choose `Existing Process` and wait
+for the form to refresh.
+
+![Existing Process](screenshot/05_importing_nodes/0402_choose_existing_process.png)
+
+### Choose Configuration
+
+You can now choose the configuration to import processes from. Or simply hit
+`Next` to just utilize a process from the current configuration.
+
+![Choose Configuration](screenshot/05_importing_nodes/0403_choose_configuration.png)
+
+### Select Processes
+
+Now select the processes you want to import and submit the form.
+
+![Select Processes](screenshot/05_importing_nodes/0404_choose_process.png)
+
+### Import Successful
+
+You are now looking at the result. The process has been imported. Do not forget
+to save your changes!
+
+![Import Successful](screenshot/05_importing_nodes/0405_import_successful.png)
+
+## Navigation with Imported Processes
+
+### Seamless Breadcrumbs
+
+You may have already noticed that the breadcrumbs integrate the hierarchy
+of the imported process. Once you navigate further the actions below the
+breadcrumbs change and don't permit to unlock editing.
+
+![Seamless Breadcrumbs](screenshot/05_importing_nodes/0406_breadcrumb_integration.png)
+
+To change imported processes you need to open them in their original
+configuration first. To do so click on the arrow to the right which is
+displayed in a tile's action urls in the upper left. While in tree view
+these can be found at the very right of an process' row.
+
+![Jump To Original](screenshot/05_importing_nodes/0407_jump_to_original.png)
diff --git a/doc/06-Customize-Node-Order.md b/doc/06-Customize-Node-Order.md
new file mode 100644
index 0000000..880eaa6
--- /dev/null
+++ b/doc/06-Customize-Node-Order.md
@@ -0,0 +1,71 @@
+# Customize Node Order
+
+By default all nodes are ordered alphabetically while viewing them in the UI.
+Though, it is also possible to order nodes entirely manually.
+
+> **Note**
+>
+> Once manual order is applied (no matter where) alphabetical order is
+> disabled for the entire configuration.
+
+## Reorder by Drag'n'Drop
+
+Make sure to unlock the configuration first to be able to reorder nodes.
+
+### Tile View
+
+To move a tile simply grab it with your mouse and drag it to the location you
+want it to appear at.
+
+![Grab Tile](screenshot/06_customize_node_order/0501_tiles_grab_tile.png)
+![Drop Tile](screenshot/06_customize_node_order/0502_tiles_drop_at_location.png)
+
+### Tree View
+
+While in tree view nodes can be moved the same way. You just have a narrower
+area to grab them.
+
+![Grab Row](screenshot/06_customize_node_order/0503_tree_grab_header.png)
+![Drop Row](screenshot/06_customize_node_order/0504_tree_drop_at_location.png)
+
+The tree view also has an advantage the tile view has not. It is possible to
+move nodes within the entire hierarchy. But remember to unfold processes first,
+if you want to move a node into them.
+
+## File Format Extensions
+
+The configuration file format has slightly been changed to accommodate the new
+manual order. Though, previous configurations are perfectly upwards compatible.
+
+### New Header
+
+A new header is used to flag a configuration file as being manually ordered.
+
+```
+# ManualOrder : yes
+```
+
+Once this is set alphabetical order is disabled and only the next techniques
+define the order of nodes.
+
+### Changed `display` Semantic
+
+Previously there were only two valid values for the `display` directive.
+(0 = Subprocess, 1 = Toplevel Process)
+
+```
+display 0|1;<name>;<title>
+```
+
+This has now been extended so that values greater than zero refer to the order
+of root nodes. (ascending)
+
+```
+display 0|n;<name>;<title>
+```
+
+### Significant Children Order
+
+Previously the order of a node's children in a configuration file was not
+important in any way. Now this is significant and refers to the order in
+which children appear in the UI and how process states are determined.
diff --git a/doc/07-State-Overrides.md b/doc/07-State-Overrides.md
new file mode 100644
index 0000000..dfdbaeb
--- /dev/null
+++ b/doc/07-State-Overrides.md
@@ -0,0 +1,45 @@
+# State Overrides
+
+Business processes utilize their children's states to calculate their own state.
+While you can influence this with [operators](09-Operators.md), it's also possible
+to override individual states. (This applies to host and service nodes.)
+
+## Configuring Overrides
+
+State overrides get configured per node. When adding or editing a node, you can
+define which state should be overridden with another one.
+
+Below `WARNING` is chosen as a replacement for `CRITICAL`.
+
+![Service State Override Configuration](screenshot/07_state_overrides/0701_override_config.png "Service State Override Configuration")
+
+## Identifying Overrides
+
+In tile view overridden states are indicated by an additional state ball in the
+lower left of a tile. This is then the actual state the object is in.
+
+![Overridden Tile State](screenshot/07_state_overrides/0702_overridden_tile.png "Overridden Tile State")
+
+In tree view overridden states are indicated on the very right of a row. There
+the actual state is shown and which one it is replaced with.
+
+![Overridden Tree State](screenshot/07_state_overrides/0703_overridden_tree.png "Overridden Tree State")
+
+## File Format Extensions
+
+The configuration file format has slightly been changed to accommodate state
+overrides. Though, previous configurations are perfectly upwards compatible.
+
+### New Extra Line
+
+For process nodes a new extra line is used to store state overrides.
+
+```
+state_overrides dev_database_servers!mysql;mysql|2-1
+```
+
+The full syntax for this is as follows:
+
+```
+state_overrides <process>!<child>|n-n[!<child>|n-n[,n-n]]
+```
diff --git a/doc/09-Operators.md b/doc/09-Operators.md
new file mode 100644
index 0000000..8d54ba3
--- /dev/null
+++ b/doc/09-Operators.md
@@ -0,0 +1,43 @@
+# Operators
+
+Every Business Process requires an Operator. This operator defines its behaviour and specifies how its very own state is
+going to be calculated.
+
+## AND
+
+The `AND` operator selects the **WORST** state of its child nodes:
+
+![And Operator](screenshot/09_operators/0901_and-operator.png)
+
+## OR
+
+The `OR` operator selects the **BEST** state of its child nodes:
+
+![Or Operator](screenshot/09_operators/0902_or-operator.png)
+
+![Or Operator #2](screenshot/09_operators/0903_or-operator-without-ok.png)
+
+## XOR
+
+The `XOR` operator shows OK if only one of n children is OK at the same time. In all other cases the parent node is CRITICAL.
+Useful for a service on n servers, only one of which may be running. If both were running,
+race conditions and duplication of data could occur.
+
+![Xor Operator](screenshot/09_operators/0906_xor-operator.png)
+
+![Xor Operator #2](screenshot/09_operators/0907_xor-operator-not-ok.png)
+
+## DEGRADED
+
+The `DEGRADED` operator behaves like an `AND`, but if the resulting
+state is **CRITICAL** it transforms it into a **WARNING**.
+Refer to the table below for the case-by-case
+analysis of the statuses.
+
+![Degraded Operator](screenshot/09_operators/0905_deg-operator.jpg)
+
+## MIN n
+
+The `MIN` operator selects the **WORST** state out of the **BEST n** child node states:
+
+![MIN](screenshot/09_operators/0904_min-operator.png)
diff --git a/doc/10-Monitoring.md b/doc/10-Monitoring.md
new file mode 100644
index 0000000..2d7c70c
--- /dev/null
+++ b/doc/10-Monitoring.md
@@ -0,0 +1,49 @@
+# Monitoring
+
+## Process Check Command
+
+The module provides a CLI command to check a business process.
+
+### Usage
+
+General: `icingacli businessprocess process check <process> [options]`
+
+Options:
+
+```
+ --config <configname> Name of the config that contains <process>
+ --details Show problem details as a tree
+ --colors Show colored output
+ --state-type <type> Define which state type to look at. Could be either soft
+ or hard, overrides an eventually configured default
+ --blame Show problem details as a tree reduced to the nodes
+ which have the same state as the business process
+ --root-cause Used in combination with --blame. Only shows
+ the path of the nodes which are responsible
+ for the state of the business process
+ --downtime-is-ok Treat hosts/services in downtime always as UP/OK.
+ --ack-is-ok Treat acknowledged hosts/services always as UP/OK.
+```
+
+### Detail View Integration
+
+It is possible to show the monitored process in the service detail view.
+
+For this to work, the name of the checkcommand configured in Icinga 2 must either
+be `icingacli-businessprocess` or the name that can be configured in the module
+configuration:
+
+**/etc/icingaweb2/modules/businessprocess/config.ini**
+```ini
+[DetailviewExtension]
+checkcommand_name=businessprocess-check
+```
+
+A service can define specific custom variables for this. Mandatory ones
+that are not defined, cause the detail view integration to not be active.
+
+| Variable Name | Mandatory | Description |
+|--------------------------------------|-----------|----------------------------------------------|
+| icingacli\_businessprocess\_process | Yes | The `<process>` being checked |
+| icingacli\_businessprocess\_config | No | Name of the config that contains `<process>` |
+| icingaweb\_businessprocess\_as\_tree | No | Whether to show `<process>` as tree or tiles |
diff --git a/doc/12-Web-Components-Breadcrumb.md b/doc/12-Web-Components-Breadcrumb.md
new file mode 100644
index 0000000..27391f0
--- /dev/null
+++ b/doc/12-Web-Components-Breadcrumb.md
@@ -0,0 +1,69 @@
+# Web Components: Breadcrumb
+
+All Business Process renderers show a **breadcrumb** component to always give
+you a quick indication of your current location.
+
+![Símple Breadcrumb](screenshot/12_web-components_breadcrumb/1201_simple-breadcrumb.png)
+
+The left-most section shows the title of the current *Business Process Configuration*.
+The remaining sections show the path to the current *Business Process Node* currently
+being shown.
+
+Hovering the Breadcrumb with your mouse shows you that all of it sections are
+highlighted, as they are links pointing to either the root level when clicking
+on the *Configuration Node* itself or to the corresponding *Business Process Node*.
+
+All but the last section, showing your current position in the tree. Even if
+not being highlighted, it is still a link an can be clicked in case you need
+so.
+
+In case you're showing some related details in a split-screen view of *Icinga
+Web 2*, a click on any *Breadcrumb* section will switch back to a wide single
+column view to make it obvious that you moved to another context. It is also
+perfectly legal to open any of the available links in a new browser tab or
+window.
+
+## Available actions below the Breadcrumb
+
+### Choose a renderer
+
+The first link allows to toggle the used Renderer. Currently a *Tree* and a
+*Tile* renderer are available.
+
+### Move to Full Screen Mode
+
+Every view can be shown in *Full Screen Mode*. Full screen means that left and
+upper menu together with some other details are hidden. Your Business Process
+will be able to use all of the available space. Want even more? Then please
+additionally switch your browser to full screen mode. This is usually done by
+pressing the `F11` key.
+
+Once being in full screen mode you'll find an icon on the right side that will
+allow you to switch back to normal view:
+
+![Return from fullscreen](screenshot/12_web-components_breadcrumb/1202_return-from-fullscreen.png)
+
+**Hint:** We know that the web application might request real full screen mode
+on their own. We refused doing so as many people find this being an annoying
+feature.
+
+### Unlock the Configuration
+
+When clicking `Unlock`, additional actions are shown. One of them is immediately
+shown next to the `Unlock` link and reads `Config`. It allows you to reach Configuration
+settings for the your currently loaded *Business Process Configuration*:
+
+![Unlocked config](screenshot/12_web-components_breadcrumb/1204_unlocked_config.png)
+
+But there is more. When unlocked, all nodes provide links allowing you to modify or
+to delete them. Host/Service Nodes now allow you to simulate a specific state.
+
+## Other main actions
+
+### Add content to your Dashboard
+
+When being in *locked* mode, you are allowed to add the currently shown process
+at the given path with the active renderer in the main (or a custom) [Icinga Web 2
+Dashboard](16-Add-To-Dashboard.md):
+
+![Add to Dashboard](screenshot/12_web-components_breadcrumb/1203_add-to-dashboard.png)
diff --git a/doc/13-Web-Components-Tile-Renderer.md b/doc/13-Web-Components-Tile-Renderer.md
new file mode 100644
index 0000000..4d5df8c
--- /dev/null
+++ b/doc/13-Web-Components-Tile-Renderer.md
@@ -0,0 +1,22 @@
+# Web Components: Tile Renderer
+
+The default Business Process *Renderer* is the *Tile Renderer*. It always shows
+one level of your tree, enriched with badges giving some hint on lower level
+node problems. This is what it looks like:
+
+![Tile Renderer](screenshot/13_web-components-tile-renderer/1301_tile-view.png)
+
+Please click on *Tree* below the [breadcrumb](12-Web-Components-Breadcrumb.md)
+to switch to the [Tree View](14-Web-Components-Tree-Renderer.md). In the left
+corner of every *Tile* you can find two icons, both of them will show the related
+sub-process. On your mobile phone this usually replaces your current view, please
+use the [breadcrumb](12-Web-Components-Breadcrumb.md) to navigate back to a higher
+level.
+
+On a Notebook or Desktop Computer this usually leady to a split-screen view:
+
+![Split View - Tiles and Tree](screenshot/13_web-components-tile-renderer/1302_tile-and-subtree.png)
+
+This example shows a subtree shown with the [Tree Renderer](14-Web-Components-Tree-Renderer.md),
+it is of course also perfectly legal to drill down using the *Tile Renderer*
+only.
diff --git a/doc/14-Web-Components-Tree-Renderer.md b/doc/14-Web-Components-Tree-Renderer.md
new file mode 100644
index 0000000..b761360
--- /dev/null
+++ b/doc/14-Web-Components-Tree-Renderer.md
@@ -0,0 +1,13 @@
+# Web Components: Tree Renderer
+
+The main advantage of the *Tree Renderer* is that it is able to show all nodes
+of Business Process trees at once. This works fine even for huge trees with lots
+of nodes. Please have a look at this screenshot to get an idea of how the tree
+view looks like:
+
+![Tree View](screenshot/14_web-components-tree-renderer/1401_tree-view.png)
+
+Clicking Business Process Nodes collapses or unfolds them, clicking single hosts
+or services show the related monitored object. You can of course always switch
+back to the [Tile Renderer](13-Web-Components-Tile-Renderer.md) with a single
+click at any time.
diff --git a/doc/16-Add-To-Dashboard.md b/doc/16-Add-To-Dashboard.md
new file mode 100644
index 0000000..4b9f8a8
--- /dev/null
+++ b/doc/16-Add-To-Dashboard.md
@@ -0,0 +1,20 @@
+# Show Processes on a Dashboard
+
+When being in *Locked mode*, you can add any Business Process at top or sub level
+to any Icinga Web 2 Dashboard. The related link can be found in the Tab bar:
+
+![Add to Dashboard - Link](screenshot/16_dashboard/1601_add-to-dashboard-link.png)
+
+This leads to the standard Icinga Web 2 *Add Dashlet to Dashboard* form. Feel
+free to add your Business Process View to any existing Dashboard. You might also
+want to create a dedicated Dashboard as shown in this example:
+
+![Add to Dashboard - Form](screenshot/16_dashboard/1602_add_to_dashboard-form.png)
+
+
+## Want more?
+
+Head on and add multiple Business Processes to your Dashboard to show all of
+them at once:
+
+![Sample Dashboard](screenshot/16_dashboard/1603_businessprocesses_on_dashboard.png)
diff --git a/doc/21-Store-Config.md b/doc/21-Store-Config.md
new file mode 100644
index 0000000..a8380e7
--- /dev/null
+++ b/doc/21-Store-Config.md
@@ -0,0 +1,23 @@
+# Store your Configuration
+
+Changes to your *Business Process Configuration* are added to a stack and will
+not be stored immediately. In case there are pending unstored changes, this will
+be shown on every screen:
+
+![Pending Changes](screenshot/21_store-config/2101_Pending-Changes.png)
+
+A click on *Dismiss* immediately throws away all unstored changes. A click on
+*Store* brings you to the configuration form. You have seen this before, once
+you created your [very first configuration](03-Getting-Started.md):
+
+![Store Config](screenshot/21_store-config/2102_Store-Config.png)
+
+## Config Diff
+
+If unsure what changes you're going to store, you can still check the *Config Diff*
+before finally storing to disk:
+
+![Show Diff](screenshot/21_store-config/2103_Show-Diff.png)
+
+You can also download your existing configuration to safe it elsewhere or to
+apply manual modifications with our favourite plaintext editor.
diff --git a/doc/22-Upload-Config.md b/doc/22-Upload-Config.md
new file mode 100644
index 0000000..2afdb01
--- /dev/null
+++ b/doc/22-Upload-Config.md
@@ -0,0 +1,26 @@
+# Upload a Configuration File
+
+You can upload a formerly downloaded or even a manually created file directly
+through the web frontend. Given sufficient permissions, the Dashboard provides
+a related link:
+
+![From Dashboard to Upload](screenshot/22_upload-config/2201_go-to-upload.png)
+
+## Chose a file
+
+This can be any file:
+
+![Choose a File](screenshot/22_upload-config/2202_choose-file.png)
+
+It should be valid of course, but don't worry - Icinga Business Process Modeling
+protects you from syntax errors:
+
+![Syntax Error](screenshot/22_upload-config/2203_syntax-error.png)
+
+Just for fun you could try to upload an image or whatever you want - it will not
+break. It will also protect you from accidentally overwriting existing files:
+
+![Duplicate Name](screenshot/22_upload-config/2204_duplicate-name.png)
+
+So in case you need to replace an existing process, please delete it before
+uploading a new one.
diff --git a/doc/31-Permissions.md b/doc/31-Permissions.md
new file mode 100644
index 0000000..b6b8b98
--- /dev/null
+++ b/doc/31-Permissions.md
@@ -0,0 +1,25 @@
+# Permission System
+
+The permission system of the module is based on permissions and restrictions.
+
+## Permissions
+
+The module has five levels of permissions:
+
+* Granting general module access allows a user to view business processes. (`module/businessprocess`)
+* Create permissions allow to create new business processes. (`businessprocess/create`)
+* Modify permissions allow to modify already existing ones. (`businessprocess/modify`)
+* Permission to view all business processes regardless restrictions. (`businessprocess/showall`)
+* Full permissions. (`businessprocess/*`)
+
+## Restrictions
+
+There are two ways to configure restrictions: prefix-based and access controls
+
+### Prefix-based
+
+This option allows to limit access of a role to only business processes with a specific prefix. For this the ID (Configuration name) of a business process has to start with a prefix and it has to be set as restriction on the role. (`businessprocess/prefix`)
+
+### Access controls
+
+This option allows for more fine granular permissions based on user (`AllowedUsers`), group (`AllowedGroups`) and role (`AllowedRoles`). These attributes take a comma-separated list, get added to the header of the business process configuration file and limit access to the owner and the mentioned ones.
diff --git a/doc/81-History.md b/doc/81-History.md
new file mode 100644
index 0000000..82a4024
--- /dev/null
+++ b/doc/81-History.md
@@ -0,0 +1,43 @@
+# Project History
+
+Icinga Business Process Modeling is based on the ideas of the Nagios(tm) [Business
+Process AddOn](http://bp-addon.monitoringexchange.org/) written by Bernd
+Strößenreuther. We always loved its simplicity, and while it looks pretty
+oldschool right now there are still many shops happily using it in production.
+
+![BpAddOn Overview](screenshot/81_history/8101_bpaddon-overview.png)
+
+## Compatibility
+
+We fully support the BPaddon configuration language and will continue to do so.
+It's also perfectly valid to run both products in parallel based on the very same
+config files. New features are (mostly) added in a compatible way.
+
+Configuration titles and descriptions, properties related to state types or
+permissions are examples for new features that didn't formerly exist. They are
+stored as commented metadata in the file header and therefore invisible to the
+old AddOn.
+
+The only way to break compatibility is to use newly introduced operators like
+`NOT`. Once you do so, the old AddOn will no longer be able to parse your
+configuration.
+
+![BpAddOn Details](screenshot/81_history/8102_bpaddon-detail.png)
+
+Lot's of changes went on and are still going on under the hood. We have more
+features and new language constructs. We separated the config reader from the
+state fetcher in our code base. This will allow us to eventually support config
+backends like SQL databases or the Icinga 2 DSL.
+
+This would make it easier to distribute configuration in large environments.
+
+## Improvements
+
+Major focus has been put on execution speed. So while the Web integration shows
+much more details at once and is able to display huge unfolded trees, it should
+still render and refresh faster. Same goes for the Check Plugin.
+
+Behaviour for all operators is now strictly specified and Unit-tested. You still
+can manually edit your configuration files. But much better, you also delegate
+this to your co-workers, as Business Process definitions can now be built directly
+in the GUI.
diff --git a/doc/screenshot/00_preview/0001_preview-tree-view.png b/doc/screenshot/00_preview/0001_preview-tree-view.png
new file mode 100644
index 0000000..2015a31
--- /dev/null
+++ b/doc/screenshot/00_preview/0001_preview-tree-view.png
Binary files differ
diff --git a/doc/screenshot/00_preview/0002_preview_tile_view.png b/doc/screenshot/00_preview/0002_preview_tile_view.png
new file mode 100644
index 0000000..08770ae
--- /dev/null
+++ b/doc/screenshot/00_preview/0002_preview_tile_view.png
Binary files differ
diff --git a/doc/screenshot/00_preview/0003_preview_businessprocesses_on_dashboard.png b/doc/screenshot/00_preview/0003_preview_businessprocesses_on_dashboard.png
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/doc/screenshot/00_preview/0003_preview_businessprocesses_on_dashboard.png
diff --git a/doc/screenshot/00_preview/0004_preview_tile_and_subtree.png b/doc/screenshot/00_preview/0004_preview_tile_and_subtree.png
new file mode 100644
index 0000000..e8db7ae
--- /dev/null
+++ b/doc/screenshot/00_preview/0004_preview_tile_and_subtree.png
Binary files differ
diff --git a/doc/screenshot/00_preview/0005_readme-preview.png b/doc/screenshot/00_preview/0005_readme-preview.png
new file mode 100644
index 0000000..4515c86
--- /dev/null
+++ b/doc/screenshot/00_preview/0005_readme-preview.png
Binary files differ
diff --git a/doc/screenshot/02_installation/101_menu-configuration-modules.png b/doc/screenshot/02_installation/101_menu-configuration-modules.png
new file mode 100644
index 0000000..0c2d9df
--- /dev/null
+++ b/doc/screenshot/02_installation/101_menu-configuration-modules.png
Binary files differ
diff --git a/doc/screenshot/02_installation/102_enable-module.png b/doc/screenshot/02_installation/102_enable-module.png
new file mode 100644
index 0000000..140d38d
--- /dev/null
+++ b/doc/screenshot/02_installation/102_enable-module.png
Binary files differ
diff --git a/doc/screenshot/03_getting-started/0201_empty-dashboard.png b/doc/screenshot/03_getting-started/0201_empty-dashboard.png
new file mode 100644
index 0000000..b8fddcb
--- /dev/null
+++ b/doc/screenshot/03_getting-started/0201_empty-dashboard.png
Binary files differ
diff --git a/doc/screenshot/03_getting-started/0202_create-new-configuration.png b/doc/screenshot/03_getting-started/0202_create-new-configuration.png
new file mode 100644
index 0000000..48e91fa
--- /dev/null
+++ b/doc/screenshot/03_getting-started/0202_create-new-configuration.png
Binary files differ
diff --git a/doc/screenshot/03_getting-started/0203_create-new_name.png b/doc/screenshot/03_getting-started/0203_create-new_name.png
new file mode 100644
index 0000000..02845eb
--- /dev/null
+++ b/doc/screenshot/03_getting-started/0203_create-new_name.png
Binary files differ
diff --git a/doc/screenshot/03_getting-started/0204_create-new_title.png b/doc/screenshot/03_getting-started/0204_create-new_title.png
new file mode 100644
index 0000000..99e26bb
--- /dev/null
+++ b/doc/screenshot/03_getting-started/0204_create-new_title.png
Binary files differ
diff --git a/doc/screenshot/03_getting-started/0205_create-new_description.png b/doc/screenshot/03_getting-started/0205_create-new_description.png
new file mode 100644
index 0000000..454ec82
--- /dev/null
+++ b/doc/screenshot/03_getting-started/0205_create-new_description.png
Binary files differ
diff --git a/doc/screenshot/03_getting-started/0206_create-new_backend.png b/doc/screenshot/03_getting-started/0206_create-new_backend.png
new file mode 100644
index 0000000..31fcc83
--- /dev/null
+++ b/doc/screenshot/03_getting-started/0206_create-new_backend.png
Binary files differ
diff --git a/doc/screenshot/03_getting-started/0207_create-new_state-type.png b/doc/screenshot/03_getting-started/0207_create-new_state-type.png
new file mode 100644
index 0000000..f875057
--- /dev/null
+++ b/doc/screenshot/03_getting-started/0207_create-new_state-type.png
Binary files differ
diff --git a/doc/screenshot/03_getting-started/0208_create-new_add-to-menu.png b/doc/screenshot/03_getting-started/0208_create-new_add-to-menu.png
new file mode 100644
index 0000000..c868015
--- /dev/null
+++ b/doc/screenshot/03_getting-started/0208_create-new_add-to-menu.png
Binary files differ
diff --git a/doc/screenshot/03_getting-started/0209_new-empty-configuration.png b/doc/screenshot/03_getting-started/0209_new-empty-configuration.png
new file mode 100644
index 0000000..b1fbe86
--- /dev/null
+++ b/doc/screenshot/03_getting-started/0209_new-empty-configuration.png
Binary files differ
diff --git a/doc/screenshot/03_getting-started/0210_new-on-dashboard.png b/doc/screenshot/03_getting-started/0210_new-on-dashboard.png
new file mode 100644
index 0000000..6717d3d
--- /dev/null
+++ b/doc/screenshot/03_getting-started/0210_new-on-dashboard.png
Binary files differ
diff --git a/doc/screenshot/04_first-root-node/0301_empty-config.png b/doc/screenshot/04_first-root-node/0301_empty-config.png
new file mode 100644
index 0000000..b1fbe86
--- /dev/null
+++ b/doc/screenshot/04_first-root-node/0301_empty-config.png
Binary files differ
diff --git a/doc/screenshot/04_first-root-node/0302_add-new-node.png b/doc/screenshot/04_first-root-node/0302_add-new-node.png
new file mode 100644
index 0000000..f3eeb87
--- /dev/null
+++ b/doc/screenshot/04_first-root-node/0302_add-new-node.png
Binary files differ
diff --git a/doc/screenshot/04_first-root-node/0303_node-title.png b/doc/screenshot/04_first-root-node/0303_node-title.png
new file mode 100644
index 0000000..b6f9d12
--- /dev/null
+++ b/doc/screenshot/04_first-root-node/0303_node-title.png
Binary files differ
diff --git a/doc/screenshot/04_first-root-node/0304_operator.png b/doc/screenshot/04_first-root-node/0304_operator.png
new file mode 100644
index 0000000..1ffa1c2
--- /dev/null
+++ b/doc/screenshot/04_first-root-node/0304_operator.png
Binary files differ
diff --git a/doc/screenshot/04_first-root-node/0305_display.png b/doc/screenshot/04_first-root-node/0305_display.png
new file mode 100644
index 0000000..39e30b3
--- /dev/null
+++ b/doc/screenshot/04_first-root-node/0305_display.png
Binary files differ
diff --git a/doc/screenshot/04_first-root-node/0306_info-url.png b/doc/screenshot/04_first-root-node/0306_info-url.png
new file mode 100644
index 0000000..770102d
--- /dev/null
+++ b/doc/screenshot/04_first-root-node/0306_info-url.png
Binary files differ
diff --git a/doc/screenshot/04_first-root-node/0307_first-node-created.png b/doc/screenshot/04_first-root-node/0307_first-node-created.png
new file mode 100644
index 0000000..3fd5be6
--- /dev/null
+++ b/doc/screenshot/04_first-root-node/0307_first-node-created.png
Binary files differ
diff --git a/doc/screenshot/05_importing_nodes/0401_subprocesses_only.png b/doc/screenshot/05_importing_nodes/0401_subprocesses_only.png
new file mode 100644
index 0000000..d4f58ba
--- /dev/null
+++ b/doc/screenshot/05_importing_nodes/0401_subprocesses_only.png
Binary files differ
diff --git a/doc/screenshot/05_importing_nodes/0402_choose_existing_process.png b/doc/screenshot/05_importing_nodes/0402_choose_existing_process.png
new file mode 100644
index 0000000..4fc5f63
--- /dev/null
+++ b/doc/screenshot/05_importing_nodes/0402_choose_existing_process.png
Binary files differ
diff --git a/doc/screenshot/05_importing_nodes/0403_choose_configuration.png b/doc/screenshot/05_importing_nodes/0403_choose_configuration.png
new file mode 100644
index 0000000..19b4c05
--- /dev/null
+++ b/doc/screenshot/05_importing_nodes/0403_choose_configuration.png
Binary files differ
diff --git a/doc/screenshot/05_importing_nodes/0404_choose_process.png b/doc/screenshot/05_importing_nodes/0404_choose_process.png
new file mode 100644
index 0000000..010992c
--- /dev/null
+++ b/doc/screenshot/05_importing_nodes/0404_choose_process.png
Binary files differ
diff --git a/doc/screenshot/05_importing_nodes/0405_import_successful.png b/doc/screenshot/05_importing_nodes/0405_import_successful.png
new file mode 100644
index 0000000..9f4f346
--- /dev/null
+++ b/doc/screenshot/05_importing_nodes/0405_import_successful.png
Binary files differ
diff --git a/doc/screenshot/05_importing_nodes/0406_breadcrumb_integration.png b/doc/screenshot/05_importing_nodes/0406_breadcrumb_integration.png
new file mode 100644
index 0000000..0929546
--- /dev/null
+++ b/doc/screenshot/05_importing_nodes/0406_breadcrumb_integration.png
Binary files differ
diff --git a/doc/screenshot/05_importing_nodes/0407_jump_to_original.png b/doc/screenshot/05_importing_nodes/0407_jump_to_original.png
new file mode 100644
index 0000000..8fc0e2d
--- /dev/null
+++ b/doc/screenshot/05_importing_nodes/0407_jump_to_original.png
Binary files differ
diff --git a/doc/screenshot/06_customize_node_order/0501_tiles_grab_tile.png b/doc/screenshot/06_customize_node_order/0501_tiles_grab_tile.png
new file mode 100644
index 0000000..5697786
--- /dev/null
+++ b/doc/screenshot/06_customize_node_order/0501_tiles_grab_tile.png
Binary files differ
diff --git a/doc/screenshot/06_customize_node_order/0502_tiles_drop_at_location.png b/doc/screenshot/06_customize_node_order/0502_tiles_drop_at_location.png
new file mode 100644
index 0000000..cd7b673
--- /dev/null
+++ b/doc/screenshot/06_customize_node_order/0502_tiles_drop_at_location.png
Binary files differ
diff --git a/doc/screenshot/06_customize_node_order/0503_tree_grab_header.png b/doc/screenshot/06_customize_node_order/0503_tree_grab_header.png
new file mode 100644
index 0000000..7687713
--- /dev/null
+++ b/doc/screenshot/06_customize_node_order/0503_tree_grab_header.png
Binary files differ
diff --git a/doc/screenshot/06_customize_node_order/0504_tree_drop_at_location.png b/doc/screenshot/06_customize_node_order/0504_tree_drop_at_location.png
new file mode 100644
index 0000000..828aebe
--- /dev/null
+++ b/doc/screenshot/06_customize_node_order/0504_tree_drop_at_location.png
Binary files differ
diff --git a/doc/screenshot/07_state_overrides/0701_override_config.png b/doc/screenshot/07_state_overrides/0701_override_config.png
new file mode 100644
index 0000000..49ca2ad
--- /dev/null
+++ b/doc/screenshot/07_state_overrides/0701_override_config.png
Binary files differ
diff --git a/doc/screenshot/07_state_overrides/0702_overridden_tile.png b/doc/screenshot/07_state_overrides/0702_overridden_tile.png
new file mode 100644
index 0000000..db2012e
--- /dev/null
+++ b/doc/screenshot/07_state_overrides/0702_overridden_tile.png
Binary files differ
diff --git a/doc/screenshot/07_state_overrides/0703_overridden_tree.png b/doc/screenshot/07_state_overrides/0703_overridden_tree.png
new file mode 100644
index 0000000..ccf4fde
--- /dev/null
+++ b/doc/screenshot/07_state_overrides/0703_overridden_tree.png
Binary files differ
diff --git a/doc/screenshot/09_operators/0901_and-operator.png b/doc/screenshot/09_operators/0901_and-operator.png
new file mode 100644
index 0000000..c6e7775
--- /dev/null
+++ b/doc/screenshot/09_operators/0901_and-operator.png
Binary files differ
diff --git a/doc/screenshot/09_operators/0902_or-operator.png b/doc/screenshot/09_operators/0902_or-operator.png
new file mode 100644
index 0000000..fd05ec3
--- /dev/null
+++ b/doc/screenshot/09_operators/0902_or-operator.png
Binary files differ
diff --git a/doc/screenshot/09_operators/0903_or-operator-without-ok.png b/doc/screenshot/09_operators/0903_or-operator-without-ok.png
new file mode 100644
index 0000000..e9fcd4e
--- /dev/null
+++ b/doc/screenshot/09_operators/0903_or-operator-without-ok.png
Binary files differ
diff --git a/doc/screenshot/09_operators/0904_min-operator.png b/doc/screenshot/09_operators/0904_min-operator.png
new file mode 100644
index 0000000..fd05ec3
--- /dev/null
+++ b/doc/screenshot/09_operators/0904_min-operator.png
Binary files differ
diff --git a/doc/screenshot/09_operators/0905_deg-operator.jpg b/doc/screenshot/09_operators/0905_deg-operator.jpg
new file mode 100644
index 0000000..9dc05a3
--- /dev/null
+++ b/doc/screenshot/09_operators/0905_deg-operator.jpg
Binary files differ
diff --git a/doc/screenshot/09_operators/0906_xor-operator.png b/doc/screenshot/09_operators/0906_xor-operator.png
new file mode 100644
index 0000000..fd05ec3
--- /dev/null
+++ b/doc/screenshot/09_operators/0906_xor-operator.png
Binary files differ
diff --git a/doc/screenshot/09_operators/0907_xor-operator-not-ok.png b/doc/screenshot/09_operators/0907_xor-operator-not-ok.png
new file mode 100644
index 0000000..8ec41b3
--- /dev/null
+++ b/doc/screenshot/09_operators/0907_xor-operator-not-ok.png
Binary files differ
diff --git a/doc/screenshot/12_web-components_breadcrumb/1201_simple-breadcrumb.png b/doc/screenshot/12_web-components_breadcrumb/1201_simple-breadcrumb.png
new file mode 100644
index 0000000..a4f9a4d
--- /dev/null
+++ b/doc/screenshot/12_web-components_breadcrumb/1201_simple-breadcrumb.png
Binary files differ
diff --git a/doc/screenshot/12_web-components_breadcrumb/1202_return-from-fullscreen.png b/doc/screenshot/12_web-components_breadcrumb/1202_return-from-fullscreen.png
new file mode 100644
index 0000000..fe3406b
--- /dev/null
+++ b/doc/screenshot/12_web-components_breadcrumb/1202_return-from-fullscreen.png
Binary files differ
diff --git a/doc/screenshot/12_web-components_breadcrumb/1203_add-to-dashboard.png b/doc/screenshot/12_web-components_breadcrumb/1203_add-to-dashboard.png
new file mode 100644
index 0000000..b1daec5
--- /dev/null
+++ b/doc/screenshot/12_web-components_breadcrumb/1203_add-to-dashboard.png
Binary files differ
diff --git a/doc/screenshot/12_web-components_breadcrumb/1204_unlocked_config.png b/doc/screenshot/12_web-components_breadcrumb/1204_unlocked_config.png
new file mode 100644
index 0000000..0ba84a9
--- /dev/null
+++ b/doc/screenshot/12_web-components_breadcrumb/1204_unlocked_config.png
Binary files differ
diff --git a/doc/screenshot/13_web-components-tile-renderer/1301_tile-view.png b/doc/screenshot/13_web-components-tile-renderer/1301_tile-view.png
new file mode 100644
index 0000000..80a142f
--- /dev/null
+++ b/doc/screenshot/13_web-components-tile-renderer/1301_tile-view.png
Binary files differ
diff --git a/doc/screenshot/13_web-components-tile-renderer/1302_tile-and-subtree.png b/doc/screenshot/13_web-components-tile-renderer/1302_tile-and-subtree.png
new file mode 100644
index 0000000..a03209d
--- /dev/null
+++ b/doc/screenshot/13_web-components-tile-renderer/1302_tile-and-subtree.png
Binary files differ
diff --git a/doc/screenshot/14_web-components-tree-renderer/1401_tree-view.png b/doc/screenshot/14_web-components-tree-renderer/1401_tree-view.png
new file mode 100644
index 0000000..7bacb00
--- /dev/null
+++ b/doc/screenshot/14_web-components-tree-renderer/1401_tree-view.png
Binary files differ
diff --git a/doc/screenshot/16_dashboard/1601_add-to-dashboard-link.png b/doc/screenshot/16_dashboard/1601_add-to-dashboard-link.png
new file mode 100644
index 0000000..b1daec5
--- /dev/null
+++ b/doc/screenshot/16_dashboard/1601_add-to-dashboard-link.png
Binary files differ
diff --git a/doc/screenshot/16_dashboard/1602_add_to_dashboard-form.png b/doc/screenshot/16_dashboard/1602_add_to_dashboard-form.png
new file mode 100644
index 0000000..ab2d255
--- /dev/null
+++ b/doc/screenshot/16_dashboard/1602_add_to_dashboard-form.png
Binary files differ
diff --git a/doc/screenshot/16_dashboard/1603_businessprocesses_on_dashboard.png b/doc/screenshot/16_dashboard/1603_businessprocesses_on_dashboard.png
new file mode 100644
index 0000000..116bf04
--- /dev/null
+++ b/doc/screenshot/16_dashboard/1603_businessprocesses_on_dashboard.png
Binary files differ
diff --git a/doc/screenshot/21_store-config/2101_Pending-Changes.png b/doc/screenshot/21_store-config/2101_Pending-Changes.png
new file mode 100644
index 0000000..7e33981
--- /dev/null
+++ b/doc/screenshot/21_store-config/2101_Pending-Changes.png
Binary files differ
diff --git a/doc/screenshot/21_store-config/2102_Store-Config.png b/doc/screenshot/21_store-config/2102_Store-Config.png
new file mode 100644
index 0000000..d985786
--- /dev/null
+++ b/doc/screenshot/21_store-config/2102_Store-Config.png
Binary files differ
diff --git a/doc/screenshot/21_store-config/2103_Show-Diff.png b/doc/screenshot/21_store-config/2103_Show-Diff.png
new file mode 100644
index 0000000..7025f6c
--- /dev/null
+++ b/doc/screenshot/21_store-config/2103_Show-Diff.png
Binary files differ
diff --git a/doc/screenshot/22_upload-config/2201_go-to-upload.png b/doc/screenshot/22_upload-config/2201_go-to-upload.png
new file mode 100644
index 0000000..1dda67c
--- /dev/null
+++ b/doc/screenshot/22_upload-config/2201_go-to-upload.png
Binary files differ
diff --git a/doc/screenshot/22_upload-config/2202_choose-file.png b/doc/screenshot/22_upload-config/2202_choose-file.png
new file mode 100644
index 0000000..a8717dd
--- /dev/null
+++ b/doc/screenshot/22_upload-config/2202_choose-file.png
Binary files differ
diff --git a/doc/screenshot/22_upload-config/2203_syntax-error.png b/doc/screenshot/22_upload-config/2203_syntax-error.png
new file mode 100644
index 0000000..ca81da0
--- /dev/null
+++ b/doc/screenshot/22_upload-config/2203_syntax-error.png
Binary files differ
diff --git a/doc/screenshot/22_upload-config/2204_duplicate-name.png b/doc/screenshot/22_upload-config/2204_duplicate-name.png
new file mode 100644
index 0000000..5f9c809
--- /dev/null
+++ b/doc/screenshot/22_upload-config/2204_duplicate-name.png
Binary files differ
diff --git a/doc/screenshot/81_history/8101_bpaddon-overview.png b/doc/screenshot/81_history/8101_bpaddon-overview.png
new file mode 100644
index 0000000..6ae8cb0
--- /dev/null
+++ b/doc/screenshot/81_history/8101_bpaddon-overview.png
Binary files differ
diff --git a/doc/screenshot/81_history/8102_bpaddon-detail.png b/doc/screenshot/81_history/8102_bpaddon-detail.png
new file mode 100644
index 0000000..7b38a1e
--- /dev/null
+++ b/doc/screenshot/81_history/8102_bpaddon-detail.png
Binary files differ
diff --git a/library/Businessprocess/BpConfig.php b/library/Businessprocess/BpConfig.php
new file mode 100644
index 0000000..c9e70fd
--- /dev/null
+++ b/library/Businessprocess/BpConfig.php
@@ -0,0 +1,1117 @@
+<?php
+
+namespace Icinga\Module\Businessprocess;
+
+use Exception;
+use Icinga\Application\Modules\Module;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Businessprocess\Exception\NestingError;
+use Icinga\Module\Businessprocess\Modification\ProcessChanges;
+use Icinga\Module\Businessprocess\ProvidedHook\Icingadb\IcingadbSupport;
+use Icinga\Module\Businessprocess\Storage\LegacyStorage;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+use ipl\Sql\Connection as IcingaDbConnection;
+
+class BpConfig
+{
+ const SOFT_STATE = 0;
+
+ const HARD_STATE = 1;
+
+ /**
+ * Name of the configured monitoring backend
+ *
+ * @var string
+ */
+ protected $backendName;
+
+ /**
+ * Backend to retrieve states from
+ *
+ * @var MonitoringBackend|IcingaDbConnection
+ */
+ protected $backend;
+
+ /**
+ * @var LegacyStorage
+ */
+ protected $storage;
+
+ /** @var Metadata */
+ protected $metadata;
+
+ /**
+ * Business process name
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * Business process title
+ *
+ * @var string
+ */
+ protected $title;
+
+ /**
+ * State type, soft or hard
+ *
+ * @var int
+ */
+ protected $state_type;
+
+ /**
+ * Warnings, usually filled at process build time
+ *
+ * @var array
+ */
+ protected $warnings = array();
+
+ /**
+ * Errors, usually filled at process build time
+ *
+ * @var array
+ */
+ protected $errors = array();
+
+ /**
+ * All used node objects
+ *
+ * @var array
+ */
+ protected $nodes = array();
+
+ /**
+ * Root node objects
+ *
+ * @var array
+ */
+ protected $root_nodes = array();
+
+ /**
+ * Imported nodes
+ *
+ * @var ImportedNode[]
+ */
+ protected $importedNodes = [];
+
+ /**
+ * Imported configs
+ *
+ * @var BpConfig[]
+ */
+ protected $importedConfigs = [];
+
+ /**
+ * All host names { 'hostA' => true, ... }
+ *
+ * @var array
+ */
+ protected $hosts = array();
+
+ /** @var bool Whether catchable errors should be thrown nonetheless */
+ protected $throwErrors = false;
+
+ protected $loopDetection = array();
+
+ /**
+ * Applied state simulation
+ *
+ * @var Simulation
+ */
+ protected $simulation;
+
+ protected $changeCount = 0;
+
+ protected $simulationCount = 0;
+
+ /** @var ProcessChanges */
+ protected $appliedChanges;
+
+ /** @var bool Whether the config is faulty */
+ protected $isFaulty = false;
+
+ public function __construct()
+ {
+ }
+
+ /**
+ * Retrieve metadata for this configuration
+ *
+ * @return Metadata
+ */
+ public function getMetadata()
+ {
+ if ($this->metadata === null) {
+ $this->metadata = new Metadata($this->name);
+ }
+
+ return $this->metadata;
+ }
+
+ /**
+ * Set metadata
+ *
+ * @param Metadata $metadata
+ *
+ * @return $this
+ */
+ public function setMetadata(Metadata $metadata)
+ {
+ $this->metadata = $metadata;
+ return $this;
+ }
+
+ /**
+ * Apply pending process changes
+ *
+ * @param ProcessChanges $changes
+ *
+ * @return $this
+ */
+ public function applyChanges(ProcessChanges $changes)
+ {
+ $cnt = 0;
+ foreach ($changes->getChanges() as $change) {
+ $cnt++;
+ $change->applyTo($this);
+ }
+ $this->changeCount = $cnt;
+
+ $this->appliedChanges = $changes;
+
+ return $this;
+ }
+
+ /**
+ * Apply a state simulation
+ *
+ * @param Simulation $simulation
+ *
+ * @return $this
+ */
+ public function applySimulation(Simulation $simulation)
+ {
+ $cnt = 0;
+
+ foreach ($simulation->simulations() as $node => $s) {
+ if (! $this->hasNode($node)) {
+ continue;
+ }
+ $cnt++;
+ $this->getNode($node)
+ ->setState($s->state)
+ ->setAck($s->acknowledged)
+ ->setDowntime($s->in_downtime)
+ ->setMissing(false);
+ }
+
+ $this->simulationCount = $cnt;
+
+ return $this;
+ }
+
+ /**
+ * Number of applied changes
+ *
+ * @return int
+ */
+ public function countChanges()
+ {
+ return $this->changeCount;
+ }
+
+ /**
+ * Whether changes have been applied to this configuration
+ *
+ * @return bool
+ */
+ public function hasChanges()
+ {
+ return $this->countChanges() > 0;
+ }
+
+ /**
+ * @param $name
+ *
+ * @return $this
+ */
+ public function setName($name)
+ {
+ $this->name = $name;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * @return string
+ */
+ public function getHtmlId()
+ {
+ return 'businessprocess-' . preg_replace('/[\r\n\t\s]/', '_', $this->getName());
+ }
+
+ public function setTitle($title)
+ {
+ $this->title = $title;
+ return $this;
+ }
+
+ public function getTitle()
+ {
+ return $this->getMetadata()->getTitle();
+ }
+
+ public function hasTitle()
+ {
+ return $this->getMetadata()->has('Title');
+ }
+
+ public function getBackendName()
+ {
+ return $this->getMetadata()->get('Backend');
+ }
+
+ public function hasBackendName()
+ {
+ return $this->getMetadata()->has('Backend');
+ }
+
+ public function setBackend($backend)
+ {
+ $this->backend = $backend;
+ return $this;
+ }
+
+ public function getBackend()
+ {
+ if ($this->backend === null) {
+ if (Module::exists('icingadb')
+ && (! $this->hasBackendName() && IcingadbSupport::useIcingaDbAsBackend())) {
+ $this->backend = IcingaDbObject::fetchDb();
+ } else {
+ $this->backend = MonitoringBackend::instance(
+ $this->getBackendName()
+ );
+ }
+ }
+
+ return $this->backend;
+ }
+
+ public function isReferenced()
+ {
+ foreach ($this->storage()->listProcessNames() as $bpName) {
+ if ($bpName !== $this->getName()) {
+ $bp = $this->storage()->loadProcess($bpName);
+ foreach ($bp->getImportedNodes() as $importedNode) {
+ if ($importedNode->getConfigName() === $this->getName()) {
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public function hasBackend()
+ {
+ return $this->backend !== null;
+ }
+
+ public function hasBeenChanged()
+ {
+ return false;
+ }
+
+ public function hasSimulations()
+ {
+ return $this->countSimulations() > 0;
+ }
+
+ public function countSimulations()
+ {
+ return $this->simulationCount;
+ }
+
+ public function clearAppliedChanges()
+ {
+ if ($this->appliedChanges !== null) {
+ $this->appliedChanges->clear();
+ }
+ return $this;
+ }
+
+ public function getStateType()
+ {
+ if ($this->state_type === null) {
+ if ($this->getMetadata()->has('Statetype')) {
+ switch ($this->getMetadata()->get('Statetype')) {
+ case 'hard':
+ $this->state_type = self::HARD_STATE;
+ break;
+ case 'soft':
+ $this->state_type = self::SOFT_STATE;
+ break;
+ }
+ } else {
+ $this->state_type = self::HARD_STATE;
+ }
+ }
+
+ return $this->state_type;
+ }
+
+ public function useSoftStates()
+ {
+ $this->state_type = self::SOFT_STATE;
+ return $this;
+ }
+
+ public function useHardStates()
+ {
+ $this->state_type = self::HARD_STATE;
+ return $this;
+ }
+
+ public function usesSoftStates()
+ {
+ return $this->getStateType() === self::SOFT_STATE;
+ }
+
+ public function usesHardStates()
+ {
+ return $this->getStateType() === self::HARD_STATE;
+ }
+
+ public function addRootNode($name)
+ {
+ $this->root_nodes[$name] = $this->getNode($name);
+ return $this;
+ }
+
+ public function removeRootNode($name)
+ {
+ if ($this->isRootNode($name)) {
+ unset($this->root_nodes[$name]);
+ }
+
+ return $this;
+ }
+
+ public function isRootNode($name)
+ {
+ return array_key_exists($name, $this->root_nodes);
+ }
+
+ /**
+ * @return BpNode[]
+ */
+ public function getChildren()
+ {
+ return $this->getRootNodes();
+ }
+
+ /**
+ * @return int
+ */
+ public function countChildren()
+ {
+ return count($this->root_nodes);
+ }
+
+ /**
+ * @return BpNode[]
+ */
+ public function getRootNodes()
+ {
+ return $this->root_nodes;
+ }
+
+ public function listRootNodes()
+ {
+ return array_keys($this->root_nodes);
+ }
+
+ public function getNodes()
+ {
+ return $this->nodes;
+ }
+
+ public function hasNode($name)
+ {
+ if (array_key_exists($name, $this->nodes)) {
+ return true;
+ } elseif ($name[0] === '@') {
+ list($configName, $nodeName) = preg_split('~:\s*~', substr($name, 1), 2);
+ return $this->getImportedConfig($configName)->hasNode($nodeName);
+ }
+
+ return false;
+ }
+
+ public function hasRootNode($name)
+ {
+ return array_key_exists($name, $this->root_nodes);
+ }
+
+ public function createService($host, $service)
+ {
+ $node = new ServiceNode(
+ (object) array(
+ 'hostname' => $host,
+ 'service' => $service
+ )
+ );
+ $node->setBpConfig($this);
+ $this->nodes[$node->getName()] = $node;
+ $this->hosts[$host] = true;
+ return $node;
+ }
+
+ public function createHost($host)
+ {
+ $node = new HostNode((object) array('hostname' => $host));
+ $node->setBpConfig($this);
+ $this->nodes[$node->getName()] = $node;
+ $this->hosts[$host] = true;
+ return $node;
+ }
+
+ public function calculateAllStates()
+ {
+ foreach ($this->getRootNodes() as $node) {
+ $node->getState();
+ }
+
+ return $this;
+ }
+
+ public function clearAllStates()
+ {
+ foreach ($this->getBpNodes() as $node) {
+ $node->clearState();
+ }
+
+ return $this;
+ }
+
+ public function listInvolvedHostNames(&$usedConfigs = null)
+ {
+ $hosts = $this->hosts;
+ if (! empty($this->importedNodes)) {
+ $usedConfigs[$this->getName()] = true;
+ foreach ($this->importedNodes as $node) {
+ if (isset($usedConfigs[$node->getConfigName()])) {
+ continue;
+ }
+
+ $hosts += array_flip($node->getBpConfig()->listInvolvedHostNames($usedConfigs));
+ }
+ }
+
+ return array_keys($hosts);
+ }
+
+ /**
+ * Create and attach a new process (BpNode)
+ *
+ * @param string $name Process name
+ * @param string $operator Operator (defaults to &)
+ *
+ * @return BpNode
+ */
+ public function createBp($name, $operator = '&')
+ {
+ $node = new BpNode((object) array(
+ 'name' => $name,
+ 'operator' => $operator,
+ 'child_names' => array(),
+ ));
+ $node->setBpConfig($this);
+
+ $this->addNode($name, $node);
+ return $node;
+ }
+
+ public function createMissingBp($name)
+ {
+ return $this->createBp($name)->setMissing();
+ }
+
+ public function getMissingChildren()
+ {
+ $missing = array();
+ foreach ($this->getRootNodes() as $root) {
+ $missing += $root->getMissingChildren();
+ }
+
+ return $missing;
+ }
+
+ public function createImportedNode($config, $name = null)
+ {
+ $params = (object) array('configName' => $config);
+ if ($name !== null) {
+ $params->node = $name;
+ }
+
+ $node = new ImportedNode($this, $params);
+ $this->importedNodes[$node->getName()] = $node;
+ $this->nodes[$node->getName()] = $node;
+ return $node;
+ }
+
+ public function getImportedNodes()
+ {
+ return $this->importedNodes;
+ }
+
+ public function getImportedConfig($name)
+ {
+ if (! isset($this->importedConfigs[$name])) {
+ try {
+ $import = $this->storage()->loadProcess($name);
+ } catch (Exception $e) {
+ $import = (new static())
+ ->setName($name)
+ ->setFaulty();
+ }
+
+ if ($this->usesSoftStates()) {
+ $import->useSoftStates();
+ } else {
+ $import->useHardStates();
+ }
+
+ $this->importedConfigs[$name] = $import;
+ }
+
+ return $this->importedConfigs[$name];
+ }
+
+ public function listInvolvedConfigs(&$configs = null)
+ {
+ if ($configs === null) {
+ $configs[$this->getName()] = $this;
+ }
+
+ foreach ($this->importedNodes as $node) {
+ if (! isset($configs[$node->getConfigName()])) {
+ $config = $node->getBpConfig();
+ $configs[$node->getConfigName()] = $config;
+ $config->listInvolvedConfigs($configs);
+ }
+ }
+
+ return $configs;
+ }
+
+ /**
+ * @return LegacyStorage
+ */
+ protected function storage()
+ {
+ if ($this->storage === null) {
+ $this->storage = LegacyStorage::getInstance();
+ }
+
+ return $this->storage;
+ }
+
+ /**
+ * @param string $name
+ * @return MonitoredNode|BpNode
+ * @throws Exception
+ */
+ public function getNode($name)
+ {
+ if ($name === '__unbound__') {
+ return $this->getUnboundBaseNode();
+ }
+
+ if (array_key_exists($name, $this->nodes)) {
+ return $this->nodes[$name];
+ }
+
+ if ($name[0] === '@') {
+ list($configName, $nodeName) = preg_split('~:\s*~', substr($name, 1), 2);
+ return $this->getImportedConfig($configName)->getNode($nodeName);
+ }
+
+ // Fallback: if it is a service, create an empty one:
+ $this->warn(sprintf('The node "%s" doesn\'t exist', $name));
+
+ [$name, $suffix] = self::splitNodeName($name);
+ if ($suffix !== null) {
+ if ($suffix === 'Hoststatus') {
+ return $this->createHost($name);
+ } else {
+ return $this->createService($name, $suffix);
+ }
+ }
+
+ throw new Exception(
+ sprintf('The node "%s" doesn\'t exist', $name)
+ );
+ }
+
+ /**
+ * @return BpNode
+ */
+ public function getUnboundBaseNode()
+ {
+ // Hint: state is useless here, but triggers parent/child "calculation"
+ // This is an ugly workaround and should be made obsolete
+ $this->calculateAllStates();
+
+ $names = array_keys($this->getUnboundNodes());
+ $bp = new BpNode((object) array(
+ 'name' => '__unbound__',
+ 'operator' => '&',
+ 'child_names' => $names
+ ));
+ $bp->setBpConfig($this);
+ $bp->setAlias($this->translate('Unbound nodes'));
+ return $bp;
+ }
+
+ /**
+ * @param $name
+ * @return BpNode
+ *
+ * @throws NotFoundError
+ */
+ public function getBpNode($name)
+ {
+ if ($this->hasBpNode($name)) {
+ return $this->nodes[$name];
+ }
+
+ $msg = $this->isFaulty()
+ ? sprintf(
+ t('Trying to import node "%s" from faulty config file "%s.conf"'),
+ self::unescapeName($name),
+ $this->getName()
+ )
+ : sprintf(t('Trying to access a missing business process node "%s"'), $name);
+
+ throw new NotFoundError($msg);
+ }
+
+ /**
+ * @param $name
+ *
+ * @return bool
+ */
+ public function hasBpNode($name)
+ {
+ return array_key_exists($name, $this->nodes)
+ && $this->nodes[$name] instanceof BpNode;
+ }
+
+ /**
+ * Set the state for a specific node
+ *
+ * @param string $name Node name
+ * @param int $state Desired state
+ *
+ * @return $this
+ */
+ public function setNodeState($name, $state)
+ {
+ $this->getNode($name)->setState($state);
+ return $this;
+ }
+
+ /**
+ * Add the given node to the given BpNode
+ *
+ * @param $name
+ * @param BpNode $node
+ *
+ * @return $this
+ */
+ public function addNode($name, BpNode $node)
+ {
+ if (array_key_exists($name, $this->nodes)) {
+ $this->warn(
+ sprintf(
+ mt('businessprocess', 'Node "%s" has been defined twice'),
+ $name
+ )
+ );
+ }
+
+ $this->nodes[$name] = $node;
+
+ if ($node->getDisplay() > 0) {
+ if (! $this->isRootNode($name)) {
+ $this->addRootNode($name);
+ }
+ } else {
+ if ($this->isRootNode($name)) {
+ $this->removeRootNode($name);
+ }
+ }
+
+
+ return $this;
+ }
+
+ /**
+ * Remove all occurrences of a specific node by name
+ *
+ * @param $name
+ */
+ public function removeNode($name)
+ {
+ unset($this->nodes[$name]);
+ if (array_key_exists($name, $this->root_nodes)) {
+ unset($this->root_nodes[$name]);
+ }
+
+ foreach ($this->getBpNodes() as $node) {
+ if ($node->hasChild($name)) {
+ $node->removeChild($name);
+ }
+ }
+ }
+
+ /**
+ * Get all business process nodes
+ *
+ * @return BpNode[]
+ */
+ public function getBpNodes()
+ {
+ $nodes = array();
+
+ foreach ($this->nodes as $node) {
+ if ($node instanceof BpNode) {
+ $nodes[$node->getName()] = $node;
+ }
+ }
+
+ return $nodes;
+ }
+
+ /**
+ * List all business process node names
+ *
+ * @return array
+ */
+ public function listBpNodes()
+ {
+ $nodes = array();
+
+ foreach ($this->getBpNodes() as $name => $node) {
+ $alias = $node->getAlias();
+ $nodes[$name] = $name === $alias ? $name : sprintf('%s (%s)', $alias, $node);
+ }
+
+ return $nodes;
+ }
+
+ /**
+ * All business process nodes defined in this config but not
+ * assigned to any parent
+ *
+ * @return BpNode[]
+ */
+ public function getUnboundNodes()
+ {
+ $nodes = array();
+
+ foreach ($this->getBpNodes() as $name => $node) {
+ if ($node->hasParents()) {
+ continue;
+ }
+
+ if ($node->getDisplay() === 0) {
+ $nodes[$name] = $node;
+ }
+ }
+
+ return $nodes;
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasWarnings()
+ {
+ return ! empty($this->warnings);
+ }
+
+ /**
+ * @return array
+ */
+ public function getWarnings()
+ {
+ return $this->warnings;
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasErrors()
+ {
+ return ! empty($this->errors) || $this->isEmpty();
+ }
+
+ /**
+ * @return array
+ */
+ public function getErrors()
+ {
+ $errors = $this->errors;
+ if ($this->isEmpty()) {
+ $errors[] = sprintf(
+ $this->translate(
+ 'No business process nodes for "%s" have been defined yet'
+ ),
+ $this->getTitle()
+ );
+ }
+ return $errors;
+ }
+
+ /**
+ * Translation helper
+ *
+ * @param $msg
+ *
+ * @return mixed|string
+ */
+ public function translate($msg)
+ {
+ return mt('businessprocess', $msg);
+ }
+
+ /**
+ * Add a message to our warning stack
+ *
+ * @param $msg
+ */
+ protected function warn($msg)
+ {
+ $args = func_get_args();
+ array_shift($args);
+ $this->warnings[] = vsprintf($msg, $args);
+ }
+
+ /**
+ * @param string $msg,...
+ *
+ * @return $this
+ *
+ * @throws IcingaException
+ */
+ public function addError($msg)
+ {
+ $args = func_get_args();
+ array_shift($args);
+ if (! empty($args)) {
+ $msg = vsprintf($msg, $args);
+ }
+ if ($this->throwErrors) {
+ throw new IcingaException($msg);
+ }
+
+ if (! in_array($msg, $this->errors)) {
+ $this->errors[] = $msg;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Decide whether errors should be thrown or collected
+ *
+ * @param bool $throw
+ *
+ * @return $this
+ */
+ public function throwErrors($throw = true)
+ {
+ $this->throwErrors = $throw;
+ return $this;
+ }
+
+ /**
+ * Begin loop detection for the given name
+ *
+ * Will throw a NestingError in case this node will be met again below itself
+ *
+ * @param $name
+ *
+ * @throws NestingError
+ */
+ public function beginLoopDetection($name)
+ {
+ // echo "Begin loop $name\n";
+ if (array_key_exists($name, $this->loopDetection)) {
+ $loop = array_keys($this->loopDetection);
+ $loop[] = $name;
+ $this->loopDetection = array();
+ throw new NestingError('Loop detected: %s', implode(' -> ', $loop));
+ }
+
+ $this->loopDetection[$name] = true;
+ }
+
+ /**
+ * Remove the given name from the loop detection stack
+ *
+ * @param $name
+ */
+ public function endLoopDetection($name)
+ {
+ // echo "End loop $this->name\n";
+ unset($this->loopDetection[$name]);
+ }
+
+ /**
+ * Whether this configuration has any Nodes
+ *
+ * @return bool
+ */
+ public function isEmpty()
+ {
+ // This is faster
+ if (! empty($this->root_nodes)) {
+ return false;
+ }
+
+ return count($this->listBpNodes()) === 0;
+ }
+
+ /**
+ * Export the config to array
+ *
+ * @param bool $flat If false, children will be added to the array key children, else the array will be flat
+ *
+ * @return array
+ */
+ public function toArray($flat = false)
+ {
+ $data = [
+ 'name' => $this->getTitle(),
+ 'path' => $this->getTitle()
+ ];
+
+ $children = [];
+
+ foreach ($this->getChildren() as $node) {
+ if ($flat) {
+ $children = array_merge($children, $node->toArray($data, $flat));
+ } else {
+ $children[] = $node->toArray($data, $flat);
+ }
+ }
+
+ if ($flat) {
+ $data = [$data];
+
+ if (! empty($children)) {
+ $data = array_merge($data, $children);
+ }
+ } else {
+ $data['children'] = $children;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Escape the given node name
+ *
+ * @param string $name
+ *
+ * @return string
+ */
+ public static function escapeName(string $name): string
+ {
+ return preg_replace('/((?<!\\\\);)/', '\\\\$1', $name);
+ }
+
+ /**
+ * Unescape the given node name
+ *
+ * @param string $name
+ *
+ * @return string
+ */
+ public static function unescapeName(string $name): string
+ {
+ return str_replace('\\;', ';', $name);
+ }
+
+ /**
+ * Join the given two name parts together
+ *
+ * The used separator is the semicolon. If a semicolon exists in either part, it's escaped.
+ *
+ * @param string $name
+ * @param ?string $suffix
+ *
+ * @return string
+ */
+ public static function joinNodeName(string $name, ?string $suffix = null): string
+ {
+ return self::escapeName($name) . ($suffix ? ";$suffix" : '');
+ }
+
+ /**
+ * Split the given node name into two parts
+ *
+ * The first part is always a string, with any semicolons unescaped.
+ * The second part may be null or a string otherwise.
+ *
+ * @param string $nodeName
+ *
+ * @return array
+ */
+ public static function splitNodeName(string $nodeName): array
+ {
+ $parts = preg_split('/(?<!\\\\);/', $nodeName, 2);
+ $parts[0] = self::unescapeName($parts[0]);
+
+ return array_pad($parts, 2, null);
+ }
+
+ /**
+ * Set whether the config is faulty
+ *
+ * @param bool $isFaulty
+ *
+ * @return $this
+ */
+ public function setFaulty(bool $isFaulty = true): self
+ {
+ $this->isFaulty = $isFaulty;
+
+ return $this;
+ }
+
+ /**
+ * Get whether the config is faulty
+ *
+ * @return bool
+ */
+ public function isFaulty(): bool
+ {
+ return $this->isFaulty;
+ }
+}
diff --git a/library/Businessprocess/BpNode.php b/library/Businessprocess/BpNode.php
new file mode 100644
index 0000000..ab76e3e
--- /dev/null
+++ b/library/Businessprocess/BpNode.php
@@ -0,0 +1,646 @@
+<?php
+
+namespace Icinga\Module\Businessprocess;
+
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Businessprocess\Exception\NestingError;
+use ipl\Web\Widget\Icon;
+
+class BpNode extends Node
+{
+ const OP_AND = '&';
+ const OP_OR = '|';
+ const OP_XOR = '^';
+ const OP_NOT = '!';
+ const OP_DEGRADED = '%';
+
+ protected $operator = '&';
+
+ protected $url;
+
+ protected $display = 0;
+
+ /** @var ?Node[] */
+ protected $children;
+
+ /** @var array */
+ protected $childNames = array();
+ protected $counters;
+ protected $missing = null;
+ protected $empty = null;
+ protected $missingChildren;
+ protected $stateOverrides = [];
+
+ protected static $emptyStateSummary = array(
+ 'CRITICAL' => 0,
+ 'CRITICAL-HANDLED' => 0,
+ 'WARNING' => 0,
+ 'WARNING-HANDLED' => 0,
+ 'UNKNOWN' => 0,
+ 'UNKNOWN-HANDLED' => 0,
+ 'OK' => 0,
+ 'PENDING' => 0,
+ 'MISSING' => 0,
+ 'EMPTY' => 0,
+ );
+
+ protected static $sortStateInversionMap = array(
+ 4 => 0,
+ 3 => 0,
+ 2 => 2,
+ 1 => 1,
+ 0 => 4
+ );
+
+ protected $className = 'process';
+
+ public function __construct($object)
+ {
+ $this->name = BpConfig::escapeName($object->name);
+ $this->alias = BpConfig::unescapeName($object->name);
+ $this->operator = $object->operator;
+ $this->childNames = $object->child_names;
+ }
+
+ public function getStateSummary()
+ {
+ if ($this->counters === null) {
+ $this->getState();
+ $this->counters = self::$emptyStateSummary;
+
+ foreach ($this->getChildren() as $child) {
+ if ($child->isMissing()) {
+ $this->counters['MISSING']++;
+ } else {
+ $state = $child->getStateName($this->getChildState($child));
+ if ($child->isHandled() && ($state !== 'UP' && $state !== 'OK')) {
+ $state = $state . '-HANDLED';
+ }
+
+ if ($state === 'DOWN') {
+ $this->counters['CRITICAL']++;
+ } elseif ($state === 'DOWN-HANDLED') {
+ $this->counters['CRITICAL-HANDLED']++;
+ } elseif ($state === 'UNREACHABLE') {
+ $this->counters['UNKNOWN']++;
+ } elseif ($state === 'UNREACHABLE-HANDLED') {
+ $this->counters['UNKNOWN-HANDLED']++;
+ } elseif ($state === 'PENDING-HANDLED') {
+ $this->counters['PENDING']++;
+ } elseif ($state === 'UP') {
+ $this->counters['OK']++;
+ } else {
+ $this->counters[$state]++;
+ }
+ }
+ }
+ }
+ return $this->counters;
+ }
+
+ public function hasProblems()
+ {
+ if ($this->isProblem()) {
+ return true;
+ }
+
+ $okStates = array('OK', 'UP', 'PENDING', 'MISSING');
+
+ foreach ($this->getStateSummary() as $state => $cnt) {
+ if ($cnt !== 0 && ! in_array($state, $okStates)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @param Node $node
+ * @return $this
+ * @throws ConfigurationError
+ */
+ public function addChild(Node $node)
+ {
+ if ($this->children === null) {
+ $this->getChildren();
+ }
+
+ $name = $node->getName();
+ if (array_key_exists($name, $this->children)) {
+ throw new ConfigurationError(
+ 'Node "%s" has been defined more than once',
+ $name
+ );
+ }
+
+ $this->children[$name] = $node;
+ $this->childNames[] = $name;
+ $node->addParent($this);
+ return $this;
+ }
+
+ public function getProblematicChildren()
+ {
+ $problems = array();
+
+ foreach ($this->getChildren() as $child) {
+ if (isset($this->stateOverrides[$child->getName()])) {
+ $problem = $this->getChildState($child) > 0;
+ } else {
+ $problem = $child->isProblem() || ($child instanceof BpNode && $child->hasProblems());
+ }
+
+ if ($problem) {
+ $problems[] = $child;
+ }
+ }
+
+ return $problems;
+ }
+
+ public function hasChild($name)
+ {
+ return in_array($name, $this->getChildNames());
+ }
+
+ public function removeChild($name)
+ {
+ if (($key = array_search($name, $this->getChildNames())) !== false) {
+ unset($this->childNames[$key]);
+
+ if (! empty($this->children)) {
+ unset($this->children[$name]);
+ }
+
+ $this->childNames = array_values($this->childNames);
+ }
+
+ return $this;
+ }
+
+ public function getProblemTree()
+ {
+ $tree = array();
+
+ foreach ($this->getProblematicChildren() as $child) {
+ $name = $child->getName();
+ $tree[$name] = array(
+ 'node' => $child,
+ 'children' => array()
+ );
+ if ($child instanceof BpNode) {
+ $tree[$name]['children'] = $child->getProblemTree();
+ }
+ }
+
+ return $tree;
+ }
+
+ /**
+ * Get the problem nodes as tree reduced to the nodes which have the same state as the business process
+ *
+ * @param bool $rootCause Reduce nodes to the nodes which are responsible for the state of the business process
+ *
+ * @return array
+ */
+ public function getProblemTreeBlame($rootCause = false)
+ {
+ $tree = [];
+ $nodeState = $this->getState();
+
+ if ($nodeState !== 0) {
+ foreach ($this->getChildren() as $child) {
+ $childState = $this->getChildState($child);
+ $childState = $rootCause ? $child->getSortingState($childState) : $childState;
+ if (($rootCause ? $this->getSortingState() : $nodeState) === $childState) {
+ $name = $child->getName();
+ $tree[$name] = [
+ 'children' => [],
+ 'node' => $child
+ ];
+ if ($child instanceof BpNode) {
+ $tree[$name]['children'] = $child->getProblemTreeBlame($rootCause);
+ }
+ }
+ }
+ }
+
+ return $tree;
+ }
+
+
+ public function isMissing()
+ {
+ if ($this->missing === null) {
+ $exists = false;
+ $bp = $this->getBpConfig();
+ $bp->beginLoopDetection($this->name);
+ foreach ($this->getChildren() as $child) {
+ if (! $child->isMissing()) {
+ $exists = true;
+ }
+ }
+ $bp->endLoopDetection($this->name);
+ $this->missing = ! $exists && ! empty($this->getChildren());
+ }
+ return $this->missing;
+ }
+
+ public function isEmpty()
+ {
+ $bp = $this->getBpConfig();
+ $empty = true;
+ if ($this->countChildren()) {
+ $bp->beginLoopDetection($this->name);
+ foreach ($this->getChildren() as $child) {
+ if ($child instanceof MonitoredNode) {
+ $empty = false;
+ break;
+ } elseif (!$child->isEmpty()) {
+ $empty = false;
+ }
+ }
+ $bp->endLoopDetection($this->name);
+ }
+ $this->empty = $empty;
+
+ return $this->empty;
+ }
+
+
+ public function getMissingChildren()
+ {
+ if ($this->missingChildren === null) {
+ $missing = array();
+
+ foreach ($this->getChildren() as $child) {
+ if ($child->isMissing()) {
+ $missing[$child->getAlias() ?? $child->getName()] = $child;
+ }
+
+ foreach ($child->getMissingChildren() as $m) {
+ $missing[$m->getAlias() ?? $m->getName()] = $m;
+ }
+ }
+
+ $this->missingChildren = $missing;
+ }
+
+ return $this->missingChildren;
+ }
+
+ public function getOperator()
+ {
+ return $this->operator;
+ }
+
+ public function setOperator($operator)
+ {
+ $this->assertValidOperator($operator);
+ $this->operator = $operator;
+ return $this;
+ }
+
+ protected function assertValidOperator($operator)
+ {
+ switch ($operator) {
+ case self::OP_AND:
+ case self::OP_OR:
+ case self::OP_XOR:
+ case self::OP_NOT:
+ case self::OP_DEGRADED:
+ return;
+ default:
+ if (is_numeric($operator)) {
+ return;
+ }
+ }
+
+ throw new ConfigurationError(
+ 'Got invalid operator: %s',
+ $operator
+ );
+ }
+
+ public function setInfoUrl($url)
+ {
+ $this->url = $url;
+ return $this;
+ }
+
+ public function hasInfoUrl()
+ {
+ return ! empty($this->url);
+ }
+
+ public function getInfoUrl()
+ {
+ return $this->url;
+ }
+
+ public function setStateOverrides(array $overrides, $name = null)
+ {
+ if ($name === null) {
+ $this->stateOverrides = $overrides;
+ } else {
+ $this->stateOverrides[$name] = $overrides;
+ }
+
+ return $this;
+ }
+
+ public function getStateOverrides($name = null)
+ {
+ $overrides = null;
+ if ($name !== null) {
+ if (isset($this->stateOverrides[$name])) {
+ $overrides = $this->stateOverrides[$name];
+ }
+ } else {
+ $overrides = $this->stateOverrides;
+ }
+
+ return $overrides;
+ }
+
+ public function getAlias()
+ {
+ return $this->alias ? preg_replace('~_~', ' ', $this->alias) : $this->name;
+ }
+
+ /**
+ * @return int
+ */
+ public function getState()
+ {
+ if ($this->state === null) {
+ try {
+ $this->reCalculateState();
+ } catch (NestingError $e) {
+ $this->getBpConfig()->addError(
+ $this->getBpConfig()->translate('Nesting error detected: %s'),
+ $e->getMessage()
+ );
+
+ // Failing nodes are unknown
+ $this->state = 3;
+ }
+ }
+
+ return $this->state;
+ }
+
+ /**
+ * Get the given child's state, possibly adjusted by override rules
+ *
+ * @param Node|string $child
+ * @return int
+ */
+ public function getChildState($child)
+ {
+ if (! $child instanceof Node) {
+ $child = $this->getChildByName($child);
+ }
+
+ $childName = $child->getName();
+ $childState = $child->getState();
+ if (! isset($this->stateOverrides[$childName][$childState])) {
+ return $childState;
+ }
+
+ return $this->stateOverrides[$childName][$childState];
+ }
+
+ public function getHtmlId()
+ {
+ return 'businessprocess-' . preg_replace('/[\r\n\t\s]/', '_', $this->getName());
+ }
+
+ protected function invertSortingState($state)
+ {
+ return self::$sortStateInversionMap[$state >> self::SHIFT_FLAGS] << self::SHIFT_FLAGS;
+ }
+
+ /**
+ * @return $this
+ */
+ public function reCalculateState()
+ {
+ $bp = $this->getBpConfig();
+
+ $sort_states = array();
+ $lastStateChange = 0;
+
+ if ($this->isEmpty()) {
+ // TODO: delegate this to operators, should mostly fail
+ $this->setState(self::NODE_EMPTY);
+ return $this;
+ }
+
+ foreach ($this->getChildren() as $child) {
+ $bp->beginLoopDetection($this->name);
+ if ($child instanceof MonitoredNode && $child->isMissing()) {
+ if ($child instanceof HostNode) {
+ $child->setState(self::ICINGA_UNREACHABLE);
+ } else {
+ $child->setState(self::ICINGA_UNKNOWN);
+ }
+
+ $child->setMissing();
+ }
+ $sort_states[] = $child->getSortingState($this->getChildState($child));
+ $lastStateChange = max($lastStateChange, $child->getLastStateChange());
+ $bp->endLoopDetection($this->name);
+ }
+
+ $this->setLastStateChange($lastStateChange);
+
+ switch ($this->getOperator()) {
+ case self::OP_AND:
+ $sort_state = max($sort_states);
+ break;
+ case self::OP_NOT:
+ $sort_state = $this->invertSortingState(max($sort_states));
+ break;
+ case self::OP_OR:
+ $sort_state = min($sort_states);
+ break;
+ case self::OP_XOR:
+ $actualGood = 0;
+ foreach ($sort_states as $s) {
+ if ($this->sortStateTostate($s) === self::ICINGA_OK) {
+ $actualGood++;
+ }
+ }
+
+ if ($actualGood === 1) {
+ $this->state = self::ICINGA_OK;
+ } else {
+ $this->state = self::ICINGA_CRITICAL;
+ }
+
+ return $this;
+ case self::OP_DEGRADED:
+ $maxState = max($sort_states);
+ $flags = $maxState & 0xf;
+
+ $maxIcingaState = $this->sortStateTostate($maxState);
+ $warningState = ($this->stateToSortState(self::ICINGA_WARNING) << self::SHIFT_FLAGS) + $flags;
+
+ $sort_state = ($maxIcingaState === self::ICINGA_CRITICAL) ? $warningState : $maxState;
+ break;
+ default:
+ // MIN:
+ $sort_state = 3 << self::SHIFT_FLAGS;
+
+ if (count($sort_states) >= $this->operator) {
+ $actualGood = 0;
+ foreach ($sort_states as $s) {
+ if (($s >> self::SHIFT_FLAGS) === self::ICINGA_OK) {
+ $actualGood++;
+ }
+ }
+
+ if ($actualGood >= $this->operator) {
+ // condition is fulfilled
+ $sort_state = self::ICINGA_OK;
+ } else {
+ // worst state if not fulfilled
+ $sort_state = max($sort_states);
+ }
+ }
+ }
+ if ($sort_state & self::FLAG_DOWNTIME) {
+ $this->setDowntime(true);
+ }
+ if ($sort_state & self::FLAG_ACK) {
+ $this->setAck(true);
+ }
+
+ $this->state = $this->sortStateTostate($sort_state);
+ return $this;
+ }
+
+ public function checkForLoops()
+ {
+ $bp = $this->getBpConfig();
+ foreach ($this->getChildren() as $child) {
+ $bp->beginLoopDetection($this->name);
+ if ($child instanceof BpNode) {
+ $child->checkForLoops();
+ }
+ $bp->endLoopDetection($this->name);
+ }
+
+ return $this;
+ }
+
+ public function setDisplay($display)
+ {
+ $this->display = (int) $display;
+ return $this;
+ }
+
+ public function getDisplay()
+ {
+ return $this->display;
+ }
+
+ public function setChildNames($names)
+ {
+ $this->childNames = $names;
+ $this->children = null;
+ return $this;
+ }
+
+ public function hasChildren($filter = null)
+ {
+ $childNames = $this->getChildNames();
+ return !empty($childNames);
+ }
+
+ public function getChildNames()
+ {
+ return $this->childNames;
+ }
+
+ public function getChildren($filter = null)
+ {
+ if ($this->children === null) {
+ $this->children = [];
+ foreach ($this->getChildNames() as $name) {
+ $this->children[$name] = $this->getBpConfig()->getNode($name);
+ $this->children[$name]->addParent($this);
+ }
+ }
+
+ return $this->children;
+ }
+
+ /**
+ * return BpNode[]
+ */
+ public function getChildBpNodes()
+ {
+ $children = array();
+
+ foreach ($this->getChildren() as $name => $child) {
+ if ($child instanceof BpNode) {
+ $children[$name] = $child;
+ }
+ }
+
+ return $children;
+ }
+
+ /**
+ * @param $childName
+ * @return Node
+ * @throws NotFoundError
+ */
+ public function getChildByName($childName)
+ {
+ foreach ($this->getChildren() as $name => $child) {
+ if ($name === $childName) {
+ return $child;
+ }
+ }
+
+ throw new NotFoundError('Trying to get missing child %s', $childName);
+ }
+
+ protected function assertNumericOperator()
+ {
+ if (! is_numeric($this->getOperator())) {
+ throw new ConfigurationError('Got invalid operator: %s', $this->operator);
+ }
+ }
+
+ public function operatorHtml()
+ {
+ switch ($this->getOperator()) {
+ case self::OP_AND:
+ return 'AND';
+ case self::OP_OR:
+ return 'OR';
+ case self::OP_XOR:
+ return 'XOR';
+ case self::OP_NOT:
+ return 'NOT';
+ case self::OP_DEGRADED:
+ return 'DEG';
+ default:
+ // MIN
+ $this->assertNumericOperator();
+ return 'min:' . $this->operator;
+ }
+ }
+
+ public function getIcon(): Icon
+ {
+ $this->icon = $this->hasParents() ? 'cubes' : 'sitemap';
+ return parent::getIcon();
+ }
+}
diff --git a/library/Businessprocess/Common/Sort.php b/library/Businessprocess/Common/Sort.php
new file mode 100644
index 0000000..4728af3
--- /dev/null
+++ b/library/Businessprocess/Common/Sort.php
@@ -0,0 +1,158 @@
+<?php
+// Icinga Business Process Modelling | (c) 2023 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Businessprocess\Common;
+
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\Node;
+use InvalidArgumentException;
+use ipl\Stdlib\Str;
+
+trait Sort
+{
+ /** @var ?string Current sort specification */
+ protected $sort;
+
+ /** @var ?callable Actual sorting function */
+ protected $sortFn;
+
+ /**
+ * Get the sort specification
+ *
+ * @return ?string
+ */
+ public function getSort(): ?string
+ {
+ return $this->sort;
+ }
+
+ /**
+ * Set the sort specification
+ *
+ * @param ?string $sort
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException When sorting according to the specified specification is not possible
+ */
+ public function setSort(?string $sort): self
+ {
+ if (empty($sort)) {
+ return $this;
+ }
+
+ list($sortBy, $direction) = Str::symmetricSplit($sort, ' ', 2, 'asc');
+
+ switch ($sortBy) {
+ case 'manual':
+ if ($direction === 'asc') {
+ $this->sortFn = function (array &$nodes) {
+ $firstNode = reset($nodes);
+ if ($firstNode instanceof BpNode && $firstNode->getDisplay() > 0) {
+ $nodes = self::applyManualSorting($nodes);
+ }
+
+ // Child nodes don't need to be ordered in this case, their implicit order is significant
+ };
+ } else {
+ $this->sortFn = function (array &$nodes) {
+ $firstNode = reset($nodes);
+ if ($firstNode instanceof BpNode && $firstNode->getDisplay() > 0) {
+ uasort($nodes, function (BpNode $a, BpNode $b) {
+ return $b->getDisplay() <=> $a->getDisplay();
+ });
+ } else {
+ $nodes = array_reverse($nodes);
+ }
+ };
+ }
+
+ break;
+ case 'display_name':
+ if ($direction === 'asc') {
+ $this->sortFn = function (array &$nodes) {
+ uasort($nodes, function (Node $a, Node $b) {
+ return strnatcasecmp(
+ $a->getAlias() ?? $a->getName(),
+ $b->getAlias() ?? $b->getName()
+ );
+ });
+ };
+ } else {
+ $this->sortFn = function (array &$nodes) {
+ uasort($nodes, function (Node $a, Node $b) {
+ return strnatcasecmp(
+ $b->getAlias() ?? $b->getName(),
+ $a->getAlias() ?? $a->getName()
+ );
+ });
+ };
+ }
+
+ break;
+ case 'state':
+ if ($direction === 'asc') {
+ $this->sortFn = function (array &$nodes) {
+ uasort($nodes, function (Node $a, Node $b) {
+ return $a->getSortingState() <=> $b->getSortingState();
+ });
+ };
+ } else {
+ $this->sortFn = function (array &$nodes) {
+ uasort($nodes, function (Node $a, Node $b) {
+ return $b->getSortingState() <=> $a->getSortingState();
+ });
+ };
+ }
+
+ break;
+ default:
+ throw new InvalidArgumentException(sprintf(
+ "Can't sort by %s. It's only possible to sort by manual order, display_name or state",
+ $sortBy
+ ));
+ }
+
+ $this->sort = $sort;
+
+ return $this;
+ }
+
+ /**
+ * Sort the given nodes as specified by {@see setSort()}
+ *
+ * If {@see setSort()} has not been called yet, the default sort specification is used
+ *
+ * @param array $nodes
+ *
+ * @return array
+ */
+ public function sort(array $nodes): array
+ {
+ if (empty($nodes)) {
+ return $nodes;
+ }
+
+ if ($this->sortFn !== null) {
+ call_user_func_array($this->sortFn, [&$nodes]);
+ }
+
+ return $nodes;
+ }
+
+ /**
+ * Apply manual sort order on the given process nodes
+ *
+ * @param array $bpNodes
+ *
+ * @return array
+ */
+ public static function applyManualSorting(array $bpNodes): array
+ {
+ uasort($bpNodes, function (BpNode $a, BpNode $b) {
+ return $a->getDisplay() <=> $b->getDisplay();
+ });
+
+ return $bpNodes;
+ }
+}
diff --git a/library/Businessprocess/Director/ShipConfigFiles.php b/library/Businessprocess/Director/ShipConfigFiles.php
new file mode 100644
index 0000000..17b9e1f
--- /dev/null
+++ b/library/Businessprocess/Director/ShipConfigFiles.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Director;
+
+use Icinga\Module\Director\Hook\ShipConfigFilesHook;
+use Icinga\Module\Businessprocess\Storage\LegacyStorage;
+
+class ShipConfigFiles extends ShipConfigFilesHook
+{
+ public function fetchFiles()
+ {
+ $files = array();
+
+ $storage = LegacyStorage::getInstance();
+
+ foreach ($storage->listProcesses() as $name => $title) {
+ $files['processes/' . $name . '.bp'] = $storage->getSource($name);
+ }
+
+ return $files;
+ }
+}
diff --git a/library/Businessprocess/Exception/ModificationError.php b/library/Businessprocess/Exception/ModificationError.php
new file mode 100644
index 0000000..430d513
--- /dev/null
+++ b/library/Businessprocess/Exception/ModificationError.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Exception;
+
+use Icinga\Exception\IcingaException;
+
+class ModificationError extends IcingaException
+{
+}
diff --git a/library/Businessprocess/Exception/NestingError.php b/library/Businessprocess/Exception/NestingError.php
new file mode 100644
index 0000000..89cbf81
--- /dev/null
+++ b/library/Businessprocess/Exception/NestingError.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Exception;
+
+use Icinga\Exception\IcingaException;
+
+class NestingError extends IcingaException
+{
+}
diff --git a/library/Businessprocess/HostNode.php b/library/Businessprocess/HostNode.php
new file mode 100644
index 0000000..df25630
--- /dev/null
+++ b/library/Businessprocess/HostNode.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Icinga\Module\Businessprocess;
+
+use Icinga\Module\Businessprocess\Web\Url;
+
+class HostNode extends MonitoredNode
+{
+ protected $sortStateToStateMap = array(
+ 4 => self::ICINGA_DOWN,
+ 3 => self::ICINGA_UNREACHABLE,
+ 1 => self::ICINGA_PENDING,
+ 0 => self::ICINGA_UP
+ );
+
+ protected $stateToSortStateMap = array(
+ self::ICINGA_PENDING => 1,
+ self::ICINGA_UNREACHABLE => 3,
+ self::ICINGA_DOWN => 4,
+ self::ICINGA_UP => 0,
+ );
+
+ protected $stateNames = array(
+ 'UP',
+ 'DOWN',
+ 'UNREACHABLE',
+ 99 => 'PENDING'
+ );
+
+ protected $hostname;
+
+ protected $className = 'host';
+
+ protected $icon = 'laptop';
+
+ public function __construct($object)
+ {
+ $this->name = BpConfig::joinNodeName($object->hostname, 'Hoststatus');
+ $this->hostname = $object->hostname;
+ if (isset($object->state)) {
+ $this->setState($object->state);
+ } else {
+ $this->setState(0)->setMissing();
+ }
+ }
+
+ public function getHostname()
+ {
+ return $this->hostname;
+ }
+
+ public function getUrl()
+ {
+ $params = array(
+ 'host' => $this->getHostname(),
+ );
+
+ if ($this->getBpConfig()->hasBackendName()) {
+ $params['backend'] = $this->getBpConfig()->getBackendName();
+ }
+
+ return Url::fromPath('businessprocess/host/show', $params);
+ }
+}
diff --git a/library/Businessprocess/IcingaDbObject.php b/library/Businessprocess/IcingaDbObject.php
new file mode 100644
index 0000000..cad459f
--- /dev/null
+++ b/library/Businessprocess/IcingaDbObject.php
@@ -0,0 +1,94 @@
+<?php
+
+namespace Icinga\Module\Businessprocess;
+
+use Icinga\Module\Icingadb\Common\Auth;
+use Icinga\Module\Icingadb\Common\Database as IcingadbDatabase;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Module\Icingadb\Model\Service;
+use ipl\Sql\Connection as IcingaDbConnection;
+use ipl\Web\Filter\QueryString;
+
+class IcingaDbObject
+{
+ use IcingadbDatabase;
+
+ use Auth;
+
+ /** @var BpConfig */
+ protected $config;
+
+ /** @var IcingaDbConnection */
+ protected $conn;
+
+ public function __construct()
+ {
+ $this->conn = $this->getDb();
+ }
+
+ public function fetchHosts($filter = null)
+ {
+
+ $hosts = Host::on($this->conn);
+
+ if ($filter !== null) {
+ $filterQuery = QueryString::parse($filter);
+
+ $hosts->filter($filterQuery);
+ }
+
+ $hosts->orderBy('host.name');
+
+ $this->applyIcingaDbRestrictions($hosts);
+
+ return $hosts;
+ }
+
+ public function fetchServices($filter)
+ {
+ $services = Service::on($this->conn)
+ ->with('host');
+
+ if ($filter !== null) {
+ $filterQuery = QueryString::parse($filter);
+
+ $services->filter($filterQuery);
+ }
+
+ $services->orderBy('service.name');
+
+ $this->applyIcingaDbRestrictions($services);
+
+ return $services;
+ }
+
+ public function yieldHostnames($filter = null)
+ {
+ foreach ($this->fetchHosts($filter) as $host) {
+ yield $host->name;
+ }
+ }
+
+ public function yieldServicenames($host)
+ {
+ $filter = "host.name=$host";
+
+ foreach ($this->fetchServices($filter) as $service) {
+ yield $service->name;
+ }
+ }
+
+ public static function applyIcingaDbRestrictions($query)
+ {
+ $object = new self;
+ $object->applyRestrictions($query);
+
+ return $object;
+ }
+
+ public static function fetchDb()
+ {
+ $object = new self;
+ return $object->getDb();
+ }
+}
diff --git a/library/Businessprocess/ImportedNode.php b/library/Businessprocess/ImportedNode.php
new file mode 100644
index 0000000..a0eb6b1
--- /dev/null
+++ b/library/Businessprocess/ImportedNode.php
@@ -0,0 +1,139 @@
+<?php
+
+namespace Icinga\Module\Businessprocess;
+
+use Exception;
+
+class ImportedNode extends BpNode
+{
+ /** @var BpConfig */
+ protected $parentBp;
+
+ /** @var string */
+ protected $configName;
+
+ /** @var string */
+ protected $nodeName;
+
+ /** @var BpNode */
+ protected $importedNode;
+
+ /** @var string */
+ protected $className = 'process subtree';
+
+ /** @var string */
+ protected $icon = 'download';
+
+ public function __construct(BpConfig $parentBp, $object)
+ {
+ $this->parentBp = $parentBp;
+ $this->configName = $object->configName;
+ $this->nodeName = BpConfig::escapeName($object->node);
+
+ parent::__construct((object) [
+ 'name' => '@' . $this->configName . ':' . $this->nodeName,
+ 'operator' => null,
+ 'child_names' => null
+ ]);
+ }
+
+ /**
+ * @return string
+ */
+ public function getConfigName()
+ {
+ return $this->configName;
+ }
+
+ /**
+ * @return string
+ */
+ public function getNodeName()
+ {
+ return $this->nodeName;
+ }
+
+ public function getIdentifier()
+ {
+ return $this->getName();
+ }
+
+ public function getBpConfig()
+ {
+ if ($this->bp === null) {
+ $this->bp = $this->parentBp->getImportedConfig($this->configName);
+ }
+
+ return $this->bp;
+ }
+
+ public function getAlias()
+ {
+ return $this->importedNode()->getAlias();
+ }
+
+ public function getOperator()
+ {
+ if ($this->operator === null) {
+ $this->operator = $this->importedNode()->getOperator();
+ }
+
+ return $this->operator;
+ }
+
+ public function getChildNames()
+ {
+ if ($this->childNames === null) {
+ $this->childNames = $this->importedNode()->getChildNames();
+ }
+
+ return $this->childNames;
+ }
+
+ public function isMissing()
+ {
+ if ($this->missing === null && $this->getBpConfig()->isFaulty()) {
+ $this->missing = true;
+ }
+
+ return parent::isMissing();
+ }
+
+ /**
+ * @return BpNode
+ */
+ protected function importedNode()
+ {
+ if ($this->importedNode === null) {
+ try {
+ $this->importedNode = $this->getBpConfig()->getBpNode($this->nodeName);
+ } catch (Exception $e) {
+ return $this->createFailedNode($e);
+ }
+ }
+
+ return $this->importedNode;
+ }
+
+ /**
+ * @param Exception $e
+ *
+ * @return BpNode
+ */
+ protected function createFailedNode(Exception $e)
+ {
+ $this->parentBp->addError($e->getMessage());
+ $node = new BpNode((object) array(
+ 'name' => $this->getName(),
+ 'operator' => '&',
+ 'child_names' => []
+ ));
+ $node->setBpConfig($this->getBpConfig());
+ $node->setState(2);
+ $node->setMissing()
+ ->setDowntime(false)
+ ->setAck(false);
+
+ return $node;
+ }
+}
diff --git a/library/Businessprocess/Metadata.php b/library/Businessprocess/Metadata.php
new file mode 100644
index 0000000..b640fb8
--- /dev/null
+++ b/library/Businessprocess/Metadata.php
@@ -0,0 +1,264 @@
+<?php
+
+namespace Icinga\Module\Businessprocess;
+
+use Icinga\Application\Icinga;
+use Icinga\Authentication\Auth;
+use Icinga\Exception\ProgrammingError;
+use Icinga\User;
+
+class Metadata
+{
+ /** @var string Configuration name */
+ protected $name;
+
+ protected $properties = array(
+ 'Title' => null,
+ 'Description' => null,
+ 'Owner' => null,
+ 'AllowedUsers' => null,
+ 'AllowedGroups' => null,
+ 'AllowedRoles' => null,
+ 'AddToMenu' => null,
+ 'Backend' => null,
+ 'Statetype' => null,
+ 'ManualOrder' => null,
+ // 'SLAHosts' => null
+ );
+
+ public function __construct($name)
+ {
+ $this->name = $name;
+ }
+
+ public function getTitle()
+ {
+ if ($this->has('Title')) {
+ return $this->get('Title');
+ } else {
+ return $this->name;
+ }
+ }
+
+ public function getExtendedTitle()
+ {
+ $title = $this->getTitle();
+
+ if ($title === $this->name) {
+ return $title;
+ } else {
+ return sprintf('%s (%s)', $title, $this->name);
+ }
+ }
+
+ public function getProperties()
+ {
+ return $this->properties;
+ }
+
+ public function hasKey($key)
+ {
+ return array_key_exists($key, $this->properties);
+ }
+
+ public function get($key, $default = null)
+ {
+ $this->assertKeyExists($key);
+ if ($this->properties[$key] === null) {
+ return $default;
+ }
+
+ return $this->properties[$key];
+ }
+
+ public function set($key, $value)
+ {
+ $this->assertKeyExists($key);
+ $this->properties[$key] = $value;
+
+ return $this;
+ }
+
+ public function isNull($key)
+ {
+ return null === $this->get($key);
+ }
+
+ public function has($key)
+ {
+ return null !== $this->get($key);
+ }
+
+ protected function assertKeyExists($key)
+ {
+ if (! $this->hasKey($key)) {
+ throw new ProgrammingError('Trying to access invalid header key: %s', $key);
+ }
+
+ return $this;
+ }
+
+ public function hasRestrictions()
+ {
+ return ! (
+ $this->isNull('AllowedUsers')
+ && $this->isNull('AllowedGroups')
+ && $this->isNull('AllowedRoles')
+ );
+ }
+
+ protected function getAuth()
+ {
+ return Auth::getInstance();
+ }
+
+ public function canModify(Auth $auth = null)
+ {
+ if ($auth === null) {
+ if (Icinga::app()->isCli()) {
+ return true;
+ } else {
+ $auth = $this->getAuth();
+ }
+ }
+
+ return $this->canRead($auth) && (
+ $auth->hasPermission('businessprocess/modify')
+ || $this->ownerIs($auth->getUser()->getUsername())
+ );
+ }
+
+ public function canRead(Auth $auth = null)
+ {
+ if ($auth === null) {
+ if (Icinga::app()->isCli()) {
+ return true;
+ } else {
+ $auth = $this->getAuth();
+ }
+ }
+
+ if ($auth->hasPermission('businessprocess/showall')) {
+ return true;
+ }
+
+ $prefixes = $auth->getRestrictions('businessprocess/prefix');
+ if (! empty($prefixes)) {
+ if (! $this->nameIsPrefixedWithOneOf($prefixes)) {
+ return false;
+ }
+ }
+
+ if (! $this->hasRestrictions()) {
+ return true;
+ }
+
+ if (! $auth->isAuthenticated()) {
+ return false;
+ }
+
+ return $this->userCanRead($auth->getUser());
+ }
+
+ public function nameIsPrefixedWithOneOf(array $prefixes)
+ {
+ foreach ($prefixes as $prefix) {
+ if (substr($this->name, 0, strlen($prefix)) === $prefix) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ protected function userCanRead(User $user)
+ {
+ $username = $user->getUsername();
+
+ return $this->ownerIs($username)
+ || $this->isInAllowedUserList($username)
+ || $this->isMemberOfAllowedGroups($user)
+ || $this->hasOneOfTheAllowedRoles($user);
+ }
+
+ public function ownerIs($username)
+ {
+ return $this->get('Owner') === $username;
+ }
+
+ public function listAllowedUsers()
+ {
+ // TODO: $this->get('AllowedUsers', array());
+ $list = $this->get('AllowedUsers');
+ if ($list === null) {
+ return array();
+ } else {
+ return $this->splitCommaSeparated($list);
+ }
+ }
+
+ public function listAllowedGroups()
+ {
+ $list = $this->get('AllowedGroups');
+ if ($list === null) {
+ return array();
+ } else {
+ return $this->splitCommaSeparated($list);
+ }
+ }
+
+ public function listAllowedRoles()
+ {
+ $list = $this->get('AllowedRoles');
+ if ($list === null) {
+ return array();
+ } else {
+ return $this->splitCommaSeparated($list);
+ }
+ }
+
+ public function isInAllowedUserList($username)
+ {
+ foreach ($this->listAllowedUsers() as $allowedUser) {
+ if ($username === $allowedUser) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public function isMemberOfAllowedGroups(User $user)
+ {
+ foreach ($this->listAllowedGroups() as $group) {
+ if ($user->isMemberOf($group)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public function hasOneOfTheAllowedRoles(User $user)
+ {
+ foreach ($this->listAllowedRoles() as $roleName) {
+ foreach ($user->getRoles() as $role) {
+ if ($role->getName() === $roleName) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public function isManuallyOrdered()
+ {
+ return $this->get('ManualOrder') === 'yes';
+ }
+
+ protected function splitCommaSeparated($string)
+ {
+ return preg_split('/\s*,\s*/', $string, -1, PREG_SPLIT_NO_EMPTY);
+ }
+}
diff --git a/library/Businessprocess/Modification/NodeAction.php b/library/Businessprocess/Modification/NodeAction.php
new file mode 100644
index 0000000..b5baa5d
--- /dev/null
+++ b/library/Businessprocess/Modification/NodeAction.php
@@ -0,0 +1,179 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Modification;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\Exception\ModificationError;
+use Icinga\Module\Businessprocess\Node;
+use Icinga\Exception\ProgrammingError;
+
+/**
+ * Abstract NodeAction class
+ *
+ * Every instance of a NodeAction represents a single applied change. Changes are pushed to
+ * a stack and consumed from there. When persisted, NodeActions are serialized with their name,
+ * node name and optionally additional properties according preserveProperties. For each property
+ * that should be preserved, getter and setter methods have to be defined.
+ *
+ * @package Icinga\Module\Businessprocess
+ */
+abstract class NodeAction
+{
+ /** @var string Name of this action (currently create, modify, remove) */
+ protected $actionName;
+
+ /** @var string Name of the node this action applies to */
+ protected $nodeName;
+
+ /** @var array Properties which should be preserved when serializing this action */
+ protected $preserveProperties = array();
+
+ /**
+ * NodeAction constructor.
+ *
+ * @param Node|string $node
+ */
+ public function __construct($node = null)
+ {
+ if ($node !== null) {
+ $this->nodeName = (string) $node;
+ }
+ }
+
+ /**
+ * Every NodeAction must be able to apply itself to a BusinessProcess
+ *
+ * @param BpConfig $config
+ * @return mixed
+ */
+ abstract public function applyTo(BpConfig $config);
+
+ /**
+ * Every NodeAction must be able to tell whether it can be applied to a BusinessProcess
+ *
+ * @param BpConfig $config
+ *
+ * @throws ModificationError
+ *
+ * @return bool
+ */
+ abstract public function appliesTo(BpConfig $config);
+
+ /**
+ * The name of the node this modification applies to
+ *
+ * @return string
+ */
+ public function getNodeName()
+ {
+ return $this->nodeName;
+ }
+
+ public function hasNode()
+ {
+ return $this->nodeName !== null;
+ }
+
+ /**
+ * Whether this is an instance of a given action name
+ *
+ * @param string $actionName
+ * @return bool
+ */
+ public function is($actionName)
+ {
+ return $this->getActionName() === $actionName;
+ }
+
+ /**
+ * Throw a ModificationError
+ *
+ * @param string $msg
+ * @param mixed ...
+ *
+ * @throws ModificationError
+ */
+ protected function error($msg)
+ {
+ $error = ModificationError::create(func_get_args());
+ /** @var ModificationError $error */
+ throw $error;
+ }
+
+ /**
+ * Create an instance of a given actionName for a specific Node
+ *
+ * @param string $actionName
+ * @param string $nodeName
+ *
+ * @return static
+ */
+ public static function create($actionName, $nodeName)
+ {
+ $className = __NAMESPACE__ . '\\Node' . ucfirst($actionName) . 'Action';
+
+ return new $className($nodeName);
+ }
+
+ /**
+ * Returns a JSON-encoded serialized NodeAction
+ *
+ * @return string
+ */
+ public function serialize()
+ {
+ $object = (object) array(
+ 'actionName' => $this->getActionName(),
+ 'nodeName' => $this->getNodeName(),
+ 'properties' => array()
+ );
+
+ foreach ($this->preserveProperties as $key) {
+ $func = 'get' . ucfirst($key);
+ $object->properties[$key] = $this->$func();
+ }
+
+ return json_encode($object);
+ }
+
+ /**
+ * Decodes a JSON-serialized NodeAction and returns an object instance
+ *
+ * @param $string
+ * @return NodeAction
+ */
+ public static function unSerialize($string)
+ {
+ $object = json_decode($string, true);
+ $action = self::create($object['actionName'], $object['nodeName']);
+
+ foreach ($object['properties'] as $key => $val) {
+ $func = 'set' . ucfirst($key);
+ $action->$func($val);
+ }
+
+ return $action;
+ }
+
+ /**
+ * Returns the defined action name or determines such from the class name
+ *
+ * @return string The action name
+ *
+ * @throws ProgrammingError when no such class exists
+ */
+ public function getActionName()
+ {
+ if ($this->actionName === null) {
+ if (! preg_match('/\\\Node(\w+)Action$/', get_class($this), $m)) {
+ throw new ProgrammingError(
+ '"%s" is not a NodeAction class',
+ get_class($this)
+ );
+ }
+ $this->actionName = lcfirst($m[1]);
+ }
+
+ return $this->actionName;
+ }
+}
diff --git a/library/Businessprocess/Modification/NodeAddChildrenAction.php b/library/Businessprocess/Modification/NodeAddChildrenAction.php
new file mode 100644
index 0000000..162c380
--- /dev/null
+++ b/library/Businessprocess/Modification/NodeAddChildrenAction.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Modification;
+
+use Icinga\Module\Businessprocess\BpConfig;
+
+class NodeAddChildrenAction extends NodeAction
+{
+ protected $children = array();
+
+ protected $preserveProperties = array('children');
+
+ /**
+ * @inheritdoc
+ */
+ public function appliesTo(BpConfig $config)
+ {
+ $name = $this->getNodeName();
+
+ if (! $config->hasBpNode($name)) {
+ $this->error('Process "%s" not found', $name);
+ }
+
+ return true;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function applyTo(BpConfig $config)
+ {
+ $node = $config->getBpNode($this->getNodeName());
+
+ foreach ($this->children as $name) {
+ if (! $config->hasNode($name) || $config->getNode($name)->getBpConfig()->getName() !== $config->getName()) {
+ [$prefix, $suffix] = BpConfig::splitNodeName($name);
+ if ($suffix !== null) {
+ if ($suffix === 'Hoststatus') {
+ $config->createHost($prefix);
+ } else {
+ $config->createService($prefix, $suffix);
+ }
+ } elseif ($name[0] === '@' && strpos($name, ':') !== false) {
+ list($configName, $nodeName) = preg_split('~:\s*~', substr($name, 1), 2);
+ $config->createImportedNode($configName, $nodeName);
+ }
+ }
+ $node->addChild($config->getNode($name));
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param array|string $children
+ * @return $this
+ */
+ public function setChildren($children)
+ {
+ if (is_string($children)) {
+ $children = array($children);
+ }
+ $this->children = $children;
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getChildren()
+ {
+ return $this->children;
+ }
+}
diff --git a/library/Businessprocess/Modification/NodeApplyManualOrderAction.php b/library/Businessprocess/Modification/NodeApplyManualOrderAction.php
new file mode 100644
index 0000000..4ad53e0
--- /dev/null
+++ b/library/Businessprocess/Modification/NodeApplyManualOrderAction.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Modification;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\Common\Sort;
+
+class NodeApplyManualOrderAction extends NodeAction
+{
+ use Sort;
+
+ public function appliesTo(BpConfig $config)
+ {
+ return $config->getMetadata()->get('ManualOrder') !== 'yes';
+ }
+
+ public function applyTo(BpConfig $config)
+ {
+ $i = 0;
+ foreach ($config->getBpNodes() as $name => $node) {
+ if ($node->getDisplay() > 0) {
+ $node->setDisplay(++$i);
+ }
+
+ if ($node->hasChildren()) {
+ $node->setChildNames(array_keys(
+ $this->setSort('display_name asc')
+ ->sort($node->getChildren())
+ ));
+ }
+ }
+
+ $config->getMetadata()->set('ManualOrder', 'yes');
+ }
+}
diff --git a/library/Businessprocess/Modification/NodeCopyAction.php b/library/Businessprocess/Modification/NodeCopyAction.php
new file mode 100644
index 0000000..80d781b
--- /dev/null
+++ b/library/Businessprocess/Modification/NodeCopyAction.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Modification;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\Common\Sort;
+
+class NodeCopyAction extends NodeAction
+{
+ use Sort;
+
+ /**
+ * @param BpConfig $config
+ * @return bool
+ */
+ public function appliesTo(BpConfig $config)
+ {
+ $name = $this->getNodeName();
+
+ if (! $config->hasBpNode($name)) {
+ $this->error('Process "%s" not found', $name);
+ }
+
+ if ($config->hasRootNode($name)) {
+ $this->error('A toplevel node with name "%s" already exists', $name);
+ }
+
+ return true;
+ }
+
+ /**
+ * @param BpConfig $config
+ */
+ public function applyTo(BpConfig $config)
+ {
+ $name = $this->getNodeName();
+
+ $display = 1;
+ if ($config->getMetadata()->isManuallyOrdered()) {
+ $rootNodes = self::applyManualSorting($config->getRootNodes());
+ $display = end($rootNodes)->getDisplay() + 1;
+ }
+
+ $config->addRootNode($name)
+ ->getBpNode($name)
+ ->setDisplay($display);
+ }
+}
diff --git a/library/Businessprocess/Modification/NodeCreateAction.php b/library/Businessprocess/Modification/NodeCreateAction.php
new file mode 100644
index 0000000..167d3bc
--- /dev/null
+++ b/library/Businessprocess/Modification/NodeCreateAction.php
@@ -0,0 +1,129 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Modification;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\Node;
+
+class NodeCreateAction extends NodeAction
+{
+ /** @var string */
+ protected $parentName;
+
+ /** @var array */
+ protected $properties = array();
+
+ /** @var array */
+ protected $preserveProperties = array('parentName', 'properties');
+
+ /**
+ * @param Node $name
+ */
+ public function setParent(Node $name)
+ {
+ $this->parentName = $name->getName();
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasParent()
+ {
+ return $this->parentName !== null;
+ }
+
+ /**
+ * @return string
+ */
+ public function getParentName()
+ {
+ return $this->parentName;
+ }
+
+ /**
+ * @param string $name
+ */
+ public function setParentName($name)
+ {
+ $this->parentName = $name;
+ }
+
+ /**
+ * @return array
+ */
+ public function getProperties()
+ {
+ return $this->properties;
+ }
+
+ /**
+ * @param array $properties
+ * @return $this
+ */
+ public function setProperties($properties)
+ {
+ $this->properties = (array) $properties;
+ return $this;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function appliesTo(BpConfig $config)
+ {
+ $name = $this->getNodeName();
+ if ($config->hasNode($name)) {
+ $this->error('A node with name "%s" already exists', $name);
+ }
+
+ $parent = $this->getParentName();
+ if ($parent !== null && !$config->hasBpNode($parent)) {
+ $this->error('Parent process "%s" missing', $parent);
+ }
+
+ return true;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function applyTo(BpConfig $config)
+ {
+ $name = $this->getNodeName();
+
+ $properties = array(
+ 'name' => $name,
+ 'operator' => $this->properties['operator'],
+ );
+ if (array_key_exists('childNames', $this->properties)) {
+ $properties['child_names'] = $this->properties['childNames'];
+ } else {
+ $properties['child_names'] = array();
+ }
+ $node = new BpNode((object) $properties);
+ $node->setBpConfig($config);
+
+ foreach ($this->getProperties() as $key => $val) {
+ if ($key === 'parentName') {
+ $config->getBpNode($val)->addChild($node);
+ continue;
+ }
+ $func = 'set' . ucfirst($key);
+ $node->$func($val);
+ }
+
+ if ($node->getDisplay() > 1) {
+ $i = $node->getDisplay();
+ foreach ($config->getRootNodes() as $_ => $rootNode) {
+ if ($rootNode->getDisplay() >= $node->getDisplay()) {
+ $rootNode->setDisplay(++$i);
+ }
+ }
+ }
+
+ $config->addNode($name, $node);
+
+ return $node;
+ }
+}
diff --git a/library/Businessprocess/Modification/NodeModifyAction.php b/library/Businessprocess/Modification/NodeModifyAction.php
new file mode 100644
index 0000000..1b33094
--- /dev/null
+++ b/library/Businessprocess/Modification/NodeModifyAction.php
@@ -0,0 +1,121 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Modification;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\Node;
+
+class NodeModifyAction extends NodeAction
+{
+ protected $properties = array();
+
+ protected $formerProperties = array();
+
+ protected $preserveProperties = array('formerProperties', 'properties');
+
+ /**
+ * Set properties for a specific node
+ *
+ * Can be called multiple times
+ *
+ * @param Node $node
+ * @param array $properties
+ *
+ * @return $this
+ */
+ public function setNodeProperties(Node $node, array $properties)
+ {
+ foreach (array_keys($properties) as $key) {
+ $this->properties[$key] = $properties[$key];
+
+ if (array_key_exists($key, $this->formerProperties)) {
+ continue;
+ }
+
+ $func = 'get' . ucfirst($key);
+ $this->formerProperties[$key] = $node->$func();
+ }
+
+ return $this;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function appliesTo(BpConfig $config)
+ {
+ $name = $this->getNodeName();
+
+ if (! $config->hasNode($name)) {
+ $this->error('Node "%s" not found', $name);
+ }
+
+ $node = $config->getNode($name);
+
+ foreach ($this->properties as $key => $val) {
+ $currentVal = $node->{'get' . ucfirst($key)}();
+ if ($this->formerProperties[$key] !== $currentVal) {
+ $this->error(
+ 'Property %s of node "%s" changed its value from "%s" to "%s"',
+ $key,
+ $name,
+ $this->formerProperties[$key],
+ $currentVal
+ );
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function applyTo(BpConfig $config)
+ {
+ $node = $config->getNode($this->getNodeName());
+
+ foreach ($this->properties as $key => $val) {
+ $func = 'set' . ucfirst($key);
+ $node->$func($val);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param $properties
+ * @return $this
+ */
+ public function setProperties($properties)
+ {
+ $this->properties = $properties;
+ return $this;
+ }
+
+ /**
+ * @param $properties
+ * @return $this
+ */
+ public function setFormerProperties($properties)
+ {
+ $this->formerProperties = $properties;
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getProperties()
+ {
+ return $this->properties;
+ }
+
+ /**
+ * @return array
+ */
+ public function getFormerProperties()
+ {
+ return $this->formerProperties;
+ }
+}
diff --git a/library/Businessprocess/Modification/NodeMoveAction.php b/library/Businessprocess/Modification/NodeMoveAction.php
new file mode 100644
index 0000000..4c4305d
--- /dev/null
+++ b/library/Businessprocess/Modification/NodeMoveAction.php
@@ -0,0 +1,227 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Modification;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\Common\Sort;
+
+class NodeMoveAction extends NodeAction
+{
+ use Sort;
+
+ /**
+ * @var string
+ */
+ protected $parent;
+
+ /**
+ * @var string
+ */
+ protected $newParent;
+
+ /**
+ * @var int
+ */
+ protected $from;
+
+ /**
+ * @var int
+ */
+ protected $to;
+
+ protected $preserveProperties = ['parent', 'newParent', 'from', 'to'];
+
+ public function setParent($name)
+ {
+ $this->parent = $name;
+ }
+
+ public function getParent()
+ {
+ return $this->parent;
+ }
+
+ public function setNewParent($name)
+ {
+ $this->newParent = $name;
+ }
+
+ public function getNewParent()
+ {
+ return $this->newParent;
+ }
+
+ public function setFrom($from)
+ {
+ $this->from = (int) $from;
+ }
+
+ public function getFrom()
+ {
+ return $this->from;
+ }
+
+ public function setTo($to)
+ {
+ $this->to = (int) $to;
+ }
+
+ public function getTo()
+ {
+ return $this->to;
+ }
+
+ public function appliesTo(BpConfig $config)
+ {
+ if (! $config->getMetadata()->isManuallyOrdered()) {
+ $this->error('Process configuration is not manually ordered yet');
+ }
+
+ $name = $this->getNodeName();
+ if ($this->parent !== null) {
+ if (! $config->hasBpNode($this->parent)) {
+ $this->error('Parent process "%s" missing', $this->parent);
+ }
+ $parent = $config->getBpNode($this->parent);
+ if (! $parent->hasChild($name)) {
+ $this->error('Node "%s" not found in process "%s"', $name, $this->parent);
+ }
+
+ $nodes = $parent->getChildNames();
+ if (! isset($nodes[$this->from]) || $nodes[$this->from] !== $name) {
+ $reversedNodes = array_reverse($nodes); // The user may have reversed the sort direction
+ if (! isset($reversedNodes[$this->from]) || $reversedNodes[$this->from] !== $name) {
+ $this->error('Node "%s" not found at position %d', $name, $this->from);
+ } else {
+ $this->from = array_search($reversedNodes[$this->from], $nodes, true);
+ $this->to = array_search($reversedNodes[$this->to], $nodes, true);
+ }
+ }
+ } else {
+ if (! $config->hasRootNode($name)) {
+ $this->error('Toplevel process "%s" not found', $name);
+ }
+
+ $nodes = array_keys(self::applyManualSorting($config->getRootNodes()));
+ if (! isset($nodes[$this->from]) || $nodes[$this->from] !== $name) {
+ $reversedNodes = array_reverse($nodes); // The user may have reversed the sort direction
+ if (! isset($reversedNodes[$this->from]) || $reversedNodes[$this->from] !== $name) {
+ $this->error('Toplevel process "%s" not found at position %d', $name, $this->from);
+ } else {
+ $this->from = array_search($reversedNodes[$this->from], $nodes, true);
+ $this->to = array_search($reversedNodes[$this->to], $nodes, true);
+ }
+ }
+ }
+
+ if ($this->parent !== $this->newParent) {
+ if ($this->newParent !== null) {
+ if (! $config->hasBpNode($this->newParent)) {
+ $this->error('New parent process "%s" missing', $this->newParent);
+ } elseif ($config->getBpNode($this->newParent)->hasChild($name)) {
+ $this->error(
+ 'New parent process "%s" already has a node with the name "%s"',
+ $this->newParent,
+ $name
+ );
+ }
+
+ $childrenCount = $config->getBpNode($this->newParent)->countChildren();
+ if ($this->to > 0 && $childrenCount < $this->to) {
+ $this->error(
+ 'New parent process "%s" has not enough children. Target position %d out of range',
+ $this->newParent,
+ $this->to
+ );
+ }
+ } else {
+ if ($config->hasRootNode($name)) {
+ $this->error('Process "%s" is already a toplevel process', $name);
+ }
+
+ $childrenCount = $config->countChildren();
+ if ($this->to > 0 && $childrenCount < $this->to) {
+ $this->error(
+ 'Process configuration has not enough toplevel processes. Target position %d out of range',
+ $this->to
+ );
+ }
+ }
+ }
+
+ return true;
+ }
+
+ public function applyTo(BpConfig $config)
+ {
+ $name = $this->getNodeName();
+ if ($this->parent !== null) {
+ $nodes = $config->getBpNode($this->parent)->getChildren();
+ } else {
+ $nodes = self::applyManualSorting($config->getRootNodes());
+ }
+
+ $node = $nodes[$name];
+ $nodes = array_merge(
+ array_slice($nodes, 0, $this->from, true),
+ array_slice($nodes, $this->from + 1, null, true)
+ );
+ if ($this->parent === $this->newParent) {
+ $nodes = array_merge(
+ array_slice($nodes, 0, $this->to, true),
+ [$name => $node],
+ array_slice($nodes, $this->to, null, true)
+ );
+ } else {
+ if ($this->newParent !== null) {
+ $newNodes = $config->getBpNode($this->newParent)->getChildren();
+ } else {
+ $newNodes = self::applyManualSorting($config->getRootNodes());
+ }
+
+ $newNodes = array_merge(
+ array_slice($newNodes, 0, $this->to, true),
+ [$name => $node],
+ array_slice($newNodes, $this->to, null, true)
+ );
+
+ if ($this->newParent !== null) {
+ $config->getBpNode($this->newParent)->setChildNames(array_keys($newNodes));
+ } else {
+ $config->addRootNode($name);
+
+ $i = 0;
+ foreach ($newNodes as $newName => $newNode) {
+ /** @var BpNode $newNode */
+ if ($newNode->getDisplay() > 0 || $newName === $name) {
+ $i += 1;
+ if ($newNode->getDisplay() !== $i) {
+ $newNode->setDisplay($i);
+ }
+ }
+ }
+ }
+ }
+
+ if ($this->parent !== null) {
+ $config->getBpNode($this->parent)->setChildNames(array_keys($nodes));
+ } else {
+ if ($this->newParent !== null) {
+ $config->removeRootNode($name);
+ $node->setDisplay(0);
+ }
+
+ $i = 0;
+ foreach ($nodes as $_ => $oldNode) {
+ /** @var BpNode $oldNode */
+ if ($oldNode->getDisplay() > 0) {
+ $i += 1;
+ if ($oldNode->getDisplay() !== $i) {
+ $oldNode->setDisplay($i);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/library/Businessprocess/Modification/NodeRemoveAction.php b/library/Businessprocess/Modification/NodeRemoveAction.php
new file mode 100644
index 0000000..6100146
--- /dev/null
+++ b/library/Businessprocess/Modification/NodeRemoveAction.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Modification;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\Node;
+
+/**
+ * NodeRemoveAction
+ *
+ * Tracks removed nodes
+ *
+ * @package Icinga\Module\Businessprocess
+ */
+class NodeRemoveAction extends NodeAction
+{
+ protected $preserveProperties = array('parentName');
+
+ protected $parentName;
+
+ /**
+ * @param $parentName
+ * @return $this
+ */
+ public function setParentName($parentName = null)
+ {
+ $this->parentName = $parentName;
+ return $this;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getParentName()
+ {
+ return $this->parentName;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function appliesTo(BpConfig $config)
+ {
+ $name = $this->getNodeName();
+ $parent = $this->getParentName();
+ if ($parent === null) {
+ if (!$config->hasNode($name)) {
+ $this->error('Toplevel process "%s" not found', $name);
+ }
+ } else {
+ if (! $config->hasNode($parent)) {
+ $this->error('Parent process "%s" missing', $parent);
+ } elseif (! $config->getBpNode($parent)->hasChild($name)) {
+ $this->error('Node "%s" not found in process "%s"', $name, $parent);
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function applyTo(BpConfig $config)
+ {
+ $name = $this->getNodeName();
+ $parentName = $this->getParentName();
+ $node = $config->getNode($name);
+
+ /** @var ?BpNode $parentBpNode */
+ $parentBpNode = $parentName ? $config->getNode($parentName) : null;
+ $this->updateStateOverrides($node, $parentBpNode);
+
+ if ($parentName === null) {
+ if (! $config->hasBpNode($name)) {
+ $config->removeNode($name);
+ } else {
+ $oldDisplay = $config->getBpNode($name)->getDisplay();
+ $config->removeNode($name);
+ if ($config->getMetadata()->isManuallyOrdered()) {
+ foreach ($config->getRootNodes() as $_ => $node) {
+ $nodeDisplay = $node->getDisplay();
+ if ($nodeDisplay > $oldDisplay) {
+ $node->setDisplay($node->getDisplay() - 1);
+ } elseif ($nodeDisplay === $oldDisplay) {
+ break; // Stop immediately to not make things worse ;)
+ }
+ }
+ }
+ }
+ } else {
+ $parent = $config->getBpNode($parentName);
+ $parent->removeChild($name);
+ $node->removeParent($parentName);
+ if (! $node->hasParents()) {
+ $config->removeNode($name);
+ }
+ }
+ }
+
+ /**
+ * Update state overrides
+ *
+ * @param Node $node
+ * @param BpNode|null $nodeParent
+ *
+ * @return void
+ */
+ private function updateStateOverrides(Node $node, ?BpNode $nodeParent): void
+ {
+ $parents = [];
+ if ($nodeParent !== null) {
+ $parents = [$nodeParent];
+ } else {
+ $parents = $node->getParents();
+ }
+
+ foreach ($parents as $parent) {
+ $parentStateOverrides = $parent->getStateOverrides();
+ unset($parentStateOverrides[$node->getName()]);
+ $parent->setStateOverrides($parentStateOverrides);
+ }
+ }
+}
diff --git a/library/Businessprocess/Modification/ProcessChanges.php b/library/Businessprocess/Modification/ProcessChanges.php
new file mode 100644
index 0000000..9257558
--- /dev/null
+++ b/library/Businessprocess/Modification/ProcessChanges.php
@@ -0,0 +1,294 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Modification;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\Node;
+use Icinga\Web\Session\SessionNamespace as Session;
+
+class ProcessChanges
+{
+ /** @var NodeAction[] */
+ protected $changes = array();
+
+ /** @var Session */
+ protected $session;
+
+ /** @var BpConfig */
+ protected $config;
+
+ /** @var bool */
+ protected $hasBeenModified = false;
+
+ /** @var string Session storage key for this processes changes */
+ protected $sessionKey;
+
+ /**
+ * ProcessChanges constructor.
+ *
+ * Direct access is not allowed
+ */
+ private function __construct()
+ {
+ }
+
+ /**
+ * @param BpConfig $bp
+ * @param Session $session
+ *
+ * @return ProcessChanges
+ */
+ public static function construct(BpConfig $bp, Session $session)
+ {
+ $key = 'changes.' . $bp->getName();
+ $changes = new ProcessChanges();
+ $changes->sessionKey = $key;
+
+ if ($actions = $session->get($key)) {
+ foreach ($actions as $string) {
+ $changes->push(NodeAction::unSerialize($string));
+ }
+ }
+ $changes->session = $session;
+ $changes->config = $bp;
+
+ return $changes;
+ }
+
+ /**
+ * @param Node $node
+ * @param $properties
+ *
+ * @return $this
+ */
+ public function modifyNode(Node $node, $properties)
+ {
+ $action = new NodeModifyAction($node);
+ $action->setNodeProperties($node, $properties);
+ return $this->push($action, true);
+ }
+
+ /**
+ * @param Node $node
+ * @param $properties
+ *
+ * @return $this
+ */
+ public function addChildrenToNode($children, Node $node = null)
+ {
+ $action = new NodeAddChildrenAction($node);
+ $action->setChildren($children);
+ return $this->push($action, true);
+ }
+
+ /**
+ * @param Node|string $nodeName
+ * @param array $properties
+ * @param Node $parent
+ *
+ * @return $this
+ */
+ public function createNode($nodeName, $properties, Node $parent = null)
+ {
+ $action = new NodeCreateAction($nodeName);
+ $action->setProperties($properties);
+ if ($parent !== null) {
+ $action->setParent($parent);
+ }
+ return $this->push($action, true);
+ }
+
+ /**
+ * @param $nodeName
+ * @return $this
+ */
+ public function copyNode($nodeName)
+ {
+ $action = new NodeCopyAction($nodeName);
+ return $this->push($action, true);
+ }
+
+ /**
+ * @param Node $node
+ * @param string $parentName
+ * @return $this
+ */
+ public function deleteNode(Node $node, $parentName = null)
+ {
+ $action = new NodeRemoveAction($node);
+ if ($parentName !== null) {
+ $action->setParentName($parentName);
+ }
+
+ return $this->push($action, true);
+ }
+
+ /**
+ * Move the given node
+ *
+ * @param Node $node
+ * @param int $from
+ * @param int $to
+ * @param string $newParent
+ * @param string $parent
+ *
+ * @return $this
+ */
+ public function moveNode(Node $node, $from, $to, $newParent, $parent = null)
+ {
+ $action = new NodeMoveAction($node);
+ $action->setParent($parent);
+ $action->setNewParent($newParent);
+ $action->setFrom($from);
+ $action->setTo($to);
+
+ return $this->push($action, true);
+ }
+
+ /**
+ * Apply manual order on the entire bp configuration file
+ *
+ * @return $this
+ */
+ public function applyManualOrder()
+ {
+ return $this->push(new NodeApplyManualOrderAction(), true);
+ }
+
+ /**
+ * Add a new action to the stack
+ *
+ * @param NodeAction $change
+ * @param bool $apply
+ *
+ * @return $this
+ */
+ public function push(NodeAction $change, $apply = false)
+ {
+ if ($apply && $change->appliesTo($this->config)) {
+ $change->applyTo($this->config);
+ }
+
+ $this->changes[] = $change;
+ $this->hasBeenModified = true;
+ return $this;
+ }
+
+ /**
+ * Get all stacked actions
+ *
+ * @return NodeAction[]
+ */
+ public function getChanges()
+ {
+ return $this->changes;
+ }
+
+ /**
+ * Forget all changes and remove them from the Session
+ *
+ * @return $this
+ */
+ public function clear()
+ {
+ $this->hasBeenModified = true;
+ $this->changes = array();
+ $this->session->set($this->getSessionKey(), null);
+ return $this;
+ }
+
+ /**
+ * Whether there are no stacked changes
+ *
+ * @return bool
+ */
+ public function isEmpty()
+ {
+ return $this->count() === 0;
+ }
+
+ /**
+ * Number of stacked changes
+ *
+ * @return int
+ */
+ public function count()
+ {
+ return count($this->changes);
+ }
+
+ /**
+ * Get the first change on the stack, false if empty
+ *
+ * @return NodeAction|boolean
+ */
+ public function shift()
+ {
+ if ($this->isEmpty()) {
+ return false;
+ }
+
+ $this->hasBeenModified = true;
+ return array_shift($this->changes);
+ }
+
+ /**
+ * Get the last change on the stack, false if empty
+ *
+ * @return NodeAction|boolean
+ */
+ public function pop()
+ {
+ if ($this->isEmpty()) {
+ return false;
+ }
+
+ $this->hasBeenModified = true;
+ return array_pop($this->changes);
+ }
+
+ /**
+ * The identifier used for this processes changes in our Session storage
+ *
+ * @return string
+ */
+ protected function getSessionKey()
+ {
+ return $this->sessionKey;
+ }
+
+ protected function hasBeenModified()
+ {
+ return $this->hasBeenModified;
+ }
+
+ /**
+ * @return array
+ */
+ public function serialize()
+ {
+ $serialized = array();
+ foreach ($this->getChanges() as $change) {
+ $serialized[] = $change->serialize();
+ }
+
+ return $serialized;
+ }
+
+ /**
+ * Persist to session on destruction
+ */
+ public function __destruct()
+ {
+ if (! $this->hasBeenModified()) {
+ unset($this->session);
+ return;
+ }
+ $session = $this->session;
+ $key = $this->getSessionKey();
+ if (! $this->isEmpty()) {
+ $session->set($key, $this->serialize());
+ }
+ unset($this->session);
+ }
+}
diff --git a/library/Businessprocess/MonitoredNode.php b/library/Businessprocess/MonitoredNode.php
new file mode 100644
index 0000000..7047e5d
--- /dev/null
+++ b/library/Businessprocess/MonitoredNode.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Icinga\Module\Businessprocess;
+
+use ipl\Html\Html;
+
+abstract class MonitoredNode extends Node
+{
+ abstract public function getUrl();
+
+ public function getLink()
+ {
+ if ($this->isMissing()) {
+ return Html::tag('a', ['href' => '#'], $this->getAlias() ?? $this->getName());
+ } else {
+ return Html::tag('a', ['href' => $this->getUrl()], $this->getAlias() ?? $this->getName());
+ }
+ }
+}
diff --git a/library/Businessprocess/Monitoring/Backend/Ido/Query/CustomVarJoinTemplateOverride.php b/library/Businessprocess/Monitoring/Backend/Ido/Query/CustomVarJoinTemplateOverride.php
new file mode 100644
index 0000000..385ca59
--- /dev/null
+++ b/library/Businessprocess/Monitoring/Backend/Ido/Query/CustomVarJoinTemplateOverride.php
@@ -0,0 +1,84 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Monitoring\Backend\Ido\Query;
+
+use Icinga\Module\Monitoring\Backend\Ido\Query\ServicecommenthistoryQuery;
+use Icinga\Module\Monitoring\Backend\Ido\Query\ServicecommentQuery;
+use Icinga\Module\Monitoring\Backend\Ido\Query\ServicedowntimeQuery;
+use Icinga\Module\Monitoring\Backend\Ido\Query\ServicedowntimestarthistoryQuery;
+use Icinga\Module\Monitoring\Backend\Ido\Query\ServiceflappingstarthistoryQuery;
+use Icinga\Module\Monitoring\Backend\Ido\Query\ServicegroupQuery;
+use Icinga\Module\Monitoring\Backend\Ido\Query\ServicenotificationQuery;
+use Icinga\Module\Monitoring\Backend\Ido\Query\ServicestatehistoryQuery;
+use Zend_Db_Select;
+
+trait CustomVarJoinTemplateOverride
+{
+ private $customVarsJoinTemplate = '%1$s = %2$s.object_id AND %2$s.varname LIKE %3$s';
+
+ /**
+ * This is a 1:1 copy of {@see IdoQuery::joinCustomvar()} to be able to
+ * adjust {@see IdoQuery::$customVarsJoinTemplate} as it's private
+ */
+ 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 \Icinga\Module\Monitoring\Backend\Ido\Query\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;
+ }
+}
diff --git a/library/Businessprocess/Monitoring/Backend/Ido/Query/HostStatusQuery.php b/library/Businessprocess/Monitoring/Backend/Ido/Query/HostStatusQuery.php
new file mode 100644
index 0000000..e6ea238
--- /dev/null
+++ b/library/Businessprocess/Monitoring/Backend/Ido/Query/HostStatusQuery.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Monitoring\Backend\Ido\Query;
+
+class HostStatusQuery extends \Icinga\Module\Monitoring\Backend\Ido\Query\HoststatusQuery
+{
+ use CustomVarJoinTemplateOverride;
+}
diff --git a/library/Businessprocess/Monitoring/Backend/Ido/Query/ServiceStatusQuery.php b/library/Businessprocess/Monitoring/Backend/Ido/Query/ServiceStatusQuery.php
new file mode 100644
index 0000000..618f3a1
--- /dev/null
+++ b/library/Businessprocess/Monitoring/Backend/Ido/Query/ServiceStatusQuery.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Monitoring\Backend\Ido\Query;
+
+class ServiceStatusQuery extends \Icinga\Module\Monitoring\Backend\Ido\Query\ServicestatusQuery
+{
+ use CustomVarJoinTemplateOverride;
+}
diff --git a/library/Businessprocess/Monitoring/DataView/HostStatus.php b/library/Businessprocess/Monitoring/DataView/HostStatus.php
new file mode 100644
index 0000000..edc1814
--- /dev/null
+++ b/library/Businessprocess/Monitoring/DataView/HostStatus.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Monitoring\DataView;
+
+use Icinga\Data\ConnectionInterface;
+use Icinga\Module\Businessprocess\Monitoring\Backend\Ido\Query\HostStatusQuery;
+
+class HostStatus extends \Icinga\Module\Monitoring\DataView\Hoststatus
+{
+ public function __construct(ConnectionInterface $connection, array $columns = null)
+ {
+ parent::__construct($connection, $columns);
+
+ $this->query = new HostStatusQuery($connection->getResource(), $columns);
+ }
+}
diff --git a/library/Businessprocess/Monitoring/DataView/ServiceStatus.php b/library/Businessprocess/Monitoring/DataView/ServiceStatus.php
new file mode 100644
index 0000000..f3a9c3c
--- /dev/null
+++ b/library/Businessprocess/Monitoring/DataView/ServiceStatus.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Monitoring\DataView;
+
+use Icinga\Data\ConnectionInterface;
+use Icinga\Module\Businessprocess\Monitoring\Backend\Ido\Query\ServiceStatusQuery;
+
+class ServiceStatus extends \Icinga\Module\Monitoring\DataView\Servicestatus
+{
+ public function __construct(ConnectionInterface $connection, array $columns = null)
+ {
+ parent::__construct($connection, $columns);
+
+ $this->query = new ServiceStatusQuery($connection->getResource(), $columns);
+ }
+}
diff --git a/library/Businessprocess/MonitoringRestrictions.php b/library/Businessprocess/MonitoringRestrictions.php
new file mode 100644
index 0000000..c7d2cef
--- /dev/null
+++ b/library/Businessprocess/MonitoringRestrictions.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Icinga\Module\Businessprocess;
+
+use Icinga\Authentication\Auth;
+use Icinga\Data\Filter\Filter;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\QueryException;
+
+class MonitoringRestrictions
+{
+ /**
+ * Return a filter for the given restriction
+ *
+ * @param string $name Name of the restriction
+ *
+ * @return Filter|null Filter object or null if the authenticated user is not restricted
+ * @throws ConfigurationError If the restriction contains invalid filter columns
+ */
+ public static function getRestriction($name)
+ {
+ // Borrowed from Icinga\Module\Monitoring\Controller
+ $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 (Auth::getInstance()->getRestrictions($name) as $filter) {
+ if ($filter === '*') {
+ return Filter::matchAny();
+ }
+
+ try {
+ $restriction->addFilter(Filter::fromQueryString($filter));
+ } catch (QueryException $e) {
+ throw new ConfigurationError(
+ mt(
+ 'monitoring',
+ '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
+ );
+ }
+ }
+
+ return $restriction;
+ }
+}
diff --git a/library/Businessprocess/Node.php b/library/Businessprocess/Node.php
new file mode 100644
index 0000000..a0c07d2
--- /dev/null
+++ b/library/Businessprocess/Node.php
@@ -0,0 +1,570 @@
+<?php
+
+namespace Icinga\Module\Businessprocess;
+
+use Icinga\Exception\ProgrammingError;
+use ipl\Html\Html;
+use ipl\Web\Widget\Icon;
+
+abstract class Node
+{
+ const FLAG_DOWNTIME = 1;
+ const FLAG_ACK = 2;
+ const FLAG_MISSING = 4;
+ const FLAG_NONE = 8;
+ const SHIFT_FLAGS = 4;
+
+ const ICINGA_OK = 0;
+ const ICINGA_WARNING = 1;
+ const ICINGA_CRITICAL = 2;
+ const ICINGA_UNKNOWN = 3;
+ const ICINGA_UP = 0;
+ const ICINGA_DOWN = 1;
+ const ICINGA_UNREACHABLE = 2;
+ const ICINGA_PENDING = 99;
+ const NODE_EMPTY = 128;
+
+ /** @var bool Whether to treat acknowledged hosts/services always as UP/OK */
+ protected static $ackIsOk = false;
+
+ /** @var bool Whether to treat hosts/services in downtime always as UP/OK */
+ protected static $downtimeIsOk = false;
+
+ protected $sortStateToStateMap = array(
+ 4 => self::ICINGA_CRITICAL,
+ 3 => self::ICINGA_UNKNOWN,
+ 2 => self::ICINGA_WARNING,
+ 1 => self::ICINGA_PENDING,
+ 0 => self::ICINGA_OK
+ );
+
+ protected $stateToSortStateMap = array(
+ self::ICINGA_PENDING => 1,
+ self::ICINGA_UNKNOWN => 3,
+ self::ICINGA_CRITICAL => 4,
+ self::ICINGA_WARNING => 2,
+ self::ICINGA_OK => 0,
+ self::NODE_EMPTY => 0
+ );
+
+ /** @var ?string Alias of the node */
+ protected $alias;
+
+ /**
+ * Main business process object
+ *
+ * @var BpConfig
+ */
+ protected $bp;
+
+ /**
+ * Parent nodes
+ *
+ * @var array
+ */
+ protected $parents = array();
+
+ /**
+ * Node identifier
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * Node state
+ *
+ * @var ?int
+ */
+ protected $state;
+
+ /**
+ * Whether this nodes state has been acknowledged
+ *
+ * @var bool
+ */
+ protected $ack;
+
+ /**
+ * Whether this node is in a scheduled downtime
+ *
+ * @var bool
+ */
+ protected $downtime;
+
+ // obsolete
+ protected $duration;
+
+ /**
+ * This node's icon
+ *
+ * @var ?string
+ */
+ protected $icon;
+
+ /**
+ * Last state change, unix timestamp
+ *
+ * @var int
+ */
+ protected $lastStateChange;
+
+ protected $missing = false;
+
+ protected $empty = false;
+
+ protected $className = 'unknown';
+
+ protected $stateNames = array(
+ 'OK',
+ 'WARNING',
+ 'CRITICAL',
+ 'UNKNOWN',
+ 99 => 'PENDING',
+ 128 => 'EMPTY'
+ );
+
+ /**
+ * Set whether to treat acknowledged hosts/services always as UP/OK
+ *
+ * @param bool $ackIsOk
+ */
+ public static function setAckIsOk($ackIsOk = true)
+ {
+ self::$ackIsOk = $ackIsOk;
+ }
+
+ /**
+ * Set whether to treat hosts/services in downtime always as UP/OK
+ *
+ * @param bool $downtimeIsOk
+ */
+ public static function setDowntimeIsOk($downtimeIsOk = true)
+ {
+ self::$downtimeIsOk = $downtimeIsOk;
+ }
+
+ public function setBpConfig(BpConfig $bp)
+ {
+ $this->bp = $bp;
+ return $this;
+ }
+
+ public function getBpConfig()
+ {
+ return $this->bp;
+ }
+
+ public function setMissing($missing = true)
+ {
+ $this->missing = $missing;
+ return $this;
+ }
+
+ public function isProblem()
+ {
+ return $this->getState() > 0;
+ }
+
+ public function hasBeenChanged()
+ {
+ return false;
+ }
+
+ public function isMissing()
+ {
+ return $this->missing;
+ }
+
+ public function hasMissingChildren()
+ {
+ return count($this->getMissingChildren()) > 0;
+ }
+
+ public function getMissingChildren()
+ {
+ return array();
+ }
+
+ public function hasInfoUrl()
+ {
+ return false;
+ }
+
+ public function setState($state)
+ {
+ $this->state = (int) $state;
+ $this->missing = false;
+ return $this;
+ }
+
+ /**
+ * Forget my state
+ *
+ * @return $this
+ */
+ public function clearState()
+ {
+ $this->state = null;
+ return $this;
+ }
+
+ public function setAck($ack = true)
+ {
+ $this->ack = $ack;
+ return $this;
+ }
+
+ public function setDowntime($downtime = true)
+ {
+ $this->downtime = $downtime;
+ return $this;
+ }
+
+ public function getStateName($state = null)
+ {
+ $states = $this->enumStateNames();
+ if ($state === null) {
+ return $states[ $this->getState() ];
+ } else {
+ return $states[ $state ];
+ }
+ }
+
+ public function enumStateNames()
+ {
+ return $this->stateNames;
+ }
+
+ public function getState()
+ {
+ if ($this->state === null) {
+ throw new ProgrammingError(
+ sprintf(
+ 'Node %s is unable to retrieve it\'s state',
+ $this->name
+ )
+ );
+ }
+
+ return $this->state;
+ }
+
+ public function getSortingState($state = null)
+ {
+ if ($state === null) {
+ $state = $this->getState();
+ }
+
+ if (self::$ackIsOk && $this->isAcknowledged()) {
+ $state = self::ICINGA_OK;
+ }
+
+ if (self::$downtimeIsOk && $this->isInDowntime()) {
+ $state = self::ICINGA_OK;
+ }
+
+ $sort = $this->stateToSortState($state);
+ $sort = ($sort << self::SHIFT_FLAGS)
+ + ($this->isInDowntime() ? self::FLAG_DOWNTIME : 0)
+ + ($this->isAcknowledged() ? self::FLAG_ACK : 0);
+ if (! ($sort & (self::FLAG_DOWNTIME | self::FLAG_ACK))) {
+ $sort |= self::FLAG_NONE;
+ }
+
+ return $sort;
+ }
+
+ public function getLastStateChange()
+ {
+ return $this->lastStateChange;
+ }
+
+ public function setLastStateChange($timestamp)
+ {
+ $this->lastStateChange = $timestamp;
+ return $this;
+ }
+
+ public function addParent(Node $parent)
+ {
+ $this->parents[] = $parent;
+ return $this;
+ }
+
+ public function getDuration()
+ {
+ return $this->duration;
+ }
+
+ public function isHandled()
+ {
+ return $this->isInDowntime() || $this->isAcknowledged();
+ }
+
+ public function isInDowntime()
+ {
+ if ($this->downtime === null) {
+ $this->getState();
+ }
+ return $this->downtime;
+ }
+
+ public function isAcknowledged()
+ {
+ if ($this->ack === null) {
+ $this->getState();
+ }
+ return $this->ack;
+ }
+
+ public function getChildren($filter = null)
+ {
+ return array();
+ }
+
+ public function countChildren($filter = null)
+ {
+ return count($this->getChildren($filter));
+ }
+
+ public function hasChildren($filter = null)
+ {
+ return $this->countChildren($filter) > 0;
+ }
+
+ public function isEmpty()
+ {
+ return $this->countChildren() === 0;
+ }
+
+ public function hasAlias()
+ {
+ return $this->alias !== null;
+ }
+
+ /**
+ * Get the alias of the node
+ *
+ * @return ?string
+ */
+ public function getAlias()
+ {
+ return $this->alias;
+ }
+
+ /**
+ * Set the alias of the node
+ *
+ * @param string $alias
+ *
+ * @return $this
+ */
+ public function setAlias($alias)
+ {
+ $this->alias = $alias;
+
+ return $this;
+ }
+
+ public function hasParents()
+ {
+ return count($this->parents) > 0;
+ }
+
+ public function hasParentName($name)
+ {
+ foreach ($this->getParents() as $parent) {
+ if ($parent->getName() === $name) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public function removeParent($name)
+ {
+ $this->parents = array_filter(
+ $this->parents,
+ function (BpNode $parent) use ($name) {
+ return $parent->getName() !== $name;
+ }
+ );
+
+ return $this;
+ }
+
+ /**
+ * @return BpNode[]
+ */
+ public function getParents()
+ {
+ return $this->parents;
+ }
+
+ /**
+ * @param BpConfig $rootConfig
+ *
+ * @return array
+ */
+ public function getPaths($rootConfig = null)
+ {
+ $differentConfig = false;
+ if ($rootConfig === null) {
+ $rootConfig = $this->getBpConfig();
+ } else {
+ $differentConfig = $this->getBpConfig()->getName() !== $rootConfig->getName();
+ }
+
+ $paths = [];
+ foreach ($this->parents as $parent) {
+ foreach ($parent->getPaths($rootConfig) as $path) {
+ $path[] = $differentConfig ? $this->getIdentifier() : $this->getName();
+ $paths[] = $path;
+ }
+ }
+
+ if (! $this instanceof ImportedNode && $this->getBpConfig()->hasRootNode($this->getName())) {
+ $paths[] = [$differentConfig ? $this->getIdentifier() : $this->getName()];
+ } elseif (! $this->hasParents()) {
+ $paths[] = ['__unbound__', $differentConfig ? $this->getIdentifier() : $this->getName()];
+ }
+
+ return $paths;
+ }
+
+ protected function stateToSortState($state)
+ {
+ if (array_key_exists($state, $this->stateToSortStateMap)) {
+ return $this->stateToSortStateMap[$state];
+ }
+
+ throw new ProgrammingError(
+ 'Got invalid state for node %s: %s',
+ $this->getName(),
+ var_export($state, true) . var_export($this->stateToSortStateMap, true)
+ );
+ }
+
+ protected function sortStateTostate($sortState)
+ {
+ $sortState = $sortState >> self::SHIFT_FLAGS;
+ if (array_key_exists($sortState, $this->sortStateToStateMap)) {
+ return $this->sortStateToStateMap[$sortState];
+ }
+
+ throw new ProgrammingError('Got invalid sorting state %s', $sortState);
+ }
+
+ public function getObjectClassName()
+ {
+ return $this->className;
+ }
+
+ public function getLink()
+ {
+ return Html::tag('a', ['href' => '#', 'class' => 'toggle'], new Icon('caret-down'));
+ }
+
+ public function getIcon(): Icon
+ {
+ return new Icon($this->icon ?? 'circle-exclamation');
+ }
+
+ public function operatorHtml()
+ {
+ return '&nbsp;';
+ }
+
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Get the Node operators
+ *
+ * @return array
+ */
+ public static function getOperators(): array
+ {
+ return [
+ '&' => t('AND'),
+ '|' => t('OR'),
+ '^' => t('XOR'),
+ '!' => t('NOT'),
+ '%' => t('DEGRADED'),
+ '1' => t('MIN 1'),
+ '2' => t('MIN 2'),
+ '3' => t('MIN 3'),
+ '4' => t('MIN 4'),
+ '5' => t('MIN 5'),
+ '6' => t('MIN 6'),
+ '7' => t('MIN 7'),
+ '8' => t('MIN 8'),
+ '9' => t('MIN 9'),
+ ];
+ }
+
+ public function getIdentifier()
+ {
+ return '@' . $this->getBpConfig()->getName() . ':' . $this->getName();
+ }
+
+ public function __toString()
+ {
+ return $this->getName();
+ }
+
+ public function __destruct()
+ {
+ unset($this->parents);
+ }
+
+ /**
+ * Export the node to array
+ *
+ * @param array $parent The node's parent. Used to construct the path to the node
+ * @param bool $flat If false, children will be added to the array key children, else the array will be flat
+ *
+ * @return array
+ */
+ public function toArray(array $parent = null, $flat = false)
+ {
+ $data = [
+ 'name' => $this->getAlias(),
+ 'state' => $this->getStateName(),
+ 'since' => $this->getLastStateChange(),
+ 'in_downtime' => $this->isInDowntime() ? true : false
+ ];
+
+ if ($parent !== null) {
+ $data['path'] = $parent['path'] . '!' . $this->getAlias();
+ } else {
+ $data['path'] = $this->getAlias();
+ }
+
+ $children = [];
+
+ foreach ($this->getChildren() as $node) {
+ if ($flat) {
+ $children = array_merge($children, $node->toArray($data, $flat));
+ } else {
+ $children[] = $node->toArray($data, $flat);
+ }
+ }
+
+ if ($flat) {
+ $data = [$data];
+
+ if (! empty($children)) {
+ $data = array_merge($data, $children);
+ }
+ } else {
+ $data['children'] = $children;
+ }
+
+ return $data;
+ }
+}
diff --git a/library/Businessprocess/ProvidedHook/Icingadb/HostActions.php b/library/Businessprocess/ProvidedHook/Icingadb/HostActions.php
new file mode 100644
index 0000000..ac18959
--- /dev/null
+++ b/library/Businessprocess/ProvidedHook/Icingadb/HostActions.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\ProvidedHook\Icingadb;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Icingadb\Hook\HostActionsHook;
+use Icinga\Module\Icingadb\Model\Host;
+use ipl\Web\Widget\Link;
+
+class HostActions extends HostActionsHook
+{
+ public function getActionsForObject(Host $host): array
+ {
+ $label = mt('businessprocess', 'Business Impact');
+ return array(
+ new Link(
+ $label,
+ 'businessprocess/node/impact?name='
+ . rawurlencode(BpConfig::joinNodeName($host->name, 'Hoststatus'))
+ )
+ );
+ }
+}
diff --git a/library/Businessprocess/ProvidedHook/Icingadb/IcingadbSupport.php b/library/Businessprocess/ProvidedHook/Icingadb/IcingadbSupport.php
new file mode 100644
index 0000000..1ff37d3
--- /dev/null
+++ b/library/Businessprocess/ProvidedHook/Icingadb/IcingadbSupport.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\ProvidedHook\Icingadb;
+
+use Icinga\Module\Icingadb\Hook\IcingadbSupportHook;
+
+class IcingadbSupport extends IcingadbSupportHook
+{
+
+}
diff --git a/library/Businessprocess/ProvidedHook/Icingadb/ServiceActions.php b/library/Businessprocess/ProvidedHook/Icingadb/ServiceActions.php
new file mode 100644
index 0000000..d416d90
--- /dev/null
+++ b/library/Businessprocess/ProvidedHook/Icingadb/ServiceActions.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\ProvidedHook\Icingadb;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Icingadb\Hook\ServiceActionsHook;
+use Icinga\Module\Icingadb\Model\Service;
+use ipl\Web\Widget\Link;
+
+class ServiceActions extends ServiceActionsHook
+{
+ public function getActionsForObject(Service $service): array
+ {
+ $label = mt('businessprocess', 'Business Impact');
+ return array(
+ new Link(
+ $label,
+ sprintf(
+ 'businessprocess/node/impact?name=%s',
+ rawurlencode(BpConfig::joinNodeName($service->host->name, $service->name))
+ )
+ )
+ );
+ }
+}
diff --git a/library/Businessprocess/ProvidedHook/Icingadb/ServiceDetailExtension.php b/library/Businessprocess/ProvidedHook/Icingadb/ServiceDetailExtension.php
new file mode 100644
index 0000000..6d10af2
--- /dev/null
+++ b/library/Businessprocess/ProvidedHook/Icingadb/ServiceDetailExtension.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\ProvidedHook\Icingadb;
+
+use Icinga\Module\Businessprocess\Renderer\TileRenderer;
+use Icinga\Module\Businessprocess\Renderer\TreeRenderer;
+use Icinga\Module\Businessprocess\State\IcingaDbState;
+use Icinga\Module\Businessprocess\Storage\LegacyStorage;
+use Icinga\Module\Businessprocess\Web\Url;
+use Icinga\Module\Icingadb\Hook\ServiceDetailExtensionHook;
+use Icinga\Module\Icingadb\Model\Service;
+use ipl\Html\Html;
+use ipl\Html\HtmlDocument;
+use ipl\Html\HtmlString;
+use ipl\Html\ValidHtml;
+
+class ServiceDetailExtension extends ServiceDetailExtensionHook
+{
+ /** @var ?LegacyStorage */
+ private $storage;
+
+ /** @var string */
+ private $commandName;
+
+ protected function init()
+ {
+ $this->setSection(self::GRAPH_SECTION);
+
+ try {
+ $this->storage = LegacyStorage::getInstance();
+ $this->commandName = $this->getModule()->getConfig()->get(
+ 'DetailviewExtension',
+ 'checkcommand_name',
+ 'icingacli-businessprocess'
+ );
+ } catch (\Exception $e) {
+ // Ignore and don't display anything
+ }
+ }
+
+ public function getHtmlForObject(Service $service): ValidHtml
+ {
+ if (! isset($this->storage)
+ || $service->checkcommand_name !== $this->commandName
+ ) {
+ return HtmlString::create('');
+ }
+
+ $bpName = $service->customvars['icingacli_businessprocess_config'] ?? null;
+ if (! $bpName) {
+ $bpName = key($this->storage->listProcessNames());
+ }
+
+ $nodeName = $service->customvars['icingacli_businessprocess_process'] ?? null;
+ if (! $nodeName) {
+ return HtmlString::create('');
+ }
+
+ $bp = $this->storage->loadProcess($bpName);
+ $node = $bp->getBpNode($nodeName);
+
+ IcingaDbState::apply($bp);
+
+ if ($service->customvars['icingaweb_businessprocess_as_tree'] ?? false) {
+ $renderer = new TreeRenderer($bp, $node);
+ $tag = 'ul';
+ } else {
+ $renderer = new TileRenderer($bp, $node);
+ $tag = 'div';
+ }
+
+ $renderer->setUrl(Url::fromPath('businessprocess/process/show?config=' . $bpName . '&node=' . $nodeName));
+ $renderer->ensureAssembled()->getFirst($tag)->setAttribute('data-base-target', '_next');
+
+ return (new HtmlDocument())->addHtml(Html::tag('h2', 'Business Process'), $renderer);
+ }
+}
diff --git a/library/Businessprocess/ProvidedHook/Monitoring/DetailviewExtension.php b/library/Businessprocess/ProvidedHook/Monitoring/DetailviewExtension.php
new file mode 100644
index 0000000..691acec
--- /dev/null
+++ b/library/Businessprocess/ProvidedHook/Monitoring/DetailviewExtension.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\ProvidedHook\Monitoring;
+
+use Icinga\Module\Businessprocess\Renderer\TileRenderer;
+use Icinga\Module\Businessprocess\Renderer\TreeRenderer;
+use Icinga\Module\Businessprocess\State\MonitoringState;
+use Icinga\Module\Businessprocess\Storage\LegacyStorage;
+use Icinga\Module\Businessprocess\Web\Url;
+use Icinga\Module\Monitoring\Hook\DetailviewExtensionHook;
+use Icinga\Module\Monitoring\Object\MonitoredObject;
+use Icinga\Module\Monitoring\Object\Service;
+
+class DetailviewExtension extends DetailviewExtensionHook
+{
+ /** @var ?LegacyStorage */
+ private $storage;
+
+ /** @var string */
+ private $commandName;
+
+ /**
+ * Initialize storage
+ */
+ public function init()
+ {
+ try {
+ $this->storage = LegacyStorage::getInstance();
+ $this->commandName = $this->getModule()->getConfig()->get(
+ 'DetailviewExtension',
+ 'checkcommand_name',
+ 'icingacli-businessprocess'
+ );
+ } catch (\Exception $e) {
+ // Ignore and don't display anything
+ }
+ }
+
+ /**
+ * Returns the rendered Tree-/TileRenderer HTML
+ *
+ * @param MonitoredObject $object
+ *
+ * @return string
+ */
+ public function getHtmlForObject(MonitoredObject $object)
+ {
+ if (! isset($this->storage)
+ || ! $object instanceof Service
+ || $object->check_command !== $this->commandName
+ ) {
+ return '';
+ }
+
+ $bpName = $object->_service_icingacli_businessprocess_config;
+ if (! $bpName) {
+ $bpName = key($this->storage->listProcessNames());
+ }
+
+ $nodeName = $object->_service_icingacli_businessprocess_process;
+ if (! $nodeName) {
+ return '';
+ }
+
+ $bp = $this->storage->loadProcess($bpName);
+ $node = $bp->getBpNode($nodeName);
+
+ MonitoringState::apply($bp);
+
+ if (filter_var($object->_service_icingaweb_businessprocess_as_tree, FILTER_VALIDATE_BOOLEAN)) {
+ $renderer = new TreeRenderer($bp, $node);
+ $tag = 'ul';
+ } else {
+ $renderer = new TileRenderer($bp, $node);
+ $tag = 'div';
+ }
+
+ $renderer->setUrl(Url::fromPath('businessprocess/process/show?config=' . $bpName . '&node=' . $nodeName));
+ $renderer->ensureAssembled()->getFirst($tag)->setAttribute('data-base-target', '_next');
+
+ return '<h2>Business Process</h2>' . $renderer;
+ }
+}
diff --git a/library/Businessprocess/ProvidedHook/Monitoring/HostActions.php b/library/Businessprocess/ProvidedHook/Monitoring/HostActions.php
new file mode 100644
index 0000000..e2b9c59
--- /dev/null
+++ b/library/Businessprocess/ProvidedHook/Monitoring/HostActions.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\ProvidedHook\Monitoring;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Monitoring\Hook\HostActionsHook;
+use Icinga\Module\Monitoring\Object\Host;
+
+class HostActions extends HostActionsHook
+{
+ public function getActionsForHost(Host $host)
+ {
+ $label = mt('businessprocess', 'Business Impact');
+ return array(
+ $label => 'businessprocess/node/impact?name='
+ . rawurlencode(BpConfig::joinNodeName($host->getName(), 'Hoststatus'))
+ );
+ }
+}
diff --git a/library/Businessprocess/ProvidedHook/Monitoring/ServiceActions.php b/library/Businessprocess/ProvidedHook/Monitoring/ServiceActions.php
new file mode 100644
index 0000000..ce9fabf
--- /dev/null
+++ b/library/Businessprocess/ProvidedHook/Monitoring/ServiceActions.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\ProvidedHook\Monitoring;
+
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Monitoring\Hook\ServiceActionsHook;
+use Icinga\Module\Monitoring\Object\Service;
+use Icinga\Web\Url;
+
+class ServiceActions extends ServiceActionsHook
+{
+ public function getActionsForService(Service $service)
+ {
+ $label = mt('businessprocess', 'Business Impact');
+ return array(
+ $label => sprintf(
+ 'businessprocess/node/impact?name=%s',
+ rawurlencode(BpConfig::joinNodeName($service->getHost()->getName(), $service->getName()))
+ )
+ );
+ }
+}
diff --git a/library/Businessprocess/Renderer/Breadcrumb.php b/library/Businessprocess/Renderer/Breadcrumb.php
new file mode 100644
index 0000000..4272b76
--- /dev/null
+++ b/library/Businessprocess/Renderer/Breadcrumb.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Renderer;
+
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\Renderer\TileRenderer\NodeTile;
+use Icinga\Module\Businessprocess\Web\Url;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use ipl\Web\Widget\Icon;
+
+class Breadcrumb extends BaseHtmlElement
+{
+ protected $tag = 'ul';
+
+ protected $defaultAttributes = array(
+ 'class' => 'breadcrumb',
+ 'data-base-target' => '_main'
+ );
+
+ /**
+ * @param Renderer $renderer
+ * @return static
+ */
+ public static function create(Renderer $renderer)
+ {
+ $bp = $renderer->getBusinessProcess();
+ $breadcrumb = new static;
+ $bpUrl = $renderer->getBaseUrl();
+ if ($bpUrl->getParam('action') === 'delete') {
+ $bpUrl->remove('action');
+ }
+
+ $breadcrumb->add(Html::tag('li')->add(
+ Html::tag(
+ 'a',
+ [
+ 'href' => Url::fromPath('businessprocess'),
+ 'title' => mt('businessprocess', 'Show Overview')
+ ],
+ new Icon('house')
+ )
+ ));
+ $breadcrumb->add(Html::tag('li')->add(
+ Html::tag('a', ['href' => $bpUrl], $bp->getTitle())
+ ));
+ $path = $renderer->getCurrentPath();
+
+ $parts = array();
+ while ($nodeName = array_pop($path)) {
+ /** @var BpNode $node */
+ $node = $bp->getNode($nodeName);
+ $renderer->setParentNode($node);
+ array_unshift(
+ $parts,
+ static::renderNode($node, $path, $renderer)
+ );
+ }
+ $breadcrumb->add($parts);
+
+ return $breadcrumb;
+ }
+
+ /**
+ * @param BpNode $node
+ * @param array $path
+ * @param Renderer $renderer
+ *
+ * @return NodeTile
+ */
+ protected static function renderNode(BpNode $node, $path, Renderer $renderer)
+ {
+ // TODO: something more generic than NodeTile?
+ $renderer = clone($renderer);
+ $renderer->lock()->setIsBreadcrumb();
+ $p = new NodeTile($renderer, $node, $path);
+ $p->setTag('li');
+ return $p;
+ }
+}
diff --git a/library/Businessprocess/Renderer/Renderer.php b/library/Businessprocess/Renderer/Renderer.php
new file mode 100644
index 0000000..6a5d624
--- /dev/null
+++ b/library/Businessprocess/Renderer/Renderer.php
@@ -0,0 +1,431 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Renderer;
+
+use Icinga\Exception\ProgrammingError;
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\Common\Sort;
+use Icinga\Module\Businessprocess\ImportedNode;
+use Icinga\Module\Businessprocess\MonitoredNode;
+use Icinga\Module\Businessprocess\Node;
+use Icinga\Module\Businessprocess\Web\Url;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use ipl\Html\HtmlDocument;
+use ipl\Stdlib\Str;
+use ipl\Web\Widget\StateBadge;
+
+abstract class Renderer extends HtmlDocument
+{
+ use Sort;
+
+ /** @var BpConfig */
+ protected $config;
+
+ /** @var BpNode */
+ protected $parent;
+
+ /** @var bool Administrative actions are hidden unless unlocked */
+ protected $locked = true;
+
+ /** @var Url */
+ protected $url;
+
+ /** @var Url */
+ protected $baseUrl;
+
+ /** @var array */
+ protected $path = array();
+
+ /** @var bool */
+ protected $isBreadcrumb = false;
+
+ /**
+ * Renderer constructor.
+ *
+ * @param BpConfig $config
+ * @param BpNode|null $parent
+ */
+ public function __construct(BpConfig $config, BpNode $parent = null)
+ {
+ $this->config = $config;
+ $this->parent = $parent;
+ }
+
+ /**
+ * @return BpConfig
+ */
+ public function getBusinessProcess()
+ {
+ return $this->config;
+ }
+
+ /**
+ * Whether this will render all root nodes
+ *
+ * @return bool
+ */
+ public function wantsRootNodes()
+ {
+ return $this->parent === null;
+ }
+
+ /**
+ * Whether this will only render parts of given config
+ *
+ * @return bool
+ */
+ public function rendersSubNode()
+ {
+ return $this->parent !== null;
+ }
+
+ public function rendersImportedNode()
+ {
+ return $this->parent !== null && $this->parent->getBpConfig()->getName() !== $this->config->getName();
+ }
+
+ public function setParentNode(BpNode $node)
+ {
+ $this->parent = $node;
+ return $this;
+ }
+
+ /**
+ * @return BpNode
+ */
+ public function getParentNode()
+ {
+ return $this->parent;
+ }
+
+ /**
+ * @return BpNode[]
+ */
+ public function getParentNodes()
+ {
+ if ($this->wantsRootNodes()) {
+ return array();
+ }
+
+ return $this->parent->getParents();
+ }
+
+ /**
+ * @return BpNode[]
+ */
+ public function getChildNodes()
+ {
+ if ($this->wantsRootNodes()) {
+ return $this->config->getRootNodes();
+ } else {
+ return $this->parent->getChildren();
+ }
+ }
+
+ /**
+ * Get the default sort specification
+ *
+ * @return string
+ */
+ public function getDefaultSort(): string
+ {
+ if ($this->config->getMetadata()->isManuallyOrdered()) {
+ return 'manual asc';
+ }
+
+ return 'display_name asc';
+ }
+
+ /**
+ * Get whether a custom sort order is applied
+ *
+ * @return bool
+ */
+ public function appliesCustomSorting(): bool
+ {
+ if (empty($this->getSort())) {
+ return false;
+ }
+
+ list($sortBy, $_) = Str::symmetricSplit($this->getSort(), ' ', 2);
+ list($defaultSortBy, $_) = Str::symmetricSplit($this->getDefaultSort(), ' ', 2);
+
+ return $sortBy !== $defaultSortBy;
+ }
+
+ /**
+ * @return int
+ */
+ public function countChildNodes()
+ {
+ if ($this->wantsRootNodes()) {
+ return $this->config->countChildren();
+ } else {
+ return $this->parent->countChildren();
+ }
+ }
+
+ /**
+ * @param $summary
+ * @return ?BaseHtmlElement
+ */
+ public function renderStateBadges($summary, $totalChildren)
+ {
+ $itemCount = Html::tag(
+ 'span',
+ [
+ 'class' => [
+ 'item-count',
+ ]
+ ],
+ sprintf(mtp('businessprocess', '%u Child', '%u Children', $totalChildren), $totalChildren)
+ );
+
+ $elements = array_filter([
+ $this->createBadgeGroup($summary, 'CRITICAL'),
+ $this->createBadgeGroup($summary, 'UNKNOWN'),
+ $this->createBadgeGroup($summary, 'WARNING'),
+ $this->createBadge($summary, 'MISSING'),
+ $this->createBadge($summary, 'PENDING')
+ ]);
+
+ if (!empty($elements)) {
+ $container = Html::tag('ul', ['class' => 'state-badges']);
+ $container->add($itemCount);
+ foreach ($elements as $element) {
+ $container->add($element);
+ }
+
+ return $container;
+ }
+ return null;
+ }
+
+ protected function createBadge($summary, $state)
+ {
+ if ($summary[$state] !== 0) {
+ return Html::tag('li', new StateBadge($summary[$state], strtolower($state)));
+ }
+
+ return null;
+ }
+
+ protected function createBadgeGroup($summary, $state)
+ {
+ $content = [];
+ if ($summary[$state] !== 0) {
+ $content[] = Html::tag('li', new StateBadge($summary[$state], strtolower($state)));
+ }
+
+ if ($summary[$state . '-HANDLED'] !== 0) {
+ $content[] = Html::tag('li', new StateBadge($summary[$state . '-HANDLED'], strtolower($state), true));
+ }
+
+ if (empty($content)) {
+ return null;
+ }
+
+ return Html::tag('li', Html::tag('ul', $content));
+ }
+
+ public function getNodeClasses(Node $node)
+ {
+ if ($node->isMissing()) {
+ $classes = array('missing');
+ } else {
+ if ($node->isEmpty() && ! $node instanceof MonitoredNode) {
+ $classes = array('empty');
+ } else {
+ $classes = [strtolower($node->getStateName(
+ $this->parent !== null ? $this->parent->getChildState($node) : null
+ ))];
+ }
+ if ($node->hasMissingChildren()) {
+ $classes[] = 'missing-children';
+ }
+ }
+
+ if ($node->isHandled()) {
+ $classes[] = 'handled';
+ }
+
+ if ($node instanceof BpNode) {
+ $classes[] = 'process-node';
+ } else {
+ $classes[] = 'monitored-node';
+ }
+ // TODO: problem?
+ return $classes;
+ }
+
+ /**
+ * Return the url to the given node's source configuration
+ *
+ * @param BpNode $node
+ *
+ * @return Url
+ */
+ public function getSourceUrl(BpNode $node)
+ {
+ if ($node instanceof ImportedNode) {
+ $name = $node->getNodeName();
+ $paths = $node->getBpConfig()->getBpNode($name)->getPaths();
+ } else {
+ $name = $node->getName();
+ $paths = $node->getPaths();
+ }
+
+ $url = clone $this->getUrl();
+ $url->setParams([
+ 'config' => $node->getBpConfig()->getName(),
+ 'node' => $name
+ ]);
+ // This depends on the fact that the node's root path is the last element in $paths
+ $url->getParams()->addValues('path', array_slice(array_pop($paths), 0, -1));
+ if (! $this->isLocked()) {
+ $url->getParams()->add('unlocked', true);
+ }
+
+ return $url;
+ }
+
+ /**
+ * @param Node $node
+ * @param $path
+ * @return string
+ */
+ public function getId(Node $node, $path)
+ {
+ return 'businessprocess-' . md5((empty($path) ? '' : implode(';', $path)) . $node->getName());
+ }
+
+ public function setPath(array $path)
+ {
+ $this->path = $path;
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getPath()
+ {
+ return $this->path;
+ }
+
+ public function getCurrentPath()
+ {
+ $path = $this->getPath();
+ if ($this->rendersSubNode()) {
+ $path[] = $this->rendersImportedNode()
+ ? $this->parent->getIdentifier()
+ : $this->parent->getName();
+ }
+
+ return $path;
+ }
+
+ /**
+ * @param Url $url
+ * @return $this
+ */
+ public function setUrl(Url $url)
+ {
+ $this->url = $url->without(array(
+ 'action',
+ 'deletenode',
+ 'deleteparent',
+ 'editnode',
+ 'simulationnode',
+ 'view'
+ ));
+ $this->setBaseUrl($this->url);
+ return $this;
+ }
+
+ /**
+ * @param Url $url
+ * @return $this
+ */
+ protected function setBaseUrl(Url $url)
+ {
+ $this->baseUrl = $url->without(array('node', 'path'));
+ return $this;
+ }
+
+ public function getUrl()
+ {
+ return $this->url;
+ }
+
+ /**
+ * @return Url
+ * @throws ProgrammingError
+ */
+ public function getBaseUrl()
+ {
+ if ($this->baseUrl === null) {
+ throw new ProgrammingError('Renderer has no baseUrl');
+ }
+
+ return clone($this->baseUrl);
+ }
+
+ /**
+ * @return bool
+ */
+ public function isLocked()
+ {
+ return $this->locked;
+ }
+
+ /**
+ * @return $this
+ */
+ public function lock()
+ {
+ $this->locked = true;
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function unlock()
+ {
+ $this->locked = false;
+ return $this;
+ }
+
+ /**
+ * TODO: Get rid of this
+ *
+ * @return $this
+ */
+ public function setIsBreadcrumb()
+ {
+ $this->isBreadcrumb = true;
+ return $this;
+ }
+
+ public function isBreadcrumb()
+ {
+ return $this->isBreadcrumb;
+ }
+
+ protected function createUnboundParent(BpConfig $bp)
+ {
+ return $bp->getNode('__unbound__');
+ }
+
+ /**
+ * Just to be on the safe side
+ */
+ public function __destruct()
+ {
+ unset($this->parent);
+ unset($this->config);
+ }
+}
diff --git a/library/Businessprocess/Renderer/TileRenderer.php b/library/Businessprocess/Renderer/TileRenderer.php
new file mode 100644
index 0000000..21c2f6a
--- /dev/null
+++ b/library/Businessprocess/Renderer/TileRenderer.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Renderer;
+
+use Icinga\Module\Businessprocess\ImportedNode;
+use Icinga\Module\Businessprocess\Renderer\TileRenderer\NodeTile;
+use Icinga\Module\Businessprocess\Web\Form\CsrfToken;
+use ipl\Html\Html;
+
+class TileRenderer extends Renderer
+{
+ public function assemble()
+ {
+ $bp = $this->config;
+ $nodesDiv = Html::tag(
+ 'div',
+ [
+ 'class' => ['sortable', 'tiles', $this->howMany()],
+ 'data-base-target' => '_self',
+ 'data-sortable-disabled' => $this->isLocked() || $this->appliesCustomSorting()
+ ? 'true'
+ : 'false',
+ 'data-sortable-data-id-attr' => 'id',
+ 'data-sortable-direction' => 'horizontal', // Otherwise movement is buggy on small lists
+ 'data-csrf-token' => CsrfToken::generate()
+ ]
+ );
+
+ if ($this->wantsRootNodes()) {
+ $nodesDiv->getAttributes()->add(
+ 'data-action-url',
+ $this->getUrl()->with(['config' => $bp->getName()])->getAbsoluteUrl()
+ );
+ } else {
+ $nodeName = $this->parent instanceof ImportedNode
+ ? $this->parent->getNodeName()
+ : $this->parent->getName();
+ $nodesDiv->getAttributes()
+ ->add('data-node-name', $nodeName)
+ ->add('data-action-url', $this->getUrl()
+ ->with([
+ 'config' => $this->parent->getBpConfig()->getName(),
+ 'node' => $nodeName
+ ])
+ ->getAbsoluteUrl());
+ }
+
+ $path = $this->getCurrentPath();
+ foreach ($this->sort($this->getChildNodes()) as $name => $node) {
+ $this->add(new NodeTile($this, $node, $path));
+ }
+
+ if ($this->wantsRootNodes()) {
+ $unbound = $this->createUnboundParent($bp);
+ if ($unbound->hasChildren()) {
+ $this->add(new NodeTile($this, $unbound));
+ }
+ }
+
+ $nodesDiv->addHtml(...$this->getContent());
+ $this->setHtmlContent($nodesDiv);
+ }
+
+ /**
+ * A CSS class giving a rough indication of how many nodes we have
+ *
+ * This is used to show larger tiles when there are few and smaller
+ * ones if there are many.
+ *
+ * @return string
+ */
+ protected function howMany()
+ {
+ $count = $this->countChildNodes();
+ $howMany = 'normal';
+
+ if ($count <= 6) {
+ $howMany = 'few';
+ } elseif ($count > 12) {
+ $howMany = 'many';
+ }
+
+ return $howMany;
+ }
+}
diff --git a/library/Businessprocess/Renderer/TileRenderer/NodeTile.php b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php
new file mode 100644
index 0000000..1f32f54
--- /dev/null
+++ b/library/Businessprocess/Renderer/TileRenderer/NodeTile.php
@@ -0,0 +1,353 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Renderer\TileRenderer;
+
+use Icinga\Date\DateFormatter;
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\ImportedNode;
+use Icinga\Module\Businessprocess\MonitoredNode;
+use Icinga\Module\Businessprocess\Node;
+use Icinga\Module\Businessprocess\Renderer\Renderer;
+use Icinga\Web\Url;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use ipl\Web\Widget\Icon;
+use ipl\Web\Widget\Link;
+use ipl\Web\Widget\StateBall;
+
+class NodeTile extends BaseHtmlElement
+{
+ protected $tag = 'div';
+
+ protected $renderer;
+
+ protected $name;
+
+ protected $node;
+
+ protected $path;
+
+ /**
+ * @var BaseHtmlElement
+ */
+ private $actions;
+
+ /**
+ * NodeTile constructor.
+ * @param Renderer $renderer
+ * @param Node $node
+ * @param ?array $path
+ */
+ public function __construct(Renderer $renderer, Node $node, $path = null)
+ {
+ $this->renderer = $renderer;
+ $this->node = $node;
+ $this->path = $path;
+ }
+
+ protected function actions()
+ {
+ if ($this->actions === null) {
+ $this->addActions();
+ }
+ return $this->actions;
+ }
+
+ protected function addActions()
+ {
+ $this->actions = Html::tag(
+ 'div',
+ [
+ 'class' => 'actions'
+ ]
+ );
+
+ return $this->add($this->actions);
+ }
+
+ public function render()
+ {
+ $renderer = $this->renderer;
+ $node = $this->node;
+
+ $attributes = $this->getAttributes();
+ $attributes->add('class', $renderer->getNodeClasses($node));
+ $attributes->add('id', $renderer->getId($node, $this->path));
+ if (! $renderer->isLocked()) {
+ $attributes->add('data-node-name', $node->getName());
+ }
+
+ if (! $renderer->isBreadcrumb()) {
+ $this->addDetailsActions();
+
+ if (! $renderer->isLocked()) {
+ $this->addActionLinks();
+ }
+ }
+ if (! $node instanceof ImportedNode || $node->getBpConfig()->hasNode($node->getName())) {
+ $link = $this->getMainNodeLink();
+ if ($renderer->isBreadcrumb()) {
+ $state = strtolower($node->getStateName());
+ if ($node->isHandled()) {
+ $state = $state . ' handled';
+ }
+ $link->prepend((new StateBall($state, StateBall::SIZE_MEDIUM))->addAttributes([
+ 'title' => sprintf(
+ '%s %s',
+ $state,
+ DateFormatter::timeSince($node->getLastStateChange())
+ )
+ ]));
+ }
+
+ $this->add($link);
+ } else {
+ $this->add(new Link($node->getAlias(), $this->getMainNodeUrl($node)->getAbsoluteUrl()));
+ }
+
+ if ($this->renderer->rendersSubNode()
+ && $this->renderer->getParentNode()->getChildState($node) !== $node->getState()
+ ) {
+ $this->add(
+ (new StateBall(strtolower($node->getStateName()), StateBall::SIZE_MEDIUM))
+ ->addAttributes([
+ 'class' => 'overridden-state',
+ 'title' => sprintf(
+ '%s',
+ $node->getStateName()
+ )
+ ])
+ );
+ }
+
+ if ($node instanceof BpNode && !$renderer->isBreadcrumb()) {
+ $this->add($renderer->renderStateBadges($node->getStateSummary(), $node->countChildren()));
+ }
+
+ return parent::render();
+ }
+
+ protected function getMainNodeUrl(Node $node)
+ {
+ if ($node instanceof BpNode) {
+ return $this->makeBpUrl($node);
+ } else {
+ /** @var MonitoredNode $node */
+ return $node->getUrl();
+ }
+ }
+
+ protected function buildBaseNodeUrl(Node $node)
+ {
+ $url = $this->renderer->getBaseUrl();
+
+ $p = $url->getParams();
+ if ($node instanceof ImportedNode
+ && $this->renderer->getBusinessProcess()->getName() === $node->getBpConfig()->getName()
+ ) {
+ $p->set('node', $node->getNodeName());
+ } elseif ($this->renderer->rendersImportedNode()) {
+ $p->set('node', $node->getIdentifier());
+ } else {
+ $p->set('node', $node->getName());
+ }
+
+ if (! empty($this->path)) {
+ $p->addValues('path', $this->path);
+ }
+
+ return $url;
+ }
+
+ protected function makeBpUrl(BpNode $node)
+ {
+ return $this->buildBaseNodeUrl($node);
+ }
+
+ /**
+ * @return BaseHtmlElement
+ */
+ protected function getMainNodeLink()
+ {
+ $node = $this->node;
+ $url = $this->getMainNodeUrl($node);
+ if ($node instanceof MonitoredNode) {
+ $link = Html::tag(
+ 'a',
+ ['href' => $url, 'data-base-target' => '_next'],
+ $node->getAlias() ?? $node->getName()
+ );
+ } else {
+ $link = Html::tag('a', ['href' => $url], $node->getAlias());
+ }
+
+ return $link;
+ }
+
+ protected function addDetailsActions()
+ {
+ $node = $this->node;
+ $url = $this->getMainNodeUrl($node);
+
+ if ($node instanceof BpNode) {
+ $this->actions()->add(Html::tag(
+ 'a',
+ [
+ 'href' => $url->with('mode', 'tile'),
+ 'title' => mt('businessprocess', 'Show tiles for this subtree')
+ ],
+ new Icon('grip')
+ ))->add(Html::tag(
+ 'a',
+ [
+ 'href' => $url->with('mode', 'tree'),
+ 'title' => mt('businessprocess', 'Show this subtree as a tree')
+ ],
+ new Icon('sitemap')
+ ));
+ if ($node instanceof ImportedNode) {
+ $bpConfig = $node->getBpConfig();
+ if ($bpConfig->isFaulty() || $bpConfig->hasNode($node->getName())) {
+ $this->actions()->add(Html::tag(
+ 'a',
+ [
+ 'data-base-target' => '_next',
+ 'href' => $bpConfig->isFaulty()
+ ? $this->renderer->getBaseUrl()->setParam('config', $bpConfig->getName())
+ : $this->renderer->getSourceUrl($node)->getAbsoluteUrl(),
+ 'title' => mt(
+ 'businessprocess',
+ 'Show this process as part of its original configuration'
+ )
+ ],
+ new Icon('share')
+ ));
+ }
+ }
+
+ $url = $node->getInfoUrl();
+
+ if ($url !== null) {
+ $link = Html::tag(
+ 'a',
+ [
+ 'href' => $url,
+ 'class' => 'node-info',
+ 'title' => sprintf('%s: %s', mt('businessprocess', 'More information'), $url)
+ ],
+ new Icon('info')
+ );
+ if (preg_match('#^http(?:s)?://#', $url)) {
+ $link->addAttributes(['target' => '_blank']);
+ }
+ $this->actions()->add($link);
+ }
+ } else {
+ $this->actions()->add(Html::tag(
+ 'a',
+ ['href' => $node->getUrl(), 'data-base-target' => '_next'],
+ $node->getIcon()
+ ));
+ }
+
+ if ($node->isAcknowledged()) {
+ $this->actions()->add(new Icon('check', ['class' => 'handled-icon']));
+ } elseif ($node->isInDowntime()) {
+ $this->actions()->add(new Icon('plug', ['class' => 'handled-icon']));
+ }
+ }
+
+ protected function addActionLinks()
+ {
+ $parent = $this->renderer->getParentNode();
+ if ($parent !== null) {
+ $baseUrl = Url::fromPath('businessprocess/process/show', [
+ 'config' => $parent->getBpConfig()->getName(),
+ 'node' => $parent instanceof ImportedNode
+ ? $parent->getNodeName()
+ : $parent->getName(),
+ 'unlocked' => true
+ ]);
+ } else {
+ $baseUrl = Url::fromPath('businessprocess/process/show', [
+ 'config' => $this->node->getBpConfig()->getName(),
+ 'unlocked' => true
+ ]);
+ }
+
+ if ($this->node instanceof MonitoredNode) {
+ $this->actions()->add(Html::tag(
+ 'a',
+ [
+ 'href' => $baseUrl
+ ->with('action', 'simulation')
+ ->with('simulationnode', $this->node->getName()),
+ 'title' => mt(
+ 'businessprocess',
+ 'Show the business impact of this node by simulating a specific state'
+ )
+ ],
+ new Icon('wand-magic-sparkles')
+ ));
+
+ $this->actions()->add(Html::tag(
+ 'a',
+ [
+ 'href' => $baseUrl
+ ->with('action', 'editmonitored')
+ ->with('editmonitorednode', $this->node->getName()),
+ 'title' => mt('businessprocess', 'Modify this monitored node')
+ ],
+ new Icon('edit')
+ ));
+ }
+
+ if ($this->renderer->getBusinessProcess()->getMetadata()->canModify()
+ && $this->node->getBpConfig()->getName() === $this->renderer->getBusinessProcess()->getName()
+ && $this->node->getName() !== '__unbound__'
+ ) {
+ if ($this->node instanceof BpNode) {
+ $this->actions()->add(Html::tag(
+ 'a',
+ [
+ 'href' => $baseUrl
+ ->with('action', 'edit')
+ ->with('editnode', $this->node->getName()),
+ 'title' => mt('businessprocess', 'Modify this business process node')
+ ],
+ new Icon('edit')
+ ));
+
+ $addUrl = $baseUrl->with([
+ 'node' => $this->node->getName(),
+ 'action' => 'add'
+ ]);
+ $addUrl->getParams()->addValues('path', $this->path);
+ $this->actions()->add(Html::tag(
+ 'a',
+ [
+ 'href' => $addUrl,
+ 'title' => mt('businessprocess', 'Add a new sub-node to this business process')
+ ],
+ new Icon('plus')
+ ));
+ }
+ }
+
+ if ($this->renderer->getBusinessProcess()->getMetadata()->canModify()) {
+ $params = array(
+ 'action' => 'delete',
+ 'deletenode' => $this->node->getName(),
+ );
+
+ $this->actions()->add(Html::tag(
+ 'a',
+ [
+ 'href' => $baseUrl->with($params),
+ 'title' => mt('businessprocess', 'Delete this node')
+ ],
+ new Icon('xmark')
+ ));
+ }
+ }
+}
diff --git a/library/Businessprocess/Renderer/TreeRenderer.php b/library/Businessprocess/Renderer/TreeRenderer.php
new file mode 100644
index 0000000..097d148
--- /dev/null
+++ b/library/Businessprocess/Renderer/TreeRenderer.php
@@ -0,0 +1,380 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Renderer;
+
+use Icinga\Application\Version;
+use Icinga\Date\DateFormatter;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\ImportedNode;
+use Icinga\Module\Businessprocess\Node;
+use Icinga\Module\Businessprocess\Web\Form\CsrfToken;
+use ipl\Html\Attributes;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use ipl\Html\HtmlElement;
+use ipl\Web\Widget\Icon;
+use ipl\Web\Widget\StateBall;
+
+class TreeRenderer extends Renderer
+{
+ const NEW_COLLAPSIBLE_IMPLEMENTATION_SINCE = '2.11.2';
+
+ public function assemble()
+ {
+ $bp = $this->config;
+ $htmlId = $bp->getHtmlId();
+ $tree = Html::tag(
+ 'ul',
+ [
+ 'id' => $htmlId,
+ 'class' => ['bp', 'sortable', $this->wantsRootNodes() ? '' : 'process'],
+ 'data-sortable-disabled' => $this->isLocked() || $this->appliesCustomSorting()
+ ? 'true'
+ : 'false',
+ 'data-sortable-data-id-attr' => 'id',
+ 'data-sortable-direction' => 'vertical',
+ 'data-sortable-group' => json_encode([
+ 'name' => $this->wantsRootNodes() ? 'root' : $htmlId,
+ 'put' => 'function:rowPutAllowed'
+ ]),
+ 'data-sortable-invert-swap' => 'true',
+ 'data-csrf-token' => CsrfToken::generate()
+ ],
+ $this->renderBp($bp)
+ );
+ if ($this->wantsRootNodes()) {
+ $tree->getAttributes()->add(
+ 'data-action-url',
+ $this->getUrl()->with(['config' => $bp->getName()])->getAbsoluteUrl()
+ );
+
+ if (version_compare(Version::VERSION, self::NEW_COLLAPSIBLE_IMPLEMENTATION_SINCE, '<')) {
+ $tree->getAttributes()->add('data-is-root-config', true);
+ }
+ } else {
+ $nodeName = $this->parent instanceof ImportedNode
+ ? $this->parent->getNodeName()
+ : $this->parent->getName();
+ $tree->getAttributes()
+ ->add('data-node-name', $nodeName)
+ ->add('data-action-url', $this->getUrl()
+ ->with([
+ 'config' => $this->parent->getBpConfig()->getName(),
+ 'node' => $nodeName
+ ])
+ ->getAbsoluteUrl());
+ }
+
+ $this->addHtml($tree);
+ }
+
+ /**
+ * @param BpConfig $bp
+ * @return array
+ */
+ public function renderBp(BpConfig $bp)
+ {
+ $html = [];
+ if ($this->wantsRootNodes()) {
+ $nodes = $bp->getRootNodes();
+ } else {
+ $nodes = $this->parent->getChildren();
+ }
+
+ foreach ($this->sort($nodes) as $name => $node) {
+ if ($node instanceof BpNode) {
+ $html[] = $this->renderNode($bp, $node);
+ } else {
+ $html[] = $this->renderChild($bp, $this->parent, $node);
+ }
+ }
+
+ return $html;
+ }
+
+ protected function getStateClassNames(Node $node)
+ {
+ $state = strtolower($node->getStateName());
+
+ if ($node->isMissing()) {
+ return array('missing');
+ } elseif ($state === 'ok') {
+ if ($node->hasMissingChildren()) {
+ return array('ok', 'missing-children');
+ } else {
+ return array('ok');
+ }
+ } else {
+ return array('problem', $state);
+ }
+ }
+
+ /**
+ * @param Node $node
+ * @param array $path
+ * @param BpNode $parent
+ * @return BaseHtmlElement[]
+ */
+ public function getNodeIcons(Node $node, array $path = null, BpNode $parent = null)
+ {
+ $icons = [];
+ if (empty($path) && $node instanceof BpNode) {
+ $icons[] = new Icon('sitemap');
+ } else {
+ $icons[] = $node->getIcon();
+ }
+ $state = strtolower($node->getStateName($parent !== null ? $parent->getChildState($node) : null));
+ if ($node->isHandled()) {
+ $state = $state . ' handled';
+ }
+ $icons[] = (new StateBall($state, StateBall::SIZE_MEDIUM))->addAttributes([
+ 'title' => sprintf(
+ '%s %s',
+ $state,
+ DateFormatter::timeSince($node->getLastStateChange())
+ )
+ ]);
+
+ if ($node->isAcknowledged()) {
+ $icons[] = new Icon('check');
+ } elseif ($node->isInDowntime()) {
+ $icons[] = new Icon('plug');
+ }
+
+ return $icons;
+ }
+
+ public function getOverriddenState($fakeState, Node $node)
+ {
+ $overriddenState = Html::tag('div', ['class' => 'overridden-state']);
+ $overriddenState->add(
+ (new StateBall(strtolower($node->getStateName()), StateBall::SIZE_MEDIUM))
+ ->addAttributes([
+ 'title' => sprintf(
+ '%s',
+ $node->getStateName()
+ )
+ ])
+ );
+
+ $overriddenState->add(new Icon('arrow-right'));
+ $overriddenState->add(
+ (new StateBall(strtolower($node->getStateName($fakeState)), StateBall::SIZE_MEDIUM))
+ ->addAttributes([
+ 'title' => sprintf(
+ '%s',
+ $node->getStateName($fakeState)
+ ),
+ 'class' => 'last'
+ ])
+ );
+
+ return $overriddenState;
+ }
+
+ /**
+ * @param BpConfig $bp
+ * @param Node $node
+ * @param array $path
+ *
+ * @return string
+ */
+ public function renderNode(BpConfig $bp, Node $node, $path = array())
+ {
+ $htmlId = $this->getId($node, $path);
+ $li = Html::tag(
+ 'li',
+ [
+ 'id' => $htmlId,
+ 'class' => ['bp', 'movable', $node->getObjectClassName()],
+ 'data-node-name' => $node instanceof ImportedNode
+ ? $node->getNodeName()
+ : $node->getName()
+ ]
+ );
+ $attributes = $li->getAttributes();
+ $attributes->add('class', $this->getStateClassNames($node));
+ if ($node->isHandled()) {
+ $attributes->add('class', 'handled');
+ }
+ if ($node instanceof BpNode) {
+ $attributes->add('class', 'operator');
+ } else {
+ $attributes->add('class', 'node');
+ }
+
+ $details = new HtmlElement('details', Attributes::create(['open' => true]));
+ $summary = new HtmlElement('summary');
+ if (version_compare(Version::VERSION, self::NEW_COLLAPSIBLE_IMPLEMENTATION_SINCE, '>=')) {
+ $details->getAttributes()->add('class', 'collapsible');
+ $summary->getAttributes()->add('class', 'collapsible-control'); // Helps JS, improves performance a bit
+ }
+
+ $summary->addHtml(
+ new Icon('caret-down', ['class' => 'collapse-icon']),
+ new Icon('caret-right', ['class' => 'expand-icon'])
+ );
+
+ $summary->add($this->getNodeIcons($node, $path));
+
+ $summary->add(Html::tag('span', null, $node->getAlias()));
+
+ if ($node instanceof BpNode) {
+ $summary->add(Html::tag('span', ['class' => 'op'], $node->operatorHtml()));
+ }
+
+ if ($node instanceof BpNode && $node->hasInfoUrl()) {
+ $summary->add($this->createInfoAction($node));
+ }
+
+ $differentConfig = $node->getBpConfig()->getName() !== $this->getBusinessProcess()->getName();
+ if (! $this->isLocked() && !$differentConfig) {
+ $summary->add($this->getActionIcons($bp, $node));
+ } elseif ($differentConfig) {
+ $summary->add($this->actionIcon(
+ 'share',
+ $node->getBpConfig()->isFaulty()
+ ? $this->getBaseUrl()->setParam('config', $node->getBpConfig()->getName())
+ : $this->getSourceUrl($node)->addParams(['mode' => 'tree'])->getAbsoluteUrl(),
+ mt('businessprocess', 'Show this process as part of its original configuration')
+ )->addAttributes(['data-base-target' => '_next']));
+ }
+
+ $ul = Html::tag('ul', [
+ 'class' => ['bp', 'sortable'],
+ 'data-sortable-disabled' => ($this->isLocked() || $differentConfig || $this->appliesCustomSorting())
+ ? 'true'
+ : 'false',
+ 'data-sortable-invert-swap' => 'true',
+ 'data-sortable-data-id-attr' => 'id',
+ 'data-sortable-draggable' => '.movable',
+ 'data-sortable-direction' => 'vertical',
+ 'data-sortable-group' => json_encode([
+ 'name' => $htmlId, // Unique, so that the function below is the only deciding factor
+ 'put' => 'function:rowPutAllowed'
+ ]),
+ 'data-csrf-token' => CsrfToken::generate(),
+ 'data-action-url' => $this->getUrl()
+ ->with([
+ 'config' => $node->getBpConfig()->getName(),
+ 'node' => $node instanceof ImportedNode
+ ? $node->getNodeName()
+ : $node->getName()
+ ])
+ ->getAbsoluteUrl()
+ ]);
+
+ $path[] = $differentConfig ? $node->getIdentifier() : $node->getName();
+ foreach ($this->sort($node->getChildren()) as $name => $child) {
+ if ($child instanceof BpNode) {
+ $ul->add($this->renderNode($bp, $child, $path));
+ } else {
+ $ul->add($this->renderChild($bp, $node, $child, $path));
+ }
+ }
+
+ $details->addHtml($summary);
+ $details->addHtml($ul);
+ $li->addHtml($details);
+
+ return $li;
+ }
+
+ protected function renderChild($bp, BpNode $parent, Node $node, $path = null)
+ {
+ $li = Html::tag('li', [
+ 'class' => 'movable',
+ 'id' => $this->getId($node, $path ?: []),
+ 'data-node-name' => $node->getName()
+ ]);
+
+ $li->add($this->getNodeIcons($node, $path, $parent));
+
+ $link = $node->getLink();
+ $link->getAttributes()->set('data-base-target', '_next');
+ $li->add($link);
+
+ if (($overriddenState = $parent->getChildState($node)) !== $node->getState()) {
+ $li->add($this->getOverriddenState($overriddenState, $node));
+ }
+
+ if (! $this->isLocked() && $node->getBpConfig()->getName() === $this->getBusinessProcess()->getName()) {
+ $li->add($this->getActionIcons($bp, $node));
+ }
+
+ return $li;
+ }
+
+ protected function getActionIcons(BpConfig $bp, Node $node)
+ {
+ if ($node instanceof BpNode) {
+ if ($bp->getMetadata()->canModify()) {
+ return [$this->createEditAction($bp, $node), $this->renderAddNewNode($node)];
+ } else {
+ return '';
+ }
+ } else {
+ return $this->createSimulationAction($bp, $node);
+ }
+ }
+
+ protected function createEditAction(BpConfig $bp, BpNode $node)
+ {
+ return $this->actionIcon(
+ 'edit',
+ $this->getUrl()->with(array(
+ 'action' => 'edit',
+ 'editnode' => $node->getName()
+ )),
+ mt('businessprocess', 'Modify this node')
+ );
+ }
+
+ protected function createSimulationAction(BpConfig $bp, Node $node)
+ {
+ return $this->actionIcon(
+ 'wand-magic-sparkles',
+ $this->getUrl()->with(array(
+ //'config' => $bp->getName(),
+ 'action' => 'simulation',
+ 'simulationnode' => $node->getName()
+ )),
+ mt('businessprocess', 'Simulate a specific state')
+ );
+ }
+
+ protected function createInfoAction(BpNode $node)
+ {
+ $url = $node->getInfoUrl();
+ return $this->actionIcon(
+ 'question',
+ $url,
+ sprintf('%s: %s', mt('businessprocess', 'More information'), $url)
+ )->addAttributes(['target' => '_blank']);
+ }
+
+ protected function actionIcon($icon, $url, $title)
+ {
+ return Html::tag(
+ 'a',
+ [
+ 'href' => $url,
+ 'title' => $title,
+ 'class' => 'action-link'
+ ],
+ new Icon($icon)
+ );
+ }
+
+ protected function renderAddNewNode($parent)
+ {
+ return $this->actionIcon(
+ 'plus',
+ $this->getUrl()
+ ->with('action', 'add')
+ ->with('node', $parent->getName()),
+ mt('businessprocess', 'Add a new business process node')
+ );
+ }
+}
diff --git a/library/Businessprocess/ServiceNode.php b/library/Businessprocess/ServiceNode.php
new file mode 100644
index 0000000..c80b984
--- /dev/null
+++ b/library/Businessprocess/ServiceNode.php
@@ -0,0 +1,95 @@
+<?php
+
+namespace Icinga\Module\Businessprocess;
+
+use Icinga\Module\Businessprocess\Web\Url;
+use ipl\I18n\Translation;
+
+class ServiceNode extends MonitoredNode
+{
+ use Translation;
+
+ protected $hostname;
+
+ /** @var string Alias of the host */
+ protected $hostAlias;
+
+ protected $service;
+
+ protected $className = 'service';
+
+ protected $icon = 'gear';
+
+ public function __construct($object)
+ {
+ $this->name = BpConfig::joinNodeName($object->hostname, $object->service);
+ $this->hostname = $object->hostname;
+ $this->service = $object->service;
+ if (isset($object->state)) {
+ $this->setState($object->state);
+ } else {
+ $this->setState(0)->setMissing();
+ }
+ }
+
+ public function getHostname()
+ {
+ return $this->hostname;
+ }
+
+ /**
+ * Get the host alias
+ *
+ * @return string
+ */
+ public function getHostAlias()
+ {
+ return $this->hostAlias;
+ }
+
+ /**
+ * Set the host alias
+ *
+ * @param string $hostAlias
+ *
+ * @return $this
+ */
+ public function setHostAlias($hostAlias)
+ {
+ $this->hostAlias = $hostAlias;
+
+ return $this;
+ }
+
+ public function getServiceDescription()
+ {
+ return $this->service;
+ }
+
+ public function getAlias()
+ {
+ if ($this->getHostAlias() === null || $this->alias === null) {
+ return null;
+ }
+
+ return sprintf(
+ $this->translate('%s on %s', '<service> on <host>'),
+ $this->alias,
+ $this->getHostAlias()
+ );
+ }
+
+ public function getUrl()
+ {
+ $params = array(
+ 'host' => $this->getHostname(),
+ 'service' => $this->getServiceDescription()
+ );
+
+ if ($this->getBpConfig()->hasBackendName()) {
+ $params['backend'] = $this->getBpConfig()->getBackendName();
+ }
+
+ return Url::fromPath('businessprocess/service/show', $params);
+ }
+}
diff --git a/library/Businessprocess/Simulation.php b/library/Businessprocess/Simulation.php
new file mode 100644
index 0000000..1bc9d1d
--- /dev/null
+++ b/library/Businessprocess/Simulation.php
@@ -0,0 +1,185 @@
+<?php
+
+namespace Icinga\Module\Businessprocess;
+
+use Icinga\Exception\ProgrammingError;
+use Icinga\Web\Session\SessionNamespace;
+
+class Simulation
+{
+ const DEFAULT_SESSION_KEY = 'bp-simulations';
+
+ /**
+ * @var SessionNamespace
+ */
+ protected $session;
+
+ /**
+ * @var string
+ */
+ protected $sessionKey;
+
+ /**
+ * @var array
+ */
+ protected $simulations = array();
+
+ /**
+ * Simulation constructor.
+ * @param array $simulations
+ */
+ public function __construct(array $simulations = array())
+ {
+ $this->simulations = $simulations;
+ }
+
+ /**
+ * @param array $simulations
+ * @return static
+ */
+ public static function create(array $simulations = array())
+ {
+ return new static($simulations);
+ }
+
+ /**
+ * @param SessionNamespace $session
+ * @param null $sessionKey
+ * @return $this
+ */
+ public static function fromSession(SessionNamespace $session, $sessionKey = null)
+ {
+ return static::create()
+ ->setSessionKey($sessionKey)
+ ->persistToSession($session);
+ }
+
+ /**
+ * @param string $key
+ * @return $this
+ */
+ public function setSessionKey($key = null)
+ {
+ if ($key === null) {
+ $this->sessionKey = Simulation::DEFAULT_SESSION_KEY;
+ } else {
+ $this->sessionKey = $key;
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param SessionNamespace $session
+ * @return $this
+ */
+ public function persistToSession(SessionNamespace $session)
+ {
+ $this->session = $session;
+ $this->simulations = $this->session->get($this->sessionKey, array());
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function simulations()
+ {
+ return $this->simulations;
+ }
+
+ /**
+ * @param $simulations
+ * @return $this
+ */
+ protected function setSimulations($simulations)
+ {
+ $this->simulations = $simulations;
+ if ($this->session !== null) {
+ $this->session->set($this->sessionKey, $simulations);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function clear()
+ {
+ $this->simulations = array();
+ if ($this->session !== null) {
+ $this->session->set($this->sessionKey, array());
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return int
+ */
+ public function count()
+ {
+ return count($this->simulations());
+ }
+
+ /**
+ * @return bool
+ */
+ public function isEmpty()
+ {
+ return $this->count() === 0;
+ }
+
+ /**
+ * @param $node
+ * @param $properties
+ */
+ public function set($node, $properties)
+ {
+ $simulations = $this->simulations();
+ $simulations[$node] = $properties;
+ $this->setSimulations($simulations);
+ }
+
+ /**
+ * @param $name
+ * @return bool
+ */
+ public function hasNode($name)
+ {
+ $simulations = $this->simulations();
+ return array_key_exists($name, $simulations);
+ }
+
+ /**
+ * @param $name
+ * @return mixed
+ * @throws ProgrammingError
+ */
+ public function getNode($name)
+ {
+ $simulations = $this->simulations();
+ if (! array_key_exists($name, $simulations)) {
+ throw new ProgrammingError('Trying to access invalid node %s', $name);
+ }
+ return $simulations[$name];
+ }
+
+ /**
+ * @param $node
+ * @return bool
+ */
+ public function remove($node)
+ {
+ $simulations = $this->simulations();
+ if (array_key_exists($node, $simulations)) {
+ unset($simulations[$node]);
+ $this->setSimulations($simulations);
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/library/Businessprocess/State/IcingaDbState.php b/library/Businessprocess/State/IcingaDbState.php
new file mode 100644
index 0000000..1a66900
--- /dev/null
+++ b/library/Businessprocess/State/IcingaDbState.php
@@ -0,0 +1,191 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\State;
+
+use Exception;
+use Icinga\Application\Benchmark;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\IcingaDbObject;
+use Icinga\Module\Businessprocess\ServiceNode;
+use Icinga\Module\Icingadb\Common\IcingaRedis;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Module\Icingadb\Model\Service;
+use ipl\Sql\Connection as IcingaDbConnection;
+use ipl\Stdlib\Filter;
+
+class IcingaDbState
+{
+ /** @var BpConfig */
+ protected $config;
+
+ /** @var IcingaDbConnection */
+ protected $backend;
+
+ public function __construct(BpConfig $config)
+ {
+ $this->config = $config;
+ $this->backend = IcingaDbObject::fetchDb();
+ }
+
+ public static function apply(BpConfig $config)
+ {
+ $self = new static($config);
+ $self->retrieveStatesFromBackend();
+
+ return $config;
+ }
+
+ public function retrieveStatesFromBackend()
+ {
+ $config = $this->config;
+
+ try {
+ $this->reallyRetrieveStatesFromBackend();
+ } catch (Exception $e) {
+ $config->addError(
+ $config->translate('Could not retrieve process state: %s'),
+ $e->getMessage()
+ );
+ }
+ }
+
+ public function reallyRetrieveStatesFromBackend()
+ {
+ $config = $this->config;
+
+ $involvedHostNames = $config->listInvolvedHostNames();
+ if (empty($involvedHostNames)) {
+ return $this;
+ }
+
+ Benchmark::measure(sprintf(
+ 'Retrieving states for business process %s using Icinga DB backend',
+ $config->getName()
+ ));
+
+ $hosts = Host::on($this->backend)->columns([
+ 'id' => 'host.id',
+ 'name' => 'host.name',
+ 'display_name' => 'host.display_name',
+ 'hard_state' => 'host.state.hard_state',
+ 'soft_state' => 'host.state.soft_state',
+ 'last_state_change' => 'host.state.last_state_change',
+ 'in_downtime' => 'host.state.in_downtime',
+ 'is_acknowledged' => 'host.state.is_acknowledged'
+ ])->filter(Filter::equal('host.name', $involvedHostNames));
+
+ $services = Service::on($this->backend)->columns([
+ 'id' => 'service.id',
+ 'name' => 'service.name',
+ 'display_name' => 'service.display_name',
+ 'host_name' => 'host.name',
+ 'host_display_name' => 'host.display_name',
+ 'hard_state' => 'service.state.hard_state',
+ 'soft_state' => 'service.state.soft_state',
+ 'last_state_change' => 'service.state.last_state_change',
+ 'in_downtime' => 'service.state.in_downtime',
+ 'is_acknowledged' => 'service.state.is_acknowledged'
+ ])->filter(Filter::equal('host.name', $involvedHostNames));
+
+ // All of this is ipl-sql now, for performance reasons
+ foreach ($config->listInvolvedConfigs() as $cfg) {
+ $serviceIds = [];
+ $serviceResults = [];
+ foreach ($this->backend->yieldAll($services->assembleSelect()) as $row) {
+ $row->hex_id = bin2hex(is_resource($row->id) ? stream_get_contents($row->id) : $row->id);
+ $serviceIds[] = $row->hex_id;
+ $serviceResults[] = $row;
+ }
+
+ $redisServiceResults = iterator_to_array(IcingaRedis::fetchServiceState($serviceIds, [
+ 'hard_state',
+ 'soft_state',
+ 'last_state_change',
+ 'in_downtime',
+ 'is_acknowledged'
+ ]));
+ foreach ($serviceResults as $row) {
+ if (isset($redisServiceResults[$row->hex_id])) {
+ $row = (object) array_merge(
+ (array) $row,
+ $redisServiceResults[$row->hex_id]
+ );
+ }
+
+ $this->handleDbRow($row, $cfg, 'service');
+ }
+
+ Benchmark::measure('Retrieved states for ' . count($serviceIds) . ' services in ' . $config->getName());
+
+ $hostIds = [];
+ $hostResults = [];
+ foreach ($this->backend->yieldAll($hosts->assembleSelect()) as $row) {
+ $row->hex_id = bin2hex(is_resource($row->id) ? stream_get_contents($row->id) : $row->id);
+ $hostIds[] = $row->hex_id;
+ $hostResults[] = $row;
+ }
+
+ $redisHostResults = iterator_to_array(IcingaRedis::fetchHostState($hostIds, [
+ 'hard_state',
+ 'soft_state',
+ 'last_state_change',
+ 'in_downtime',
+ 'is_acknowledged'
+ ]));
+ foreach ($hostResults as $row) {
+ if (isset($redisHostResults[$row->hex_id])) {
+ $row = (object) array_merge(
+ (array) $row,
+ $redisHostResults[$row->hex_id]
+ );
+ }
+
+ $this->handleDbRow($row, $cfg, 'host');
+ }
+
+ Benchmark::measure('Retrieved states for ' . count($hostIds) . ' hosts in ' . $config->getName());
+ }
+
+ Benchmark::measure('Got states for business process ' . $config->getName());
+
+ return $this;
+ }
+
+ protected function handleDbRow($row, BpConfig $config, $type)
+ {
+ if ($type === 'service') {
+ $key = BpConfig::joinNodeName($row->host_name, $row->name);
+ } else {
+ $key = BpConfig::joinNodeName($row->name, 'Hoststatus');
+ }
+
+ // We fetch more states than we need, so skip unknown ones
+ if (! $config->hasNode($key)) {
+ return;
+ }
+
+ $node = $config->getNode($key);
+
+ if ($this->config->usesHardStates()) {
+ if ($row->hard_state !== null) {
+ $node->setState($row->hard_state)->setMissing(false);
+ }
+ } else {
+ if ($row->soft_state !== null) {
+ $node->setState($row->soft_state)->setMissing(false);
+ }
+ }
+
+ if ($row->last_state_change !== null) {
+ $node->setLastStateChange($row->last_state_change / 1000.0);
+ }
+
+ $node->setDowntime($row->in_downtime === 'y');
+ $node->setAck($row->is_acknowledged === 'y');
+ $node->setAlias($row->display_name);
+
+ if ($node instanceof ServiceNode) {
+ $node->setHostAlias($row->host_display_name);
+ }
+ }
+}
diff --git a/library/Businessprocess/State/MonitoringState.php b/library/Businessprocess/State/MonitoringState.php
new file mode 100644
index 0000000..b6a2391
--- /dev/null
+++ b/library/Businessprocess/State/MonitoringState.php
@@ -0,0 +1,151 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\State;
+
+use Exception;
+use Icinga\Application\Benchmark;
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\ServiceNode;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+
+class MonitoringState
+{
+ /** @var BpConfig */
+ protected $config;
+
+ /** @var MonitoringBackend */
+ protected $backend;
+
+ private function __construct(BpConfig $config)
+ {
+ $this->config = $config;
+ $this->backend = $config->getBackend();
+ }
+
+ public static function apply(BpConfig $config)
+ {
+ $self = new static($config);
+ $self->retrieveStatesFromBackend();
+ return $config;
+ }
+
+ public function retrieveStatesFromBackend()
+ {
+ $config = $this->config;
+
+ try {
+ $this->reallyRetrieveStatesFromBackend();
+ } catch (Exception $e) {
+ $config->addError(
+ $config->translate('Could not retrieve process state: %s'),
+ $e->getMessage()
+ );
+ }
+ }
+
+ public function reallyRetrieveStatesFromBackend()
+ {
+ $config = $this->config;
+
+ Benchmark::measure('Retrieving states for business process ' . $config->getName());
+ $backend = $this->backend;
+
+ if ($config->usesHardStates()) {
+ $hostStateColumn = 'host_hard_state';
+ $hostStateChangeColumn = 'host_last_hard_state_change';
+ $serviceStateColumn = 'service_hard_state';
+ $serviceStateChangeColumn = 'service_last_hard_state_change';
+ } else {
+ $hostStateColumn = 'host_state';
+ $hostStateChangeColumn = 'host_last_state_change';
+ $serviceStateColumn = 'service_state';
+ $serviceStateChangeColumn = 'service_last_state_change';
+ }
+
+ $hosts = $config->listInvolvedHostNames();
+ if (empty($hosts)) {
+ return $this;
+ }
+
+ $hostFilter = Filter::expression('host_name', '=', $hosts);
+
+ $hostStatus = $backend->select()->from('hostStatus', array(
+ 'hostname' => 'host_name',
+ 'last_state_change' => $hostStateChangeColumn,
+ 'in_downtime' => 'host_in_downtime',
+ 'ack' => 'host_acknowledged',
+ 'state' => $hostStateColumn,
+ 'display_name' => 'host_display_name'
+ ))->applyFilter($hostFilter)->getQuery()->fetchAll();
+
+ Benchmark::measure('Retrieved states for ' . count($hostStatus) . ' hosts in ' . $config->getName());
+
+ // NOTE: we intentionally filter by host_name ONLY
+ // Tests with host IN ... AND service IN shows longer query times
+ // while retrieving 1635 (in 5ms) vs. 1388 (in ~430ms) services
+ $serviceStatus = $backend->select()->from('serviceStatus', array(
+ 'hostname' => 'host_name',
+ 'service' => 'service_description',
+ 'last_state_change' => $serviceStateChangeColumn,
+ 'in_downtime' => 'service_in_downtime',
+ 'ack' => 'service_acknowledged',
+ 'state' => $serviceStateColumn,
+ 'display_name' => 'service_display_name',
+ 'host_display_name' => 'host_display_name'
+ ))->applyFilter($hostFilter)->getQuery()->fetchAll();
+
+ Benchmark::measure('Retrieved states for ' . count($serviceStatus) . ' services in ' . $config->getName());
+
+ $configs = $config->listInvolvedConfigs();
+ foreach ($configs as $cfg) {
+ foreach ($serviceStatus as $row) {
+ $this->handleDbRow($row, $cfg);
+ }
+ foreach ($hostStatus as $row) {
+ $this->handleDbRow($row, $cfg);
+ }
+ }
+
+ // TODO: Union, single query?
+ Benchmark::measure('Got states for business process ' . $config->getName());
+
+ return $this;
+ }
+
+ protected function handleDbRow($row, BpConfig $config)
+ {
+ $key = BpConfig::joinNodeName(
+ $row->hostname,
+ property_exists($row, 'service')
+ ? $row->service
+ : 'Hoststatus'
+ );
+
+ // We fetch more states than we need, so skip unknown ones
+ if (! $config->hasNode($key)) {
+ return;
+ }
+
+ $node = $config->getNode($key);
+
+ if ($row->state !== null) {
+ $node->setState($row->state)->setMissing(false);
+ }
+ if ($row->last_state_change !== null) {
+ $node->setLastStateChange($row->last_state_change);
+ }
+ if ((int) $row->in_downtime === 1) {
+ $node->setDowntime(true);
+ }
+ if ((int) $row->ack === 1) {
+ $node->setAck(true);
+ }
+
+ $node->setAlias($row->display_name);
+
+ if ($node instanceof ServiceNode) {
+ $node->setHostAlias($row->host_display_name);
+ }
+ }
+}
diff --git a/library/Businessprocess/Storage/ConfigDiff.php b/library/Businessprocess/Storage/ConfigDiff.php
new file mode 100644
index 0000000..133cfb7
--- /dev/null
+++ b/library/Businessprocess/Storage/ConfigDiff.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Storage;
+
+use ipl\Html\ValidHtml;
+use Jfcherng\Diff\Differ;
+use Jfcherng\Diff\Factory\RendererFactory;
+
+class ConfigDiff implements ValidHtml
+{
+ protected $a;
+
+ protected $b;
+
+ protected $diff;
+ protected $opcodes;
+
+ protected function __construct($a, $b)
+ {
+ if (empty($a)) {
+ $this->a = array();
+ } else {
+ $this->a = explode("\n", (string) $a);
+ }
+
+ if (empty($b)) {
+ $this->b = array();
+ } else {
+ $this->b = explode("\n", (string) $b);
+ }
+
+ $options = array(
+ 'context' => 5,
+ // 'ignoreWhitespace' => true,
+ // 'ignoreCase' => true,
+ );
+ $this->diff = new Differ($this->a, $this->b, $options);
+ }
+
+ /**
+ * @return string
+ */
+ public function render()
+ {
+ return $this->renderHtmlSideBySide();
+ }
+
+ public function renderHtmlSideBySide()
+ {
+ $renderer = RendererFactory::make('SideBySide');
+ return $renderer->render($this->diff);
+ }
+
+ public function renderHtmlInline()
+ {
+ $renderer = RendererFactory::make('Inline');
+ return $renderer->render($this->diff);
+ }
+
+ public function renderTextContext()
+ {
+ $renderer = RendererFactory::make('Context');
+ return $renderer->render($this->diff);
+ }
+
+ public function renderTextUnified()
+ {
+ $renderer = RendererFactory::make('Unified');
+ return $renderer->render($this->diff);
+ }
+
+ public static function create($a, $b)
+ {
+ $diff = new static($a, $b);
+ return $diff;
+ }
+}
diff --git a/library/Businessprocess/Storage/LegacyConfigParser.php b/library/Businessprocess/Storage/LegacyConfigParser.php
new file mode 100644
index 0000000..754c7ff
--- /dev/null
+++ b/library/Businessprocess/Storage/LegacyConfigParser.php
@@ -0,0 +1,413 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Storage;
+
+use Icinga\Application\Benchmark;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\SystemPermissionException;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\Metadata;
+
+class LegacyConfigParser
+{
+ /** @var ?string */
+ protected static $prevKey;
+
+ /** @var int */
+ protected $currentLineNumber;
+
+ /** @var string */
+ protected $currentFilename;
+
+ protected $name;
+
+ /** @var BpConfig */
+ protected $config;
+
+ /** @var array */
+ protected $missingNodes = [];
+
+ /**
+ * LegacyConfigParser constructor
+ *
+ * @param $name
+ */
+ private function __construct($name)
+ {
+ $this->name = $name;
+ $this->config = new BpConfig();
+ $this->config->setName($name);
+ }
+
+ /**
+ * @return BpConfig
+ */
+ public function getParsedConfig()
+ {
+ return $this->config;
+ }
+
+ /**
+ * @param $name
+ * @param $filename
+ *
+ * @return BpConfig
+ */
+ public static function parseFile($name, $filename)
+ {
+ Benchmark::measure('Loading business process ' . $name);
+ $parser = new static($name);
+ $parser->reallyParseFile($filename);
+ Benchmark::measure('Business process ' . $name . ' loaded');
+ return $parser->getParsedConfig();
+ }
+
+ /**
+ * @param $name
+ * @param $string
+ *
+ * @return BpConfig
+ */
+ public static function parseString($name, $string)
+ {
+ Benchmark::measure('Loading BP config from file: ' . $name);
+ $parser = new static($name);
+
+ $config = $parser->getParsedConfig();
+ $config->setMetadata(
+ static::readMetadataFromString($name, $string)
+ );
+
+ foreach (preg_split('/\r?\n/', $string) as $line) {
+ $parser->parseLine($line);
+ }
+
+ $parser->resolveMissingNodes();
+
+ Benchmark::measure('Business process ' . $name . ' loaded');
+ return $config;
+ }
+
+ protected function reallyParseFile($filename)
+ {
+ $file = $this->currentFilename = $filename;
+ $fh = @fopen($file, 'r');
+ if (! $fh) {
+ throw new SystemPermissionException('Could not open "%s"', $filename);
+ }
+
+ $config = $this->config;
+ $config->setMetadata(
+ $this::readMetadataFromFileHeader($config->getName(), $filename)
+ );
+
+ $this->currentLineNumber = 0;
+ while ($line = fgets($fh)) {
+ $this->parseLine($line);
+ }
+
+ $this->resolveMissingNodes();
+
+ fclose($fh);
+ unset($this->currentLineNumber);
+ unset($this->currentFilename);
+ }
+
+ /**
+ * Resolve previously missed business process nodes
+ *
+ * @throws ConfigurationError In case a referenced process does not exist
+ */
+ protected function resolveMissingNodes()
+ {
+ foreach ($this->missingNodes as $name => $parents) {
+ foreach ($parents as $parent) {
+ /** @var BpNode $parent */
+ $parent->addChild($this->config->getNode($name));
+ }
+ }
+ }
+
+ public static function readMetadataFromFileHeader($name, $filename)
+ {
+ $metadata = new Metadata($name);
+ $fh = fopen($filename, 'r');
+ $cnt = 0;
+ static::$prevKey = null;
+ while ($cnt < 15 && false !== ($line = fgets($fh))) {
+ $cnt++;
+ static::parseHeaderLine($line, $metadata);
+ }
+
+ fclose($fh);
+ return $metadata;
+ }
+
+ public static function readMetadataFromString($name, &$string)
+ {
+ $metadata = new Metadata($name);
+
+ $lines = preg_split('/\r?\n/', substr($string, 0, 8092));
+ static::$prevKey = null;
+
+ foreach ($lines as $line) {
+ static::parseHeaderLine($line, $metadata);
+ }
+
+ return $metadata;
+ }
+
+ protected function splitCommaSeparated($string)
+ {
+ return preg_split('/\s*,\s*/', $string, -1, PREG_SPLIT_NO_EMPTY);
+ }
+
+ protected function readHeaderString($string, Metadata $metadata)
+ {
+ foreach (preg_split('/\r?\n/', $string) as $line) {
+ $this->parseHeaderLine($line, $metadata);
+ }
+
+ return $metadata;
+ }
+
+ /**
+ * @return array
+ */
+ protected function emptyHeader()
+ {
+ return array(
+ 'Title' => null,
+ 'Description' => null,
+ 'Owner' => null,
+ 'AllowedUsers' => null,
+ 'AllowedGroups' => null,
+ 'AllowedRoles' => null,
+ 'Backend' => null,
+ 'Statetype' => 'soft',
+ 'SLAHosts' => null
+ );
+ }
+
+ /**
+ * @param $line
+ * @param Metadata $metadata
+ */
+ protected static function parseHeaderLine($line, Metadata $metadata)
+ {
+ if (empty($line)) {
+ return;
+ }
+
+ if (preg_match('/^\s*#\s+(.+?)\s*:\s*(.+)$/', trim($line), $m)) {
+ if ($metadata->hasKey($m[1])) {
+ static::$prevKey = $m[1];
+ $metadata->set($m[1], $m[2]);
+ }
+ } elseif ($line[0] === '#') {
+ $line = ltrim($line, "#");
+
+ // Check if the line is from the multi-line comment and parse it accordingly
+ if (trim($line) !== '' && ! preg_match('/^\s*(.+?)\s*:$/', trim($line), $m) && static::$prevKey) {
+ $line = trim(
+ substr(
+ $line,
+ strlen(sprintf("%-15s :", static::$prevKey)) + 2
+ ),
+ "\n\r"
+ );
+
+ $description = $metadata->get(static::$prevKey) . "\n" . $line;
+ $metadata->set(static::$prevKey, $description);
+ }
+ }
+ }
+
+ /**
+ * @param $line
+ * @param BpConfig $bp
+ */
+ protected function parseDisplay(&$line, BpConfig $bp)
+ {
+ list($display, $name, $desc) = preg_split('~\s*(?<!\\\\);\s*~', substr($line, 8), 3);
+ $bp->getBpNode($name)->setAlias($desc)->setDisplay($display);
+ if ($display > 0) {
+ $bp->addRootNode($name);
+ }
+ }
+
+ protected function parseInfoUrl(&$line, BpConfig $bp)
+ {
+ list($name, $url) = preg_split('~\s*(?<!\\\\);\s*~', substr($line, 9), 2);
+ $bp->getBpNode($name)->setInfoUrl($url);
+ }
+
+ protected function parseStateOverrides(&$line, BpConfig $bp)
+ {
+ // state_overrides <bp-node>!<child>|n-n[,n-n]!<child>|n-n[,n-n]
+ $segments = preg_split('~\s*!\s*~', substr($line, 16));
+ /** @var BpNode $node */
+ $node = $bp->getNode(array_shift($segments));
+ foreach ($segments as $overrideDef) {
+ list($childName, $overrides) = preg_split('~\s*\|\s*~', $overrideDef, 2);
+
+ $stateOverrides = [];
+ foreach (preg_split('~\s*,\s*~', $overrides) as $override) {
+ list($from, $to) = preg_split('~\s*-\s*~', $override, 2);
+ $stateOverrides[(int) $from] = (int) $to;
+ }
+
+ $node->setStateOverrides($stateOverrides, $childName);
+ }
+ }
+
+ protected function parseExtraLine(&$line, $typeLength, BpConfig $bp)
+ {
+ $type = substr($line, 0, $typeLength);
+ if (substr($type, 0, 7) === 'display') {
+ $this->parseDisplay($line, $bp);
+ return true;
+ }
+
+ switch ($type) {
+ case 'external_info':
+ case 'extra_info':
+ break;
+ case 'info_url':
+ $this->parseInfoUrl($line, $bp);
+ break;
+ case 'state_overrides':
+ $this->parseStateOverrides($line, $bp);
+ break;
+ case 'template':
+ // compat, ignoring for now
+ break;
+ default:
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Parses a single line
+ *
+ * Adds eventual new knowledge to the given Business Process config
+ *
+ * @param $line
+ *
+ * @throws ConfigurationError
+ */
+ protected function parseLine(&$line)
+ {
+ $bp = $this->config;
+ $line = trim($line);
+
+ $this->currentLineNumber++;
+
+ // Skip empty or comment-only lines
+ if (empty($line) || $line[0] === '#') {
+ return;
+ }
+
+ // Space found in the first 16 cols? Might be a line with extra information
+ $pos = strpos($line, ' ');
+ if ($pos !== false && $pos < 16) {
+ if ($this->parseExtraLine($line, $pos, $bp)) {
+ return;
+ }
+ }
+
+ if (strpos($line, '=') === false) {
+ $this->parseError('Got invalid line');
+ }
+
+ list($name, $value) = preg_split('~\s*=\s*~', $line, 2);
+
+ $op = '&';
+ if (preg_match_all('~(?<!\\\\)([\|\+&\!\%\^])~', $value, $m)) {
+ $op = implode('', $m[1]);
+ for ($i = 1; $i < strlen($op); $i++) {
+ if ($op[$i] !== $op[$i - 1]) {
+ $this->parseError('Mixing operators is not allowed');
+ }
+ }
+ }
+ $op = $op[0];
+ $op_name = $op;
+
+ if ($op === '+') {
+ if (! preg_match('~^(\d+)(?::(\d+))?\s*of:\s*(.+?)$~', $value, $m)) {
+ $this->parseError('syntax: <var> = <num> of: <var1> + <var2> [+ <varn>]*');
+ }
+ $op_name = $m[1];
+ // New feature: $minWarn = $m[2];
+ $value = $m[3];
+ }
+
+ $node = new BpNode((object) array(
+ 'name' => $name,
+ 'operator' => $op_name,
+ 'child_names' => []
+ ));
+ $node->setBpConfig($bp);
+
+ $cmps = preg_split('~\s*(?<!\\\\)\\' . $op . '\s*~', $value, -1, PREG_SPLIT_NO_EMPTY);
+ foreach ($cmps as $val) {
+ $val = preg_replace('~(\\\\([\|\+&\!\%\^]))~', '$2', $val);
+ if (preg_match('~(?<!\\\\);~', $val)) {
+ if ($bp->hasNode($val)) {
+ $node->addChild($bp->getNode($val));
+ } else {
+ list($host, $service) = preg_split('~(?<!\\\\);~', $val, 2);
+ if ($service === 'Hoststatus') {
+ $node->addChild($bp->createHost(str_replace('\\;', ';', $host)));
+ } else {
+ $node->addChild($bp->createService(str_replace('\\;', ';', $host), $service));
+ }
+ }
+ } elseif ($val[0] === '@') {
+ if (strpos($val, ':') === false) {
+ throw new ConfigurationError(
+ "I'm unable to import full external configs, a node needs to be provided for '%s'",
+ $val
+ );
+ } else {
+ list($config, $nodeName) = preg_split('~:\s*~', substr($val, 1), 2);
+ $node->addChild($bp->createImportedNode($config, $nodeName));
+ }
+ } elseif ($bp->hasNode($val)) {
+ $node->addChild($bp->getNode($val));
+ } else {
+ $this->missingNodes[$val][] = $node;
+ }
+ }
+
+ $bp->addNode($name, $node);
+ }
+
+ /**
+ * @return string
+ */
+ public function getFilename()
+ {
+ return $this->currentFilename ?: '[given string]';
+ }
+
+ /**
+ * @param $msg
+ * @throws ConfigurationError
+ */
+ protected function parseError($msg)
+ {
+ throw new ConfigurationError(
+ sprintf(
+ 'Parse error on %s:%s: %s',
+ $this->getFilename(),
+ $this->currentLineNumber,
+ $msg
+ )
+ );
+ }
+}
diff --git a/library/Businessprocess/Storage/LegacyConfigRenderer.php b/library/Businessprocess/Storage/LegacyConfigRenderer.php
new file mode 100644
index 0000000..1f7e23b
--- /dev/null
+++ b/library/Businessprocess/Storage/LegacyConfigRenderer.php
@@ -0,0 +1,268 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Storage;
+
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\ImportedNode;
+
+class LegacyConfigRenderer
+{
+ /** @var array */
+ protected $renderedNodes;
+
+ protected $config;
+
+ /**
+ * LecagyConfigRenderer constructor
+ *
+ * @param BpConfig $config
+ */
+ public function __construct(BpConfig $config)
+ {
+ $this->config = $config;
+ }
+
+ /**
+ * @return string
+ */
+ public function render()
+ {
+ return $this->renderHeader() . $this->renderNodes();
+ }
+
+ /**
+ * @param BpConfig $config
+ * @return mixed
+ */
+ public static function renderConfig(BpConfig $config)
+ {
+ $renderer = new static($config);
+ return $renderer->render();
+ }
+
+ /**
+ * @return string
+ */
+ public function renderHeader()
+ {
+ $str = "### Business Process Config File ###\n#\n";
+
+ $meta = $this->config->getMetadata();
+ foreach ($meta->getProperties() as $key => $value) {
+ if ($value === null) {
+ continue;
+ }
+
+ $lineNum = 1;
+ $spaces = str_repeat(' ', strlen(sprintf("%-15s :", $key)));
+
+ foreach (preg_split('/\r?\n/', $value) as $line) {
+ if ($lineNum === 1) {
+ $str .= sprintf("# %-15s : %s\n", $key, $line);
+ } else {
+ $str .= sprintf("# %s %s\n", $spaces, $line);
+ }
+
+ $lineNum++;
+ }
+ }
+
+ $str .= "#\n###################################\n\n";
+
+ return $str;
+ }
+
+ /**
+ * @return string
+ */
+ public function renderNodes()
+ {
+ $this->renderedNodes = array();
+
+ $config = $this->config;
+ $str = '';
+
+ foreach ($config->getRootNodes() as $node) {
+ $str .= $this->requireRenderedBpNode($node);
+ }
+
+ foreach ($config->getUnboundNodes() as $name => $node) {
+ $str .= $this->requireRenderedBpNode($node);
+ }
+
+ return $str . "\n";
+ }
+
+ /**
+ * Rendered node definition, empty string if already rendered
+ *
+ * @param BpNode $node
+ *
+ * @return string
+ */
+ protected function requireRenderedBpNode(BpNode $node)
+ {
+ $name = $node->getName();
+
+ if (array_key_exists($name, $this->renderedNodes)) {
+ return '';
+ } else {
+ $this->renderedNodes[$name] = true;
+ return $this->renderBpNode($node);
+ }
+ }
+
+ /**
+ * @param BpNode $node
+ * @return string
+ */
+ protected function renderBpNode(BpNode $node)
+ {
+ $name = $node->getName();
+ // Doing this before rendering children allows us to store loops
+ $cfg = '';
+
+ foreach ($node->getChildBpNodes() as $name => $child) {
+ if ($child instanceof ImportedNode) {
+ continue;
+ }
+
+ $cfg .= $this->requireRenderedBpNode($child) . "\n";
+ }
+
+ $cfg .= static::renderSingleBpNode($node);
+
+ return $cfg;
+ }
+
+ /**
+ * @param BpNode $node
+ * @return string
+ */
+ public static function renderEqualSign(BpNode $node)
+ {
+ $op = $node->getOperator();
+ if (is_numeric($op)) {
+ return '= ' . $op . ' of:';
+ } else {
+ return '=';
+ }
+ }
+
+ /**
+ * @param BpNode $node
+ * @return string
+ */
+ public static function renderOperator(BpNode $node)
+ {
+ $op = $node->getOperator();
+ if (is_numeric($op)) {
+ return '+';
+ } else {
+ return $op;
+ }
+ }
+
+ /**
+ * @param BpNode $node
+ * @return string
+ */
+ public static function renderSingleBpNode(BpNode $node)
+ {
+ return static::renderExpression($node)
+ . static::renderStateOverrides($node)
+ . static::renderDisplay($node)
+ . static::renderInfoUrl($node);
+ }
+
+ /**
+ * @param BpNode $node
+ * @return string
+ */
+ public static function renderExpression(BpNode $node)
+ {
+ return sprintf(
+ "%s %s %s\n",
+ $node->getName(),
+ static::renderEqualSign($node),
+ static::renderChildNames($node)
+ );
+ }
+
+ /**
+ * @param BpNode $node
+ * @return string
+ */
+ public static function renderChildNames(BpNode $node)
+ {
+ $op = static::renderOperator($node);
+ $children = $node->getChildNames();
+ $str = implode(' ' . $op . ' ', array_map(function ($val) {
+ return preg_replace('~([\|\+&\!\%\^])~', '\\\\$1', $val);
+ }, $children));
+
+ if ((count($children) < 2) && $op !== '&') {
+ return $op . ' ' . $str;
+ } else {
+ return $str;
+ }
+ }
+
+ /**
+ * @param BpNode $node
+ * @return string
+ */
+ public static function renderDisplay(BpNode $node)
+ {
+ if ($node->hasAlias() || $node->getDisplay() > 0) {
+ $prio = $node->getDisplay();
+ return sprintf(
+ "display %s;%s;%s\n",
+ $prio,
+ $node->getName(),
+ $node->getAlias()
+ );
+ } else {
+ return '';
+ }
+ }
+
+ public static function renderStateOverrides(BpNode $node)
+ {
+ $stateOverrides = '';
+ foreach ($node->getStateOverrides() as $childName => $overrideRules) {
+ $overrides = [];
+ foreach ($overrideRules as $from => $to) {
+ $overrides[] = sprintf('%d-%d', $from, $to);
+ }
+
+ if (! empty($overrides)) {
+ $stateOverrides .= '!' . $childName . '|' . join(',', $overrides);
+ }
+ }
+
+ if (! $stateOverrides) {
+ return '';
+ }
+
+ return 'state_overrides ' . $node->getName() . $stateOverrides . "\n";
+ }
+
+ /**
+ * @param BpNode $node
+ * @return string
+ */
+ public static function renderInfoUrl(BpNode $node)
+ {
+ if ($node->hasInfoUrl()) {
+ return sprintf(
+ "info_url %s;%s\n",
+ $node->getName(),
+ $node->getInfoUrl()
+ );
+ } else {
+ return '';
+ }
+ }
+}
diff --git a/library/Businessprocess/Storage/LegacyStorage.php b/library/Businessprocess/Storage/LegacyStorage.php
new file mode 100644
index 0000000..f6cf1e5
--- /dev/null
+++ b/library/Businessprocess/Storage/LegacyStorage.php
@@ -0,0 +1,205 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Storage;
+
+use DirectoryIterator;
+use Icinga\Application\Hook\AuditHook;
+use Icinga\Application\Icinga;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Exception\SystemPermissionException;
+
+class LegacyStorage extends Storage
+{
+ /**
+ * All parsed configurations
+ *
+ * @var BpConfig[]
+ */
+ protected $configs = [];
+
+ /** @var string */
+ protected $configDir;
+
+ public function getConfigDir()
+ {
+ if ($this->configDir === null) {
+ $this->prepareDefaultConfigDir();
+ }
+
+ return $this->configDir;
+ }
+
+ protected function prepareDefaultConfigDir()
+ {
+ $dir = Icinga::app()
+ ->getModuleManager()
+ ->getModule('businessprocess')
+ ->getConfigDir();
+
+ // TODO: This is silly. We need Config::requireDirectory().
+ if (! is_dir($dir)) {
+ if (! is_dir(dirname($dir))) {
+ if (! @mkdir(dirname($dir))) {
+ throw new SystemPermissionException('Could not create config directory "%s"', dirname($dir));
+ }
+ }
+ if (! mkdir($dir)) {
+ throw new SystemPermissionException('Could not create config directory "%s"', $dir);
+ }
+ }
+ $dir = $dir . '/processes';
+ if (! is_dir($dir)) {
+ if (! mkdir($dir)) {
+ throw new SystemPermissionException('Could not create config directory "%s"', $dir);
+ }
+ }
+
+ $this->configDir = $dir;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function listProcesses()
+ {
+ $files = array();
+
+ foreach ($this->listAllProcessNames() as $name) {
+ $meta = $this->loadMetadata($name);
+ if (! $meta->canRead()) {
+ continue;
+ }
+
+ $files[$name] = $meta->getExtendedTitle();
+ }
+
+ return $files;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function listProcessNames()
+ {
+ $files = array();
+
+ foreach ($this->listAllProcessNames() as $name) {
+ $meta = $this->loadMetadata($name);
+ if (! $meta->canRead()) {
+ continue;
+ }
+
+ $files[$name] = $name;
+ }
+
+ return $files;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function listAllProcessNames()
+ {
+ $files = array();
+
+ foreach (new DirectoryIterator($this->getConfigDir()) as $file) {
+ if ($file->isDot()) {
+ continue;
+ }
+
+ $filename = $file->getFilename();
+ if (substr($filename, -5) === '.conf') {
+ $files[] = substr($filename, 0, -5);
+ }
+ }
+
+ natcasesort($files);
+ return $files;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function loadProcess($name)
+ {
+ if (! isset($this->configs[$name])) {
+ $this->configs[$name] = LegacyConfigParser::parseFile(
+ $name,
+ $this->getFilename($name)
+ );
+ }
+
+ return $this->configs[$name];
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function storeProcess(BpConfig $process)
+ {
+ AuditHook::logActivity('businessprocess/store', "Business Process \"{$process->getName()}\" stored");
+ file_put_contents(
+ $this->getFilename($process->getName()),
+ LegacyConfigRenderer::renderConfig($process)
+ );
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function deleteProcess($name)
+ {
+ AuditHook::logActivity('businessprocess/delete', "Business Process \"{$name}\" deleted");
+ return @unlink($this->getFilename($name));
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function loadMetadata($name)
+ {
+ if (isset($this->configs[$name])) {
+ return $this->configs[$name]->getMetadata();
+ }
+
+ return LegacyConfigParser::readMetadataFromFileHeader(
+ $name,
+ $this->getFilename($name)
+ );
+ }
+
+ public function getSource($name)
+ {
+ return file_get_contents($this->getFilename($name));
+ }
+
+ public function getFilename($name)
+ {
+ return $this->getConfigDir() . '/' . $name . '.conf';
+ }
+
+ /**
+ * @param $name
+ * @param $string
+ *
+ * @return BpConfig
+ */
+ public function loadFromString($name, $string)
+ {
+ return LegacyConfigParser::parseString($name, $string);
+ }
+
+ /**
+ * @param $name
+ * @return bool
+ */
+ public function hasProcess($name)
+ {
+ $file = $this->getFilename($name);
+ if (! is_file($file)) {
+ return false;
+ }
+
+ return $this->loadMetadata($name)->canRead();
+ }
+}
diff --git a/library/Businessprocess/Storage/Storage.php b/library/Businessprocess/Storage/Storage.php
new file mode 100644
index 0000000..c8a07ba
--- /dev/null
+++ b/library/Businessprocess/Storage/Storage.php
@@ -0,0 +1,107 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Storage;
+
+use Icinga\Application\Config;
+use Icinga\Data\ConfigObject;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\Metadata;
+
+abstract class Storage
+{
+ /**
+ * @var static
+ */
+ protected static $instance;
+
+ /**
+ * @var ConfigObject
+ */
+ protected $config;
+
+ /**
+ * Storage constructor.
+ * @param ConfigObject $config
+ */
+ public function __construct(ConfigObject $config)
+ {
+ $this->config = $config;
+ $this->init();
+ }
+
+ protected function init()
+ {
+ }
+
+ public static function getInstance()
+ {
+ if (static::$instance === null) {
+ static::$instance = new static(Config::module('businessprocess')->getSection('global'));
+ }
+
+ return static::$instance;
+ }
+
+ /**
+ * All processes readable by the current user
+ *
+ * The returned array has the form <process name> => <nice title>, sorted
+ * by title
+ *
+ * @return array
+ */
+ abstract public function listProcesses();
+
+ /**
+ * All process names readable by the current user
+ *
+ * The returned array has the form <process name> => <process name> and is
+ * sorted
+ *
+ * @return array
+ */
+ abstract public function listProcessNames();
+
+ /**
+ * All available process names, regardless of eventual restrictions
+ *
+ * @return array
+ */
+ abstract public function listAllProcessNames();
+
+ /**
+ * Whether a configuration with the given name exists
+ *
+ * @param $name
+ *
+ * @return bool
+ */
+ abstract public function hasProcess($name);
+
+ /**
+ * @param $name
+ * @return BpConfig
+ */
+ abstract public function loadProcess($name);
+
+ /**
+ * Store eventual changes applied to the given configuration
+ *
+ * @param BpConfig $config
+ *
+ * @return mixed
+ */
+ abstract public function storeProcess(BpConfig $config);
+
+ /**
+ * @param $name
+ * @return bool Whether the process has been deleted
+ */
+ abstract public function deleteProcess($name);
+
+ /**
+ * @param string $name
+ * @return Metadata
+ */
+ abstract public function loadMetadata($name);
+}
diff --git a/library/Businessprocess/Test/BaseTestCase.php b/library/Businessprocess/Test/BaseTestCase.php
new file mode 100644
index 0000000..ba32b7c
--- /dev/null
+++ b/library/Businessprocess/Test/BaseTestCase.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Test;
+
+use Icinga\Application\Config;
+use Icinga\Application\ApplicationBootstrap;
+use Icinga\Application\Icinga;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\Storage\LegacyStorage;
+
+abstract class BaseTestCase extends \Icinga\Test\BaseTestCase
+{
+ /** @var ApplicationBootstrap */
+ private static $app;
+
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->getRequestMock()->shouldReceive('getBaseUrl')->andReturn('/icingaweb2/');
+
+ $this->app()
+ ->getModuleManager()
+ ->loadModule('businessprocess');
+ }
+
+ protected function emptyConfigSection()
+ {
+ return Config::module('businessprocess')->getSection('global');
+ }
+
+ /***
+ * @return BpConfig
+ */
+ protected function makeLoop()
+ {
+ return $this->makeInstance()->loadFromString(
+ 'loop',
+ "a = b\nb = c\nc = a\nd = a"
+ );
+ }
+
+ /**
+ * @return LegacyStorage
+ */
+ protected function makeInstance()
+ {
+ return new LegacyStorage($this->emptyConfigSection());
+ }
+
+ /**
+ * @param ?string $subDir
+ * @return string
+ */
+ protected function getTestsBaseDir($subDir = null)
+ {
+ $dir = dirname(dirname(dirname(__DIR__))) . '/test';
+ if ($subDir === null) {
+ return $dir;
+ } else {
+ return $dir . '/' . ltrim($subDir, '/');
+ }
+ }
+
+ /**
+ * @return ApplicationBootstrap
+ */
+ protected function app()
+ {
+ if (self::$app === null) {
+ self::$app = Icinga::app();
+ }
+
+ return self::$app;
+ }
+}
diff --git a/library/Businessprocess/Test/Bootstrap.php b/library/Businessprocess/Test/Bootstrap.php
new file mode 100644
index 0000000..4141c16
--- /dev/null
+++ b/library/Businessprocess/Test/Bootstrap.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Test;
+
+use Icinga\Application\Cli;
+
+class Bootstrap
+{
+ public static function cli($basedir = null)
+ {
+ error_reporting(E_ALL | E_STRICT);
+ if ($basedir === null) {
+ $basedir = dirname(dirname(dirname(__DIR__)));
+ }
+ $testsDir = $basedir . '/test';
+ require_once 'Icinga/Application/Cli.php';
+
+ if (array_key_exists('ICINGAWEB_CONFIGDIR', $_SERVER)) {
+ $configDir = $_SERVER['ICINGAWEB_CONFIGDIR'];
+ } else {
+ $configDir = $testsDir . '/config';
+ }
+
+ Cli::start($testsDir, $configDir)
+ ->getModuleManager()
+ ->loadModule('ipl', $basedir . '/vendor/ipl')
+ ->loadModule('businessprocess', $basedir);
+ }
+}
diff --git a/library/Businessprocess/Web/Component/ActionBar.php b/library/Businessprocess/Web/Component/ActionBar.php
new file mode 100644
index 0000000..94458dc
--- /dev/null
+++ b/library/Businessprocess/Web/Component/ActionBar.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Component;
+
+use ipl\Html\BaseHtmlElement;
+
+class ActionBar extends BaseHtmlElement
+{
+ protected $contentSeparator = ' ';
+
+ /** @var string */
+ protected $tag = 'div';
+
+ protected $defaultAttributes = array('class' => 'action-bar');
+}
diff --git a/library/Businessprocess/Web/Component/BpDashboardTile.php b/library/Businessprocess/Web/Component/BpDashboardTile.php
new file mode 100644
index 0000000..9a4a0f6
--- /dev/null
+++ b/library/Businessprocess/Web/Component/BpDashboardTile.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Component;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use ipl\Html\Text;
+use ipl\Web\Url;
+use ipl\Web\Widget\Icon;
+use ipl\Web\Widget\Link;
+
+class BpDashboardTile extends BaseHtmlElement
+{
+ protected $tag = 'div';
+
+ protected $defaultAttributes = ['class' => 'dashboard-tile'];
+
+ public function __construct(BpConfig $bp, $title, $description, $icon, $url, $urlParams = null, $attributes = null)
+ {
+ $this->add(Html::tag(
+ 'div',
+ ['class' => 'bp-link', 'data-base-target' => '_main'],
+ (new Link(new Icon($icon), Url::fromPath($url, $urlParams ?: []), $attributes))
+ ->add(Html::tag('span', ['class' => 'header'], $title))
+ ->add($description)
+ ));
+
+ $tiles = Html::tag('div', ['class' => 'bp-root-tiles']);
+
+ foreach ($bp->getChildren() as $node) {
+ $state = strtolower($node->getStateName());
+
+ $tiles->add(Html::tag(
+ 'a',
+ [
+ 'href' => Url::fromPath($url, $urlParams ?: [])->with(['node' => $node->getName()]),
+ 'class' => "badge state-{$state}",
+ 'title' => $node->getAlias()
+ ],
+ Text::create('&nbsp;')->setEscaped()
+ ));
+ }
+
+ $this->add($tiles);
+ }
+}
diff --git a/library/Businessprocess/Web/Component/Content.php b/library/Businessprocess/Web/Component/Content.php
new file mode 100644
index 0000000..6d14197
--- /dev/null
+++ b/library/Businessprocess/Web/Component/Content.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Component;
+
+use ipl\Html\BaseHtmlElement;
+
+class Content extends BaseHtmlElement
+{
+ protected $tag = 'div';
+
+ protected $contentSeparator = "\n";
+
+ protected $defaultAttributes = array('class' => 'content');
+}
diff --git a/library/Businessprocess/Web/Component/Controls.php b/library/Businessprocess/Web/Component/Controls.php
new file mode 100644
index 0000000..259cbbb
--- /dev/null
+++ b/library/Businessprocess/Web/Component/Controls.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Component;
+
+use ipl\Html\BaseHtmlElement;
+
+class Controls extends BaseHtmlElement
+{
+ protected $tag = 'div';
+
+ protected $contentSeparator = "\n";
+
+ protected $defaultAttributes = array('class' => 'controls');
+}
diff --git a/library/Businessprocess/Web/Component/Dashboard.php b/library/Businessprocess/Web/Component/Dashboard.php
new file mode 100644
index 0000000..d211772
--- /dev/null
+++ b/library/Businessprocess/Web/Component/Dashboard.php
@@ -0,0 +1,140 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Component;
+
+use Exception;
+use Icinga\Application\Modules\Module;
+use Icinga\Authentication\Auth;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\ProvidedHook\Icingadb\IcingadbSupport;
+use Icinga\Module\Businessprocess\State\IcingaDbState;
+use Icinga\Module\Businessprocess\State\MonitoringState;
+use Icinga\Module\Businessprocess\Storage\Storage;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+
+class Dashboard extends BaseHtmlElement
+{
+ /** @var string */
+ protected $contentSeparator = "\n";
+
+ /** @var string */
+ protected $tag = 'div';
+
+ protected $defaultAttributes = array(
+ 'class' => 'overview-dashboard',
+ 'data-base-target' => '_next'
+ );
+
+ /** @var Auth */
+ protected $auth;
+
+ /** @var Storage */
+ protected $storage;
+
+ /**
+ * Dashboard constructor.
+ * @param Auth $auth
+ * @param Storage $storage
+ */
+ protected function __construct(Auth $auth, Storage $storage)
+ {
+ $this->auth = $auth;
+ $this->storage = $storage;
+ // TODO: Auth?
+ $processes = $storage->listProcessNames();
+ $this->add(
+ Html::tag('h1', null, mt('businessprocess', 'Welcome to your Business Process Overview'))
+ );
+ $this->add(Html::tag(
+ 'p',
+ null,
+ mt(
+ 'businessprocess',
+ 'From here you can reach all your defined Business Process'
+ . ' configurations, create new or modify existing ones'
+ )
+ ));
+ if ($auth->hasPermission('businessprocess/create')) {
+ $this->add(
+ new DashboardAction(
+ mt('businessprocess', 'Create'),
+ mt('businessprocess', 'Create a new Business Process configuration'),
+ 'plus',
+ 'businessprocess/process/create',
+ null,
+ array('class' => 'addnew')
+ )
+ )->add(
+ new DashboardAction(
+ mt('businessprocess', 'Upload'),
+ mt('businessprocess', 'Upload an existing Business Process configuration'),
+ 'upload',
+ 'businessprocess/process/upload',
+ null,
+ array('class' => 'addnew')
+ )
+ );
+ } elseif (empty($processes)) {
+ $this->add(
+ Html::tag('div')
+ ->add(Html::tag('h1', null, mt('businessprocess', 'Not available')))
+ ->add(Html::tag('p', null, mt(
+ 'businessprocess',
+ 'No Business Process has been defined for you'
+ )))
+ );
+ }
+
+ foreach ($processes as $name) {
+ $meta = $storage->loadMetadata($name);
+ $title = $meta->get('Title');
+
+ if ($title === null) {
+ $title = $name;
+ }
+
+ try {
+ $bp = $storage->loadProcess($name);
+ } catch (Exception $e) {
+ $this->add(new BpDashboardTile(
+ new BpConfig(),
+ $title,
+ sprintf(t('File %s has faulty config'), $name . '.conf'),
+ 'file-circle-xmark',
+ 'businessprocess/process/show',
+ ['config' => $name]
+ ));
+
+ continue;
+ }
+
+ if (Module::exists('icingadb') &&
+ (! $bp->hasBackendName() && IcingadbSupport::useIcingaDbAsBackend())
+ ) {
+ IcingaDbState::apply($bp);
+ } else {
+ MonitoringState::apply($bp);
+ }
+
+ $this->add(new BpDashboardTile(
+ $bp,
+ $title,
+ $meta->get('Description'),
+ 'sitemap',
+ 'businessprocess/process/show',
+ array('config' => $name)
+ ));
+ }
+ }
+
+ /**
+ * @param Auth $auth
+ * @param Storage $storage
+ * @return static
+ */
+ public static function create(Auth $auth, Storage $storage)
+ {
+ return new static($auth, $storage);
+ }
+}
diff --git a/library/Businessprocess/Web/Component/DashboardAction.php b/library/Businessprocess/Web/Component/DashboardAction.php
new file mode 100644
index 0000000..9bd3240
--- /dev/null
+++ b/library/Businessprocess/Web/Component/DashboardAction.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Component;
+
+use Icinga\Web\Url;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use ipl\Web\Widget\Icon;
+
+class DashboardAction extends BaseHtmlElement
+{
+ protected $tag = 'div';
+
+ protected $defaultAttributes = array('class' => 'action');
+
+ public function __construct($title, $description, $icon, $url, $urlParams = null, $attributes = null)
+ {
+ if (! isset($attributes['href'])) {
+ $attributes['href'] = Url::fromPath($url, $urlParams ?: []);
+ }
+
+ $this->add(Html::tag('a', $attributes)
+ ->add(new Icon($icon))
+ ->add(Html::tag('span', ['class' => 'header'], $title))
+ ->add($description));
+ }
+}
diff --git a/library/Businessprocess/Web/Component/RenderedProcessActionBar.php b/library/Businessprocess/Web/Component/RenderedProcessActionBar.php
new file mode 100644
index 0000000..41fa0f8
--- /dev/null
+++ b/library/Businessprocess/Web/Component/RenderedProcessActionBar.php
@@ -0,0 +1,161 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Component;
+
+use Icinga\Authentication\Auth;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\Renderer\Renderer;
+use Icinga\Module\Businessprocess\Renderer\TreeRenderer;
+use Icinga\Web\Url;
+use ipl\Html\Html;
+use ipl\Web\Widget\Icon;
+
+class RenderedProcessActionBar extends ActionBar
+{
+ public function __construct(BpConfig $config, Renderer $renderer, Url $url)
+ {
+ $meta = $config->getMetadata();
+
+ if ($renderer instanceof TreeRenderer) {
+ $link = Html::tag(
+ 'a',
+ [
+ 'href' => $url->with('mode', 'tile'),
+ 'title' => mt('businessprocess', 'Switch to Tile view')
+ ]
+ );
+ } else {
+ $link = Html::tag(
+ 'a',
+ [
+ 'href' => $url->with('mode', 'tree'),
+ 'title' => mt('businessprocess', 'Switch to Tree view')
+ ]
+ );
+ }
+
+ $link->add([
+ new Icon('grip', ['class' => $renderer instanceof TreeRenderer ? null : 'active']),
+ new Icon('sitemap', ['class' => $renderer instanceof TreeRenderer ? 'active' : null])
+ ]);
+
+ $this->add(
+ Html::tag('div', ['class' => 'view-toggle'])
+ ->add(Html::tag('span', null, mt('businessprocess', 'View')))
+ ->add($link)
+ );
+
+ $this->add(Html::tag(
+ 'a',
+ [
+ 'data-base-target' => '_main',
+ 'href' => $url->with('showFullscreen', true),
+ 'title' => mt('businessprocess', 'Switch to fullscreen mode'),
+ ],
+ [
+ new Icon('maximize'),
+ mt('businessprocess', 'Fullscreen')
+ ]
+ ));
+
+ $hasChanges = $config->hasSimulations() || $config->hasBeenChanged();
+
+ if ($renderer->isLocked()) {
+ if (! $renderer->wantsRootNodes() && $renderer->rendersImportedNode()) {
+ $span = Html::tag('span', [
+ 'class' => 'disabled',
+ 'title' => mt(
+ 'businessprocess',
+ 'Imported processes can only be changed in their original configuration'
+ )
+ ]);
+ $span->add([new Icon('lock'), mt('businessprocess', 'Editing Locked')]);
+ $this->add($span);
+ } else {
+ $this->add(Html::tag(
+ 'a',
+ [
+ 'href' => $url->with('unlocked', true),
+ 'title' => mt('businessprocess', 'Click to unlock editing for this process'),
+ ],
+ [
+ new Icon('lock'),
+ mt('businessprocess', 'Unlock Editing')
+ ]
+ ));
+ }
+ } elseif (! $hasChanges) {
+ $this->add(Html::tag(
+ 'a',
+ [
+ 'href' => $url->without('unlocked')->without('action'),
+ 'title' => mt('businessprocess', 'Click to lock editing for this process'),
+ ],
+ [
+ new Icon('lock-open'),
+ mt('businessprocess', 'Lock Editing')
+ ]
+ ));
+ }
+
+ if (($hasChanges || ! $renderer->isLocked()) && $meta->canModify()) {
+ if ($renderer->wantsRootNodes()) {
+ $this->add(Html::tag(
+ 'a',
+ [
+ 'data-base-target' => '_next',
+ 'href' => Url::fromPath('businessprocess/process/config', $this->currentProcessParams($url)),
+ 'title' => mt('businessprocess', 'Modify this process'),
+ ],
+ [
+ new Icon('wrench'),
+ mt('businessprocess', 'Config')
+ ]
+ ));
+ } else {
+ $this->add(Html::tag(
+ 'a',
+ [
+ 'href' => $url->with([
+ 'action' => 'edit',
+ 'editnode' => $url->getParam('node')
+ ])->getAbsoluteUrl(),
+ 'title' => mt('businessprocess', 'Modify this process'),
+ ],
+ [
+ new Icon('wrench'),
+ mt('businessprocess', 'Config')
+ ]
+ ));
+ }
+ }
+
+ if (($hasChanges || (! $renderer->isLocked())) && $meta->canModify()) {
+ $this->add(Html::tag(
+ 'a',
+ [
+ 'href' => $url->with('action', 'add'),
+ 'title' => mt('businessprocess', 'Add a new business process node'),
+ 'class' => 'button-link'
+ ],
+ [
+ new Icon('plus'),
+ mt('businessprocess', 'Add Node')
+ ]
+ ));
+ }
+ }
+
+ protected function currentProcessParams(Url $url)
+ {
+ $urlParams = $url->getParams();
+ $params = array();
+ foreach (array('config', 'node') as $name) {
+ if ($value = $urlParams->get($name)) {
+ $params[$name] = $value;
+ }
+ }
+
+ return $params;
+ }
+}
diff --git a/library/Businessprocess/Web/Component/Tabs.php b/library/Businessprocess/Web/Component/Tabs.php
new file mode 100644
index 0000000..aaa444e
--- /dev/null
+++ b/library/Businessprocess/Web/Component/Tabs.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Component;
+
+use ipl\Html\ValidHtml;
+
+class Tabs extends WtfTabs implements ValidHtml
+{
+}
diff --git a/library/Businessprocess/Web/Component/WtfTabs.php b/library/Businessprocess/Web/Component/WtfTabs.php
new file mode 100644
index 0000000..8f2250f
--- /dev/null
+++ b/library/Businessprocess/Web/Component/WtfTabs.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Component;
+
+use Icinga\Web\Widget\Tabs;
+
+/**
+ * Class WtfTabs
+ *
+ * TODO: Please remove this as soon as we drop support for PHP 5.3.x
+ * This works around https://bugs.php.net/bug.php?id=43200 and fixes
+ * https://github.com/Icinga/icingaweb2-module-businessprocess/issues/81
+ *
+ * @package Icinga\Module\Businessprocess\Web\Component
+ */
+class WtfTabs extends Tabs
+{
+ public function render()
+ {
+ return parent::render();
+ }
+}
diff --git a/library/Businessprocess/Web/Controller.php b/library/Businessprocess/Web/Controller.php
new file mode 100644
index 0000000..43200cc
--- /dev/null
+++ b/library/Businessprocess/Web/Controller.php
@@ -0,0 +1,262 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web;
+
+use Icinga\Application\Icinga;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\Modification\ProcessChanges;
+use Icinga\Module\Businessprocess\Storage\LegacyStorage;
+use Icinga\Module\Businessprocess\Storage\Storage;
+use Icinga\Module\Businessprocess\Web\Component\ActionBar;
+use Icinga\Module\Businessprocess\Web\Component\Controls;
+use Icinga\Module\Businessprocess\Web\Component\Content;
+use Icinga\Module\Businessprocess\Web\Component\Tabs;
+use Icinga\Module\Businessprocess\Web\Form\FormLoader;
+use Icinga\Web\Notification;
+use Icinga\Web\View;
+use ipl\Html\Html;
+use ipl\Web\Compat\CompatController;
+
+class Controller extends CompatController
+{
+ /** @var View */
+ public $view;
+
+ /** @var BpConfig */
+ protected $bp;
+
+ /** @var Tabs */
+ protected $mytabs;
+
+ /** @var Storage */
+ private $storage;
+
+ /** @var bool */
+ protected $showFullscreen;
+
+ /** @var Url */
+ private $url;
+
+ public function init()
+ {
+ $m = Icinga::app()->getModuleManager();
+ if (! $m->hasLoaded('monitoring') && $m->hasInstalled('monitoring')) {
+ $m->loadModule('monitoring');
+ }
+ $this->controls();
+ $this->content();
+ $this->url();
+ $this->view->showFullscreen
+ = $this->showFullscreen
+ = (bool) $this->_helper->layout()->showFullscreen;
+
+ $this->setViewScript('default');
+ }
+
+ /**
+ * @return Url
+ */
+ protected function url()
+ {
+ if ($this->url === null) {
+ $this->url = Url::fromPath(
+ $this->getRequest()->getUrl()->getPath()
+ )->setParams($this->getRequest()->getUrl()->getParams());
+ }
+
+ return $this->url;
+ }
+
+ /**
+ * @return ActionBar
+ */
+ protected function actions()
+ {
+ if ($this->view->actions === null) {
+ $this->view->actions = new ActionBar();
+ }
+
+ return $this->view->actions;
+ }
+
+ /**
+ * @return Controls
+ */
+ protected function controls()
+ {
+ if ($this->view->controls === null) {
+ $controls = $this->view->controls = new Controls();
+ if ($this->view->compact) {
+ $controls->getAttributes()->add('class', 'compact');
+ }
+ }
+
+ return $this->view->controls;
+ }
+
+ /**
+ * @return Content
+ */
+ protected function content()
+ {
+ if ($this->view->content === null) {
+ $content = $this->view->content = new Content();
+ if ($this->view->compact) {
+ $content->getAttributes()->add('class', 'compact');
+ }
+ }
+
+ return $this->view->content;
+ }
+
+ /**
+ * @param $label
+ * @return Tabs
+ */
+ protected function singleTab($label)
+ {
+ return $this->tabs()->add(
+ 'tab',
+ array(
+ 'label' => $label,
+ 'url' => $this->getRequest()->getUrl()
+ )
+ )->activate('tab');
+ }
+
+ /**
+ * @return Tabs
+ */
+ protected function defaultTab()
+ {
+ return $this->singleTab($this->translate('Business Process'));
+ }
+
+ /**
+ * @return Tabs
+ */
+ protected function overviewTab()
+ {
+ return $this->tabs()->add(
+ 'overview',
+ array(
+ 'label' => $this->translate('Business Process'),
+ 'url' => 'businessprocess'
+ )
+ )->activate('overview');
+ }
+
+ /**
+ * @return Tabs
+ */
+ protected function tabs()
+ {
+ // Todo: do not add to view once all of them render controls()
+ if ($this->mytabs === null) {
+ $tabs = new Tabs();
+ //$this->controls()->add($tabs);
+ $this->mytabs = $tabs;
+ }
+
+ return $this->mytabs;
+ }
+
+ protected function session()
+ {
+ return $this->Window()->getSessionNamespace('businessprocess');
+ }
+
+ protected function setViewScript($name)
+ {
+ $this->_helper->viewRenderer->setNoController(true);
+ $this->_helper->viewRenderer->setScriptAction($name);
+ return $this;
+ }
+
+ protected function addTitle($title)
+ {
+ $args = func_get_args();
+ array_shift($args);
+ $this->view->title = vsprintf($title, $args);
+ $this->controls()->add(Html::tag('h1', null, $this->view->title));
+ return $this;
+ }
+
+ protected function loadModifiedBpConfig()
+ {
+ $bp = $this->loadBpConfig();
+ $changes = ProcessChanges::construct($bp, $this->session());
+ if ($this->params->get('dismissChanges')) {
+ Notification::success(
+ sprintf(
+ $this->translate('%d pending change(s) have been dropped'),
+ $changes->count()
+ )
+ );
+ $changes->clear();
+ $this->redirectNow($this->url()->without('dismissChanges')->without('unlocked'));
+ }
+ $bp->applyChanges($changes);
+ return $bp;
+ }
+
+ protected function doNotRender()
+ {
+ $this->_helper->layout()->disableLayout();
+ $this->_helper->viewRenderer->setNoRender(true);
+ return $this;
+ }
+
+ protected function loadBpConfig()
+ {
+ $name = $this->params->get('config');
+ /** @var LegacyStorage $storage */
+ $storage = $this->storage();
+
+ if (! $storage->hasProcess($name)) {
+ $this->httpNotFound(
+ $this->translate('No such process config: "%s"'),
+ $name
+ );
+ }
+
+ $modifications = $this->session()->get('modifications', array());
+ if (array_key_exists($name, $modifications)) {
+ $bp = $storage->loadFromString($name, $modifications[$name]);
+ } else {
+ $bp = $storage->loadProcess($name);
+ }
+
+ // allow URL parameter to override configured state type
+ if (null !== ($stateType = $this->params->get('state_type'))) {
+ if ($stateType === 'soft') {
+ $bp->useSoftStates();
+ }
+ if ($stateType === 'hard') {
+ $bp->useHardStates();
+ }
+ }
+
+ $this->view->bpconfig = $this->bp = $bp;
+ $this->view->configName = $bp->getName();
+
+ return $bp;
+ }
+
+ public function loadForm($name)
+ {
+ return FormLoader::load($name, $this->Module());
+ }
+
+ /**
+ * @return LegacyStorage
+ */
+ protected function storage()
+ {
+ if ($this->storage === null) {
+ $this->storage = LegacyStorage::getInstance();
+ }
+
+ return $this->storage;
+ }
+}
diff --git a/library/Businessprocess/Web/FakeRequest.php b/library/Businessprocess/Web/FakeRequest.php
new file mode 100644
index 0000000..4e54117
--- /dev/null
+++ b/library/Businessprocess/Web/FakeRequest.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web;
+
+use Icinga\Exception\ProgrammingError;
+use Icinga\Web\Request;
+
+class FakeRequest extends Request
+{
+ /** @var string */
+ private static $baseUrl;
+
+ public static function setConfiguredBaseUrl($url)
+ {
+ self::$baseUrl = $url;
+ }
+
+ public function getBaseUrl($raw = false)
+ {
+ if (self::$baseUrl === null) {
+ throw new ProgrammingError('Cannot determine base URL on CLI if not configured');
+ } else {
+ return self::$baseUrl;
+ }
+ }
+}
diff --git a/library/Businessprocess/Web/Form/BpConfigBaseForm.php b/library/Businessprocess/Web/Form/BpConfigBaseForm.php
new file mode 100644
index 0000000..5ccdf06
--- /dev/null
+++ b/library/Businessprocess/Web/Form/BpConfigBaseForm.php
@@ -0,0 +1,135 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Form;
+
+use Icinga\Application\Config;
+use Icinga\Application\Icinga;
+use Icinga\Authentication\Auth;
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\Storage\Storage;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+use Icinga\Web\Session\SessionNamespace;
+use ipl\Sql\Connection as IcingaDbConnection;
+
+abstract class BpConfigBaseForm extends QuickForm
+{
+ /** @var Storage */
+ protected $storage;
+
+ /** @var BpConfig */
+ protected $bp;
+
+ /** @var MonitoringBackend|IcingaDbConnection*/
+ protected $backend;
+
+ /** @var SessionNamespace */
+ protected $session;
+
+ protected function listAvailableBackends()
+ {
+ $keys = [];
+ $moduleManager = Icinga::app()->getModuleManager();
+ if ($moduleManager->hasEnabled('monitoring')) {
+ $keys = array_keys(Config::module('monitoring', 'backends')->toArray());
+ $keys = array_combine($keys, $keys);
+ }
+
+ return $keys;
+ }
+
+ /**
+ * Set the storage to use
+ *
+ * @param Storage $storage
+ *
+ * @return $this
+ */
+ public function setStorage(Storage $storage): self
+ {
+ $this->storage = $storage;
+
+ return $this;
+ }
+
+ /**
+ * Set the config to use
+ *
+ * @param BpConfig $config
+ *
+ * @return $this
+ */
+ public function setProcess(BpConfig $config): self
+ {
+ $this->bp = $config;
+ $this->setBackend($config->getBackend());
+
+ return $this;
+ }
+
+ /**
+ * Set the backend to use
+ *
+ * @param MonitoringBackend|IcingaDbConnection $backend
+ *
+ * @return $this
+ */
+ public function setBackend($backend): self
+ {
+ $this->backend = $backend;
+
+ return $this;
+ }
+
+ /**
+ * Set the session namespace to use
+ *
+ * @param SessionNamespace $session
+ *
+ * @return $this
+ */
+ public function setSession(SessionNamespace $session): self
+ {
+ $this->session = $session;
+
+ return $this;
+ }
+
+ protected function prepareMetadata(BpConfig $config)
+ {
+ $meta = $config->getMetadata();
+ $auth = Auth::getInstance();
+ $meta->set('Owner', $auth->getUser()->getUsername());
+
+ if ($auth->hasPermission('businessprocess/showall')) {
+ return true;
+ }
+
+ $prefixes = $auth->getRestrictions('businessprocess/prefix');
+ if (! empty($prefixes) && ! $meta->nameIsPrefixedWithOneOf($prefixes)) {
+ if (count($prefixes) === 1) {
+ $this->getElement('name')->addError(sprintf(
+ $this->translate('Please prefix the name with "%s"'),
+ current($prefixes)
+ ));
+ } else {
+ $this->getElement('name')->addError(sprintf(
+ $this->translate('Please prefix the name with one of "%s"'),
+ implode('", "', $prefixes)
+ ));
+ }
+
+ return false;
+ }
+
+ return true;
+ }
+
+ protected function setPreferredDecorators()
+ {
+ parent::setPreferredDecorators();
+
+ $this->setAttrib('class', $this->getAttrib('class') . ' bp-form');
+
+ return $this;
+ }
+}
diff --git a/library/Businessprocess/Web/Form/CsrfToken.php b/library/Businessprocess/Web/Form/CsrfToken.php
new file mode 100644
index 0000000..9eb24ef
--- /dev/null
+++ b/library/Businessprocess/Web/Form/CsrfToken.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Form;
+
+class CsrfToken
+{
+ /**
+ * Check whether the given token is valid
+ *
+ * @param string $token Token
+ *
+ * @return bool
+ */
+ public static function isValid($token)
+ {
+ if (strpos($token, '|') === false) {
+ return false;
+ }
+
+ list($seed, $token) = explode('|', $token);
+
+ if (!is_numeric($seed)) {
+ return false;
+ }
+
+ return $token === hash('sha256', self::getSessionId() . $seed);
+ }
+
+ /**
+ * Create a new token
+ *
+ * @return string
+ */
+ public static function generate()
+ {
+ $seed = mt_rand();
+ $token = hash('sha256', self::getSessionId() . $seed);
+
+ return sprintf('%s|%s', $seed, $token);
+ }
+
+ /**
+ * Get current session id
+ *
+ * TODO: we should do this through our App or Session object
+ *
+ * @return string
+ */
+ protected static function getSessionId()
+ {
+ return session_id();
+ }
+}
diff --git a/library/Businessprocess/Web/Form/Element/Checkbox.php b/library/Businessprocess/Web/Form/Element/Checkbox.php
new file mode 100644
index 0000000..7975b82
--- /dev/null
+++ b/library/Businessprocess/Web/Form/Element/Checkbox.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Form\Element;
+
+class Checkbox extends \Icinga\Web\Form\Element\Checkbox
+{
+
+}
diff --git a/library/Businessprocess/Web/Form/Element/FormElement.php b/library/Businessprocess/Web/Form/Element/FormElement.php
new file mode 100644
index 0000000..7647a5e
--- /dev/null
+++ b/library/Businessprocess/Web/Form/Element/FormElement.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Form\Element;
+
+use Zend_Form_Element_Xhtml;
+
+class FormElement extends Zend_Form_Element_Xhtml
+{
+}
diff --git a/library/Businessprocess/Web/Form/Element/IplStateOverrides.php b/library/Businessprocess/Web/Form/Element/IplStateOverrides.php
new file mode 100644
index 0000000..5b9ea16
--- /dev/null
+++ b/library/Businessprocess/Web/Form/Element/IplStateOverrides.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Form\Element;
+
+use ipl\Html\Attributes;
+use ipl\Html\FormElement\FieldsetElement;
+
+class IplStateOverrides extends FieldsetElement
+{
+ /** @var array */
+ protected $options = [];
+
+ /**
+ * Set the options show
+ *
+ * @param array $options
+ *
+ * @return $this
+ */
+ public function setOptions(array $options): self
+ {
+ $this->options = $options;
+
+ return $this;
+ }
+
+ /**
+ * Get the options to show
+ *
+ * @return array
+ */
+ public function getOptions(): array
+ {
+ return $this->options;
+ }
+
+ public function getValues()
+ {
+ $cleanedValue = parent::getValues();
+
+ if (! empty($cleanedValue)) {
+ foreach ($cleanedValue as $from => $to) {
+ if ((int) $from === (int) $to) {
+ unset($cleanedValue[$from]);
+ }
+ }
+ }
+
+ return $cleanedValue;
+ }
+
+ protected function assemble()
+ {
+ $states = $this->getOptions();
+ foreach ($states as $state => $label) {
+ if ($state === 0) {
+ continue;
+ }
+
+ $this->addElement('select', $state, [
+ 'label' => $label,
+ 'value' => $state,
+ 'options' => [$state => $this->translate('Keep actual state')] + $states
+ ]);
+ }
+ }
+
+ protected function registerAttributeCallbacks(Attributes $attributes)
+ {
+ parent::registerAttributeCallbacks($attributes);
+
+ $this->getAttributes()
+ ->registerAttributeCallback('options', null, [$this, 'setOptions']);
+ }
+}
diff --git a/library/Businessprocess/Web/Form/Element/SimpleNote.php b/library/Businessprocess/Web/Form/Element/SimpleNote.php
new file mode 100644
index 0000000..9f757f2
--- /dev/null
+++ b/library/Businessprocess/Web/Form/Element/SimpleNote.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Form\Element;
+
+class SimpleNote extends FormElement
+{
+ public $helper = 'formSimpleNote';
+
+ /**
+ * Always ignore this element
+ * @codingStandardsIgnoreStart
+ *
+ * @var boolean
+ */
+ protected $_ignore = true;
+ // @codingStandardsIgnoreEnd
+
+ public function isValid($value, $context = null)
+ {
+ return true;
+ }
+}
diff --git a/library/Businessprocess/Web/Form/FormLoader.php b/library/Businessprocess/Web/Form/FormLoader.php
new file mode 100644
index 0000000..0cc5389
--- /dev/null
+++ b/library/Businessprocess/Web/Form/FormLoader.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Form;
+
+use Icinga\Application\Icinga;
+use Icinga\Application\Modules\Module;
+use Icinga\Exception\ProgrammingError;
+
+class FormLoader
+{
+ public static function load($name, Module $module = null)
+ {
+ if ($module === null) {
+ $basedir = Icinga::app()->getApplicationDir('forms');
+ $ns = '\\Icinga\\Web\\Forms\\';
+ } else {
+ $basedir = $module->getFormDir();
+ $ns = '\\Icinga\\Module\\' . ucfirst($module->getName()) . '\\Forms\\';
+ }
+
+ $file = null;
+ if (preg_match('~^[a-z0-9/]+$~i', $name)) {
+ $parts = preg_split('~/~', $name);
+ $class = ucfirst(array_pop($parts)) . 'Form';
+ $file = sprintf('%s/%s/%s.php', rtrim($basedir, '/'), implode('/', $parts), $class);
+ if (file_exists($file)) {
+ require_once($file);
+ $class = $ns . $class;
+ $options = array();
+ if ($module !== null) {
+ $options['icingaModule'] = $module;
+ }
+
+ return new $class($options);
+ }
+ }
+ throw new ProgrammingError(sprintf('Cannot load %s (%s), no such form', $name, $file));
+ }
+}
diff --git a/library/Businessprocess/Web/Form/QuickBaseForm.php b/library/Businessprocess/Web/Form/QuickBaseForm.php
new file mode 100644
index 0000000..36d134f
--- /dev/null
+++ b/library/Businessprocess/Web/Form/QuickBaseForm.php
@@ -0,0 +1,166 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Form;
+
+use Icinga\Application\Icinga;
+use Icinga\Application\Modules\Module;
+use ipl\Html\ValidHtml;
+use Zend_Form;
+
+abstract class QuickBaseForm extends Zend_Form implements ValidHtml
+{
+ /**
+ * The Icinga module this form belongs to. Usually only set if the
+ * form is initialized through the FormLoader
+ *
+ * @var ?Module
+ */
+ protected $icingaModule;
+
+ protected $icingaModuleName;
+
+ private $hintCount = 0;
+
+ public function __construct($options = null)
+ {
+ $this->callZfConstructor($this->handleOptions($options))
+ ->initializePrefixPaths();
+ }
+
+ protected function callZfConstructor($options = null)
+ {
+ parent::__construct($options);
+ return $this;
+ }
+
+ protected function initializePrefixPaths()
+ {
+ $this->addPrefixPathsForBusinessprocess();
+ if ($this->icingaModule && $this->icingaModuleName !== 'businessprocess') {
+ $this->addPrefixPathsForModule($this->icingaModule);
+ }
+ }
+
+ protected function addPrefixPathsForBusinessprocess()
+ {
+ $module = Icinga::app()
+ ->getModuleManager()
+ ->loadModule('businessprocess')
+ ->getModule('businessprocess');
+
+ $this->addPrefixPathsForModule($module);
+ }
+
+ public function addPrefixPathsForModule(Module $module)
+ {
+ $basedir = sprintf(
+ '%s/%s/Web/Form',
+ $module->getLibDir(),
+ ucfirst($module->getName())
+ );
+
+ $this->addPrefixPaths(array(
+ array(
+ 'prefix' => __NAMESPACE__ . '\\Element\\',
+ 'path' => $basedir . '/Element',
+ 'type' => static::ELEMENT
+ )
+ ));
+
+ return $this;
+ }
+
+ public function addHidden($name, $value = null)
+ {
+ $this->addElement('hidden', $name);
+ $el = $this->getElement($name);
+ $el->setDecorators(array('ViewHelper'));
+ if ($value !== null) {
+ $this->setDefault($name, $value);
+ $el->setValue($value);
+ }
+
+ return $this;
+ }
+
+ // TODO: Should be an element
+ public function addHtmlHint($html, $options = array())
+ {
+ return $this->addHtml('<div class="hint">' . $html . '</div>', $options);
+ }
+
+ public function addHtml($html, $options = array())
+ {
+ if (array_key_exists('name', $options)) {
+ $name = $options['name'];
+ unset($options['name']);
+ } else {
+ $name = '_HINT' . ++$this->hintCount;
+ }
+
+ $this->addElement('simpleNote', $name, $options);
+ $this->getElement($name)
+ ->setValue($html)
+ ->setIgnore(true)
+ ->setDecorators(array('ViewHelper'));
+
+ return $this;
+ }
+
+ public function optionalEnum($enum, $nullLabel = null)
+ {
+ if ($nullLabel === null) {
+ $nullLabel = $this->translate('- please choose -');
+ }
+
+ return array(null => $nullLabel) + $enum;
+ }
+
+ protected function handleOptions($options = null)
+ {
+ if ($options === null) {
+ return $options;
+ }
+
+ if (array_key_exists('icingaModule', $options)) {
+ $this->icingaModule = $options['icingaModule'];
+ $this->icingaModuleName = $this->icingaModule->getName();
+ unset($options['icingaModule']);
+ }
+
+ return $options;
+ }
+
+ public function setIcingaModule(Module $module)
+ {
+ $this->icingaModule = $module;
+ return $this;
+ }
+
+ protected function loadForm($name, Module $module = null)
+ {
+ if ($module === null) {
+ $module = $this->icingaModule;
+ }
+
+ return FormLoader::load($name, $module);
+ }
+
+ protected function valueIsEmpty($value)
+ {
+ if (is_array($value)) {
+ return empty($value);
+ }
+
+ return strlen($value) === 0;
+ }
+
+ public function translate($string)
+ {
+ if ($this->icingaModuleName === null) {
+ return t($string);
+ } else {
+ return mt($this->icingaModuleName, $string);
+ }
+ }
+}
diff --git a/library/Businessprocess/Web/Form/QuickForm.php b/library/Businessprocess/Web/Form/QuickForm.php
new file mode 100644
index 0000000..cb4d287
--- /dev/null
+++ b/library/Businessprocess/Web/Form/QuickForm.php
@@ -0,0 +1,514 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Form;
+
+use Icinga\Application\Icinga;
+use Icinga\Application\Web;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Web\Notification;
+use Icinga\Web\Request;
+use Icinga\Web\Response;
+use Icinga\Web\Url;
+use Exception;
+
+/**
+ * QuickForm wants to be a base class for simple forms
+ */
+abstract class QuickForm extends QuickBaseForm
+{
+ const ID = '__FORM_NAME';
+
+ const CSRF = '__FORM_CSRF';
+
+ /**
+ * The name of this form
+ */
+ protected $formName;
+
+ /**
+ * Whether the form has been sent
+ */
+ protected $hasBeenSent;
+
+ /**
+ * Whether the form has been sent
+ */
+ protected $hasBeenSubmitted;
+
+ /**
+ * The submit caption, element - still tbd
+ */
+ // protected $submit;
+
+ /**
+ * Our request
+ */
+ protected $request;
+
+ /**
+ * @var ?Url
+ */
+ protected $successUrl;
+
+ protected $successMessage;
+
+ protected $submitLabel;
+
+ protected $submitButtonName;
+
+ protected $deleteButtonName;
+
+ protected $fakeSubmitButtonName;
+
+ /**
+ * Whether form elements have already been created
+ */
+ protected $didSetup = false;
+
+ protected $isApiRequest = false;
+
+ public function __construct($options = null)
+ {
+ parent::__construct($options);
+
+ $this->setMethod('post');
+ $this->getActionFromRequest()
+ ->createIdElement()
+ ->regenerateCsrfToken()
+ ->setPreferredDecorators();
+ }
+
+ protected function getActionFromRequest()
+ {
+ $this->setAction(Url::fromRequest());
+ return $this;
+ }
+
+ protected function setPreferredDecorators()
+ {
+ $this->setAttrib('class', 'autofocus icinga-controls');
+ $this->setDecorators(
+ array(
+ 'Description',
+ array('FormErrors', array('onlyCustomFormErrors' => true)),
+ 'FormElements',
+ 'Form'
+ )
+ );
+
+ return $this;
+ }
+
+ protected function addSubmitButtonIfSet()
+ {
+ if (false === ($label = $this->getSubmitLabel())) {
+ return;
+ }
+
+ if ($this->submitButtonName && $el = $this->getElement($this->submitButtonName)) {
+ return;
+ }
+
+ $el = $this->createElement('submit', $label)
+ ->setLabel($label)
+ ->setDecorators(array('ViewHelper'));
+ $this->submitButtonName = $el->getName();
+ $this->addElement($el);
+
+ $fakeEl = $this->createElement('submit', '_FAKE_SUBMIT')
+ ->setLabel($label)
+ ->setDecorators(array('ViewHelper'));
+ $this->fakeSubmitButtonName = $fakeEl->getName();
+ $this->addElement($fakeEl);
+
+ $this->addDisplayGroup(
+ array($this->fakeSubmitButtonName),
+ 'fake_button',
+ array(
+ 'decorators' => array('FormElements'),
+ 'order' => 1,
+ )
+ );
+
+ $grp = array(
+ $this->submitButtonName,
+ $this->deleteButtonName
+ );
+ $this->addDisplayGroup($grp, 'buttons', array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'dl')),
+ 'DtDdWrapper',
+ ),
+ 'order' => 1000,
+ ));
+ }
+
+ protected function addSimpleDisplayGroup($elements, $name, $options)
+ {
+ if (! array_key_exists('decorators', $options)) {
+ $options['decorators'] = array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'dl')),
+ 'Fieldset',
+ );
+ }
+
+ return $this->addDisplayGroup($elements, $name, $options);
+ }
+
+ protected function createIdElement()
+ {
+ $this->detectName();
+ $this->addHidden(self::ID, $this->getName());
+ $this->getElement(self::ID)->setIgnore(true);
+ return $this;
+ }
+
+ public function getSentValue($name, $default = null)
+ {
+ $request = $this->getRequest();
+ if ($request->isPost() && $this->hasBeenSent()) {
+ return $request->getPost($name);
+ } else {
+ return $default;
+ }
+ }
+
+ public function getSubmitLabel()
+ {
+ if ($this->submitLabel === null) {
+ return $this->translate('Submit');
+ }
+
+ return $this->submitLabel;
+ }
+
+ public function setSubmitLabel($label)
+ {
+ $this->submitLabel = $label;
+ return $this;
+ }
+
+ public function setApiRequest($isApiRequest = true)
+ {
+ $this->isApiRequest = $isApiRequest;
+ return $this;
+ }
+
+ public function isApiRequest()
+ {
+ return $this->isApiRequest;
+ }
+
+ public function regenerateCsrfToken()
+ {
+ if (! $element = $this->getElement(self::CSRF)) {
+ $this->addHidden(self::CSRF, CsrfToken::generate());
+ $element = $this->getElement(self::CSRF);
+ }
+ $element->setIgnore(true);
+
+ return $this;
+ }
+
+ public function removeCsrfToken()
+ {
+ $this->removeElement(self::CSRF);
+ return $this;
+ }
+
+ public function setSuccessUrl($url, $params = null)
+ {
+ if (! $url instanceof Url) {
+ $url = Url::fromPath($url);
+ }
+ if ($params !== null) {
+ $url->setParams($params);
+ }
+ $this->successUrl = $url;
+ return $this;
+ }
+
+ public function getSuccessUrl()
+ {
+ $url = $this->successUrl ?: $this->getAction();
+ if (! $url instanceof Url) {
+ $url = Url::fromPath($url);
+ }
+
+ return $url;
+ }
+
+ protected function beforeSetup()
+ {
+ }
+
+ public function setup()
+ {
+ }
+
+ protected function onSetup()
+ {
+ }
+
+ /**
+ * @param $action string|Url
+ * @return $this
+ */
+ public function setAction($action)
+ {
+ if ($action instanceof Url) {
+ $action = $action->getAbsoluteUrl('&');
+ }
+
+ return parent::setAction($action);
+ }
+
+ public function hasBeenSubmitted()
+ {
+ if ($this->hasBeenSubmitted === null) {
+ $req = $this->getRequest();
+ if ($req->isPost()) {
+ if (! $this->hasSubmitButton()) {
+ return $this->hasBeenSubmitted = $this->hasBeenSent();
+ }
+
+ $this->hasBeenSubmitted = $this->pressedButton(
+ $this->fakeSubmitButtonName,
+ $this->getSubmitLabel()
+ ) || $this->pressedButton(
+ $this->submitButtonName,
+ $this->getSubmitLabel()
+ );
+ } else {
+ $this->hasBeenSubmitted = false;
+ }
+ }
+
+ return $this->hasBeenSubmitted;
+ }
+
+ protected function hasSubmitButton()
+ {
+ return $this->submitButtonName !== null;
+ }
+
+ protected function pressedButton($name, $label)
+ {
+ $req = $this->getRequest();
+ if (! $req->isPost()) {
+ return false;
+ }
+
+ $req = $this->getRequest();
+ $post = $req->getPost();
+
+ return array_key_exists($name, $post)
+ && $post[$name] === $label;
+ }
+
+ protected function beforeValidation($data = array())
+ {
+ }
+
+ public function prepareElements()
+ {
+ if (! $this->didSetup) {
+ $this->beforeSetup();
+ $this->setup();
+ $this->addSubmitButtonIfSet();
+ $this->onSetup();
+ $this->didSetup = true;
+ }
+
+ return $this;
+ }
+
+ public function handleRequest(Request $request = null)
+ {
+ if ($request === null) {
+ $request = $this->getRequest();
+ } else {
+ $this->setRequest($request);
+ }
+
+ $this->prepareElements();
+
+ if ($this->hasBeenSent()) {
+ $post = $request->getPost();
+ if ($this->hasBeenSubmitted()) {
+ $this->beforeValidation($post);
+ if ($this->isValid($post)) {
+ try {
+ $this->onSuccess();
+ } catch (Exception $e) {
+ $this->addException($e);
+ $this->onFailure();
+ }
+ } else {
+ $this->onFailure();
+ }
+ } else {
+ $this->setDefaults($post);
+ }
+ } else {
+ // Well...
+ }
+
+ return $this;
+ }
+
+ public function addException(Exception $e, $elementName = null)
+ {
+ $file = preg_split('/[\/\\\]/', $e->getFile(), -1, PREG_SPLIT_NO_EMPTY);
+ $file = array_pop($file);
+ $msg = sprintf(
+ '%s (%s:%d)',
+ $e->getMessage(),
+ $file,
+ $e->getLine()
+ );
+
+ if ($el = $this->getElement($elementName)) {
+ $el->addError($msg);
+ } else {
+ $this->addError($msg);
+ }
+ }
+
+ public function onSuccess()
+ {
+ $this->redirectOnSuccess();
+ }
+
+ public function setSuccessMessage($message)
+ {
+ $this->successMessage = $message;
+ return $this;
+ }
+
+ public function getSuccessMessage($message = null)
+ {
+ if ($message !== null) {
+ return $message;
+ }
+ if ($this->successMessage === null) {
+ return t('Form has successfully been sent');
+ }
+ return $this->successMessage;
+ }
+
+ public function redirectOnSuccess($message = null)
+ {
+ if ($this->isApiRequest()) {
+ // TODO: Set the status line message?
+ $this->successMessage = $this->getSuccessMessage($message);
+ return;
+ }
+
+ $url = $this->getSuccessUrl();
+ $this->notifySuccess($this->getSuccessMessage($message));
+ $this->redirectAndExit($url);
+ }
+
+ public function onFailure()
+ {
+ }
+
+ public function notifySuccess($message = null)
+ {
+ if ($message === null) {
+ $message = t('Form has successfully been sent');
+ }
+ Notification::success($message);
+ return $this;
+ }
+
+ public function notifyError($message)
+ {
+ Notification::error($message);
+ return $this;
+ }
+
+ protected function redirectAndExit($url)
+ {
+ /** @var Web $app */
+ $app = Icinga::app();
+ /** @var Response $response */
+ $response = $app->getFrontController()->getResponse();
+ $response->redirectAndExit($url);
+ }
+
+ protected function setHttpResponseCode($code)
+ {
+ /** @var Web $app */
+ $app = Icinga::app();
+ $app->getFrontController()->getResponse()->setHttpResponseCode($code);
+ return $this;
+ }
+
+ protected function onRequest()
+ {
+ }
+
+ public function setRequest(Request $request)
+ {
+ if ($this->request !== null) {
+ throw new ProgrammingError('Unable to set request twice');
+ }
+
+ $this->request = $request;
+ $this->prepareElements();
+ $this->onRequest();
+ return $this;
+ }
+
+ /**
+ * @return Request
+ */
+ public function getRequest()
+ {
+ if ($this->request === null) {
+ /** @var Web $app */
+ $app = Icinga::app();
+ /** @var Request $request */
+ $request = $app->getFrontController()->getRequest();
+ $this->setRequest($request);
+ }
+ return $this->request;
+ }
+
+ public function hasBeenSent()
+ {
+ if ($this->hasBeenSent === null) {
+ if ($this->request === null) {
+ /** @var Web $app */
+ $app = Icinga::app();
+ $req = $app->getFrontController()->getRequest();
+ } else {
+ $req = $this->request;
+ }
+
+ /** @var Request $req */
+ if ($req->isPost()) {
+ $post = $req->getPost();
+ $this->hasBeenSent = array_key_exists(self::ID, $post) &&
+ $post[self::ID] === $this->getName();
+ } else {
+ $this->hasBeenSent = false;
+ }
+ }
+
+ return $this->hasBeenSent;
+ }
+
+ protected function detectName()
+ {
+ if ($this->formName !== null) {
+ $this->setName($this->formName);
+ } else {
+ $this->setName(get_class($this));
+ }
+ }
+}
diff --git a/library/Businessprocess/Web/Form/Validator/HostServiceTermValidator.php b/library/Businessprocess/Web/Form/Validator/HostServiceTermValidator.php
new file mode 100644
index 0000000..58249f7
--- /dev/null
+++ b/library/Businessprocess/Web/Form/Validator/HostServiceTermValidator.php
@@ -0,0 +1,96 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Form\Validator;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\ServiceNode;
+use Icinga\Module\Businessprocess\State\IcingaDbState;
+use Icinga\Module\Businessprocess\State\MonitoringState;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+use ipl\I18n\Translation;
+use ipl\Validator\BaseValidator;
+use ipl\Web\FormElement\TermInput\Term;
+use LogicException;
+
+class HostServiceTermValidator extends BaseValidator
+{
+ use Translation;
+
+ /** @var ?BpNode */
+ protected $parent;
+
+ /**
+ * Set the affected process
+ *
+ * @param BpNode $parent
+ *
+ * @return $this
+ */
+ public function setParent(BpNode $parent): self
+ {
+ $this->parent = $parent;
+
+ return $this;
+ }
+
+ public function isValid($terms)
+ {
+ if ($this->parent === null) {
+ throw new LogicException('Missing parent process. Cannot validate terms.');
+ }
+
+ if (! is_array($terms)) {
+ $terms = [$terms];
+ }
+
+ $isValid = true;
+ $testConfig = new BpConfig();
+
+ foreach ($terms as $term) {
+ /** @var Term $term */
+ [$hostName, $serviceName] = BpConfig::splitNodeName($term->getSearchValue());
+ if ($serviceName !== null && $serviceName !== 'Hoststatus') {
+ $node = $testConfig->createService($hostName, $serviceName);
+ } else {
+ $node = $testConfig->createHost($hostName);
+ if ($serviceName === null) {
+ $term->setSearchValue(BpConfig::joinNodeName($hostName, 'Hoststatus'));
+ }
+ }
+
+ if ($this->parent->hasChild($term->getSearchValue())) {
+ $term->setMessage($this->translate('Already defined in this process'));
+ $isValid = false;
+ } else {
+ $testConfig->getNode('__unbound__')
+ ->addChild($node);
+ }
+ }
+
+ if ($this->parent->getBpConfig()->getBackend() instanceof MonitoringBackend) {
+ MonitoringState::apply($testConfig);
+ } else {
+ IcingaDbState::apply($testConfig);
+ }
+
+ foreach ($terms as $term) {
+ /** @var Term $term */
+ $node = $testConfig->getNode($term->getSearchValue());
+ if ($node->isMissing()) {
+ if ($node instanceof ServiceNode) {
+ $term->setMessage($this->translate('Service not found'));
+ } else {
+ $term->setMessage($this->translate('Host not found'));
+ }
+
+ $isValid = false;
+ } else {
+ $term->setLabel($node->getAlias());
+ $term->setClass($node->getObjectClassName());
+ }
+ }
+
+ return $isValid;
+ }
+}
diff --git a/library/Businessprocess/Web/Navigation/Renderer/ProcessProblemsBadge.php b/library/Businessprocess/Web/Navigation/Renderer/ProcessProblemsBadge.php
new file mode 100644
index 0000000..575dc5e
--- /dev/null
+++ b/library/Businessprocess/Web/Navigation/Renderer/ProcessProblemsBadge.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Navigation\Renderer;
+
+use Icinga\Module\Businessprocess\Storage\LegacyStorage;
+use Icinga\Web\Navigation\Renderer\BadgeNavigationItemRenderer;
+
+class ProcessProblemsBadge extends BadgeNavigationItemRenderer
+{
+ /**
+ * Cached count
+ *
+ * @var int
+ */
+ protected $count;
+
+ /** @var string */
+ private $bpConfigName;
+
+ public function getCount()
+ {
+ $count = 0;
+ if ($this->count === null) {
+ $storage = LegacyStorage::getInstance();
+ $bp = $storage->loadProcess($this->getBpConfigName());
+ foreach ($bp->getRootNodes() as $rootNode) {
+ if (! $rootNode->isEmpty() &&
+ $rootNode->getState() !== $rootNode::ICINGA_PENDING
+ && $rootNode->hasProblems()) {
+ $count++;
+ }
+ }
+
+ $this->count = $count;
+ $this->setState(self::STATE_CRITICAL);
+ }
+
+ if ($count) {
+ $this->setTitle(sprintf(
+ tp('One unhandled root node critical', '%d unhandled root nodes critical', $count),
+ $count
+ ));
+ }
+
+ return $this->count;
+ }
+
+ public function setBpConfigName($bpConfigName)
+ {
+ $this->bpConfigName = $bpConfigName;
+
+ return $this;
+ }
+
+ public function getBpConfigName()
+ {
+ return $this->bpConfigName;
+ }
+}
diff --git a/library/Businessprocess/Web/Navigation/Renderer/ProcessesProblemsBadge.php b/library/Businessprocess/Web/Navigation/Renderer/ProcessesProblemsBadge.php
new file mode 100644
index 0000000..dd419a2
--- /dev/null
+++ b/library/Businessprocess/Web/Navigation/Renderer/ProcessesProblemsBadge.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web\Navigation\Renderer;
+
+use Icinga\Application\Modules\Module;
+use Icinga\Module\Businessprocess\ProvidedHook\Icingadb\IcingadbSupport;
+use Icinga\Module\Businessprocess\State\IcingaDbState;
+use Icinga\Module\Businessprocess\State\MonitoringState;
+use Icinga\Module\Businessprocess\Storage\LegacyStorage;
+use Icinga\Web\Navigation\Renderer\BadgeNavigationItemRenderer;
+
+class ProcessesProblemsBadge extends BadgeNavigationItemRenderer
+{
+ /**
+ * Cached count
+ *
+ * @var int
+ */
+ protected $count;
+
+ public function getCount()
+ {
+ if ($this->count === null) {
+ $storage = LegacyStorage::getInstance();
+ $count = 0;
+
+ foreach ($storage->listProcessNames() as $processName) {
+ $bp = $storage->loadProcess($processName);
+ if (Module::exists('icingadb') &&
+ (! $bp->hasBackendName() && IcingadbSupport::useIcingaDbAsBackend())
+ ) {
+ IcingaDbState::apply($bp);
+ } else {
+ MonitoringState::apply($bp);
+ }
+
+ foreach ($bp->getRootNodes() as $rootNode) {
+ if (! $rootNode->isEmpty() &&
+ $rootNode->getState() !== $rootNode::ICINGA_PENDING
+ && $rootNode->hasProblems()) {
+ $count++;
+ break;
+ }
+ }
+ }
+
+ $this->count = $count;
+ $this->setState(self::STATE_CRITICAL);
+ }
+
+ return $this->count;
+ }
+}
diff --git a/library/Businessprocess/Web/Url.php b/library/Businessprocess/Web/Url.php
new file mode 100644
index 0000000..92b1e85
--- /dev/null
+++ b/library/Businessprocess/Web/Url.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Icinga\Module\Businessprocess\Web;
+
+use Icinga\Application\Icinga;
+use Icinga\Application\Web;
+use Icinga\Web\Request;
+use Icinga\Web\Url as WebUrl;
+
+/**
+ * Class Url
+ *
+ * The main purpose of this class is to get unit tests running on CLI
+ *
+ * @package Icinga\Module\Businessprocess\Web
+ */
+class Url extends WebUrl
+{
+ /**
+ * @return FakeRequest|Request
+ */
+ protected static function getRequest()
+ {
+ $app = Icinga::app();
+ if ($app->isCli()) {
+ return new FakeRequest();
+ }
+
+ /** @var Web $app */
+ return $app->getRequest();
+ }
+}
diff --git a/module.info b/module.info
new file mode 100644
index 0000000..c492473
--- /dev/null
+++ b/module.info
@@ -0,0 +1,16 @@
+Name: Businessprocess
+Version: 2.5.0
+Requires:
+ Libraries: icinga-php-library (>=0.13.0), icinga-php-thirdparty (>=0.12.0)
+ Modules: monitoring (>=2.9.0), icingadb (>=1.1.0)
+Description: A Business Process viewer and modeler
+ Provides a web-based process modeler for Icinga. It integrates as a module
+ into Icinga Web 2 and provides a plugin check command for Icinga. Tile and tree
+ views can be shown inline, as dashlets or fullscreen. All of those for whole
+ processes or just parts of them.
+
+ Hooks into the monitoring or icingadb module to show Business Impact for a
+ specific host or service and provides a Business Impact Simulation mode to
+ visualize the influence of a potential outage.
+
+ Supports legacy BPaddon config files.
diff --git a/phpcs.xml b/phpcs.xml
new file mode 100644
index 0000000..d5b9ebc
--- /dev/null
+++ b/phpcs.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0"?>
+<ruleset name="PHP_CodeSniffer">
+ <description>Sniff our code a while</description>
+
+ <file>configuration.php</file>
+ <file>run.php</file>
+ <file>application/</file>
+ <file>library/</file>
+ <file>test/</file>
+
+ <exclude-pattern>vendor/*</exclude-pattern>
+
+ <arg value="wps"/>
+ <arg name="colors"/>
+ <arg name="report-width" value="auto"/>
+ <arg name="report-full"/>
+ <arg name="report-gitblame"/>
+ <arg name="report-summary"/>
+ <arg name="encoding" value="UTF-8"/>
+
+ <rule ref="PSR2"/>
+ <rule ref="PSR2.Methods.MethodDeclaration.Underscore">
+ <exclude-pattern>library/Businessprocess/Web/Form/Element/Multiselect.php</exclude-pattern>
+ </rule>
+</ruleset>
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
new file mode 100644
index 0000000..cd648b9
--- /dev/null
+++ b/phpstan-baseline.neon
@@ -0,0 +1,4476 @@
+parameters:
+ ignoreErrors:
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Clicommands\\\\CheckCommand\\:\\:listActions\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/clicommands/CheckCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Clicommands\\\\CheckCommand\\:\\:processAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/clicommands/CheckCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Clicommands\\\\CleanupCommand\\:\\:init\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/clicommands/CleanupCommand.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:getNode\\(\\) expects string, int\\|string given\\.$#"
+ count: 1
+ path: application/clicommands/CleanupCommand.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Clicommands\\\\CleanupCommand\\:\\:\\$defaultActionName has no type specified\\.$#"
+ count: 1
+ path: application/clicommands/CleanupCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Clicommands\\\\ProcessCommand\\:\\:checkAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/clicommands/ProcessCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Clicommands\\\\ProcessCommand\\:\\:getFirstProcessName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/clicommands/ProcessCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Clicommands\\\\ProcessCommand\\:\\:init\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/clicommands/ProcessCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Clicommands\\\\ProcessCommand\\:\\:listAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/clicommands/ProcessCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Clicommands\\\\ProcessCommand\\:\\:listBpNames\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/clicommands/ProcessCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Clicommands\\\\ProcessCommand\\:\\:listConfigNames\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/clicommands/ProcessCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Clicommands\\\\ProcessCommand\\:\\:listConfigNames\\(\\) has parameter \\$withTitle with no type specified\\.$#"
+ count: 1
+ path: application/clicommands/ProcessCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Clicommands\\\\ProcessCommand\\:\\:renderProblemTree\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/clicommands/ProcessCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Clicommands\\\\ProcessCommand\\:\\:renderProblemTree\\(\\) has parameter \\$depth with no type specified\\.$#"
+ count: 1
+ path: application/clicommands/ProcessCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Clicommands\\\\ProcessCommand\\:\\:renderProblemTree\\(\\) has parameter \\$tree with no type specified\\.$#"
+ count: 1
+ path: application/clicommands/ProcessCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Clicommands\\\\ProcessCommand\\:\\:renderProblemTree\\(\\) has parameter \\$useColors with no type specified\\.$#"
+ count: 1
+ path: application/clicommands/ProcessCommand.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:getNode\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/clicommands/ProcessCommand.php
+
+ -
+ message: "#^Parameter \\#1 \\$rootCause of method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:getProblemTreeBlame\\(\\) expects bool, mixed given\\.$#"
+ count: 1
+ path: application/clicommands/ProcessCommand.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Clicommands\\\\ProcessCommand\\:\\:\\$hostColors has no type specified\\.$#"
+ count: 1
+ path: application/clicommands/ProcessCommand.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Clicommands\\\\ProcessCommand\\:\\:\\$serviceColors has no type specified\\.$#"
+ count: 1
+ path: application/clicommands/ProcessCommand.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\HostController\\:\\:moduleInit\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/HostController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\HostController\\:\\:showAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/HostController.php
+
+ -
+ message: "#^Parameter \\#2 \\$value of method Icinga\\\\Web\\\\UrlParams\\:\\:add\\(\\) expects bool\\|string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/HostController.php
+
+ -
+ message: "#^Parameter \\#2 \\$value of static method ipl\\\\Stdlib\\\\Filter\\:\\:equal\\(\\) expects array\\|bool\\|float\\|int\\|string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/HostController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\IndexController\\:\\:indexAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/IndexController.php
+
+ -
+ message: "#^Binary operation \"\\+\" between int\\|string and 1 results in an error\\.$#"
+ count: 1
+ path: application/controllers/NodeController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\NodeController\\:\\:impactAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/NodeController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:getNode\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/NodeController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\ProcessController\\:\\:configAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\ProcessController\\:\\:createAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\ProcessController\\:\\:createConfigActionBar\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\ProcessController\\:\\:createConfigActionBar\\(\\) has parameter \\$showDiff with no type specified\\.$#"
+ count: 1
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\ProcessController\\:\\:downloadAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\ProcessController\\:\\:getNode\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\ProcessController\\:\\:getProcessTabs\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\ProcessController\\:\\:handleFormatRequest\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\ProcessController\\:\\:handleSimulations\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\ProcessController\\:\\:loadActionForm\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\ProcessController\\:\\:prepareControls\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\ProcessController\\:\\:prepareControls\\(\\) has parameter \\$bp with no type specified\\.$#"
+ count: 1
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\ProcessController\\:\\:prepareControls\\(\\) has parameter \\$renderer with no type specified\\.$#"
+ count: 1
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\ProcessController\\:\\:prepareRenderer\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\ProcessController\\:\\:prepareRenderer\\(\\) has parameter \\$bp with no type specified\\.$#"
+ count: 1
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\ProcessController\\:\\:prepareRenderer\\(\\) has parameter \\$node with no type specified\\.$#"
+ count: 1
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\ProcessController\\:\\:setDynamicAutorefresh\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\ProcessController\\:\\:showAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\ProcessController\\:\\:showErrors\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\ProcessController\\:\\:showHints\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\ProcessController\\:\\:showWarnings\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\ProcessController\\:\\:sourceAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\ProcessController\\:\\:tabsForConfig\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\ProcessController\\:\\:tabsForShow\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\ProcessController\\:\\:uploadAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:getNode\\(\\) expects string, mixed given\\.$#"
+ count: 6
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Parameter \\#1 \\$node of method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\AddNodeForm\\:\\:setParentNode\\(\\) expects Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\|null, Icinga\\\\Module\\\\Businessprocess\\\\Node\\|null given\\.$#"
+ count: 1
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Parameter \\#1 \\$node of method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\EditNodeForm\\:\\:setParentNode\\(\\) expects Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\|null, Icinga\\\\Module\\\\Businessprocess\\\\Node\\|null given\\.$#"
+ count: 1
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Parameter \\#1 \\$path of method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\Renderer\\:\\:setPath\\(\\) expects array, mixed given\\.$#"
+ count: 1
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Parameter \\#1 \\$stream of function fpassthru expects resource, resource\\|false given\\.$#"
+ count: 1
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Parameter \\#1 \\$stream of function fputcsv expects resource, resource\\|false given\\.$#"
+ count: 2
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Parameter \\#1 \\$stream of function rewind expects resource, resource\\|false given\\.$#"
+ count: 1
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function strtolower expects string, mixed given\\.$#"
+ count: 1
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Parameter \\#1 \\(mixed\\) of echo cannot be converted to string\\.$#"
+ count: 1
+ path: application/controllers/ProcessController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\ServiceController\\:\\:moduleInit\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ServiceController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\ServiceController\\:\\:showAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/ServiceController.php
+
+ -
+ message: "#^Parameter \\#2 \\$value of method Icinga\\\\Web\\\\UrlParams\\:\\:add\\(\\) expects bool\\|string, mixed given\\.$#"
+ count: 2
+ path: application/controllers/ServiceController.php
+
+ -
+ message: "#^Parameter \\#2 \\$value of static method ipl\\\\Stdlib\\\\Filter\\:\\:equal\\(\\) expects array\\|bool\\|float\\|int\\|string, mixed given\\.$#"
+ count: 2
+ path: application/controllers/ServiceController.php
+
+ -
+ message: "#^Cannot access property \\$display_name on mixed\\.$#"
+ count: 2
+ path: application/controllers/SuggestionsController.php
+
+ -
+ message: "#^Cannot access property \\$host on mixed\\.$#"
+ count: 2
+ path: application/controllers/SuggestionsController.php
+
+ -
+ message: "#^Cannot access property \\$name on mixed\\.$#"
+ count: 2
+ path: application/controllers/SuggestionsController.php
+
+ -
+ message: "#^Cannot call method getExcludeTerms\\(\\) on ipl\\\\Web\\\\FormElement\\\\TermInput\\\\TermSuggestions\\|null\\.$#"
+ count: 5
+ path: application/controllers/SuggestionsController.php
+
+ -
+ message: "#^Cannot call method getOriginalSearchValue\\(\\) on ipl\\\\Web\\\\FormElement\\\\TermInput\\\\TermSuggestions\\|null\\.$#"
+ count: 14
+ path: application/controllers/SuggestionsController.php
+
+ -
+ message: "#^Cannot call method getSearchTerm\\(\\) on ipl\\\\Web\\\\FormElement\\\\TermInput\\\\TermSuggestions\\|null\\.$#"
+ count: 26
+ path: application/controllers/SuggestionsController.php
+
+ -
+ message: "#^Cannot call method matchSearch\\(\\) on ipl\\\\Web\\\\FormElement\\\\TermInput\\\\TermSuggestions\\|null\\.$#"
+ count: 2
+ path: application/controllers/SuggestionsController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\SuggestionsController\\:\\:icingadbHostAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/SuggestionsController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\SuggestionsController\\:\\:icingadbServiceAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/SuggestionsController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\SuggestionsController\\:\\:monitoringHostAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/SuggestionsController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\SuggestionsController\\:\\:monitoringServiceAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/SuggestionsController.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Controllers\\\\SuggestionsController\\:\\:processAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/controllers/SuggestionsController.php
+
+ -
+ message: "#^Parameter \\#1 \\$filter of method Icinga\\\\Module\\\\Monitoring\\\\DataView\\\\DataView\\:\\:applyFilter\\(\\) expects Icinga\\\\Data\\\\Filter\\\\Filter, Icinga\\\\Data\\\\Filter\\\\Filter\\|null given\\.$#"
+ count: 2
+ path: application/controllers/SuggestionsController.php
+
+ -
+ message: "#^Parameter \\#2 \\$filter of static method Icinga\\\\Data\\\\Filter\\\\Filter\\:\\:where\\(\\) expects string, string\\|null given\\.$#"
+ count: 14
+ path: application/controllers/SuggestionsController.php
+
+ -
+ message: "#^Parameter \\#2 \\$value of static method ipl\\\\Stdlib\\\\Filter\\:\\:equal\\(\\) expects array\\|bool\\|float\\|int\\|string, string\\|null given\\.$#"
+ count: 12
+ path: application/controllers/SuggestionsController.php
+
+ -
+ message: "#^Parameter \\#2 \\$value of static method ipl\\\\Stdlib\\\\Filter\\:\\:like\\(\\) expects array\\<string\\>\\|string, string\\|null given\\.$#"
+ count: 12
+ path: application/controllers/SuggestionsController.php
+
+ -
+ message: "#^Cannot call method getBackend\\(\\) on Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\|null\\.$#"
+ count: 2
+ path: application/forms/AddNodeForm.php
+
+ -
+ message: "#^Cannot call method getMetadata\\(\\) on Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\|null\\.$#"
+ count: 1
+ path: application/forms/AddNodeForm.php
+
+ -
+ message: "#^Cannot call method getName\\(\\) on Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\|null\\.$#"
+ count: 2
+ path: application/forms/AddNodeForm.php
+
+ -
+ message: "#^Cannot call method getNode\\(\\) on Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\|null\\.$#"
+ count: 1
+ path: application/forms/AddNodeForm.php
+
+ -
+ message: "#^Cannot call method getRootNodes\\(\\) on Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\|null\\.$#"
+ count: 1
+ path: application/forms/AddNodeForm.php
+
+ -
+ message: "#^Cannot call method getStateOverrides\\(\\) on Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\|null\\.$#"
+ count: 1
+ path: application/forms/AddNodeForm.php
+
+ -
+ message: "#^Cannot call method hasNode\\(\\) on Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\|null\\.$#"
+ count: 1
+ path: application/forms/AddNodeForm.php
+
+ -
+ message: "#^Cannot call method hasRootNode\\(\\) on Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\|null\\.$#"
+ count: 1
+ path: application/forms/AddNodeForm.php
+
+ -
+ message: "#^Cannot call method isEmpty\\(\\) on Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\|null\\.$#"
+ count: 2
+ path: application/forms/AddNodeForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\AddNodeForm\\:\\:applyManualSorting\\(\\) has parameter \\$bpNodes with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/AddNodeForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\AddNodeForm\\:\\:applyManualSorting\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/AddNodeForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\AddNodeForm\\:\\:assemble\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/AddNodeForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\AddNodeForm\\:\\:onSuccess\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/AddNodeForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\AddNodeForm\\:\\:sort\\(\\) has parameter \\$nodes with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/AddNodeForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\AddNodeForm\\:\\:sort\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/AddNodeForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$bp of static method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\ProcessChanges\\:\\:construct\\(\\) expects Icinga\\\\Module\\\\Businessprocess\\\\BpConfig, Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\|null given\\.$#"
+ count: 1
+ path: application/forms/AddNodeForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of static method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:escapeName\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/forms/AddNodeForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$node of method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\ProcessChanges\\:\\:modifyNode\\(\\) expects Icinga\\\\Module\\\\Businessprocess\\\\Node, Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\|null given\\.$#"
+ count: 1
+ path: application/forms/AddNodeForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$parent of method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\Validator\\\\HostServiceTermValidator\\:\\:setParent\\(\\) expects Icinga\\\\Module\\\\Businessprocess\\\\BpNode, Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\|null given\\.$#"
+ count: 1
+ path: application/forms/AddNodeForm.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 1
+ path: application/forms/AddNodeForm.php
+
+ -
+ message: "#^Cannot call method getLabel\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: application/forms/BpConfigForm.php
+
+ -
+ message: "#^Cannot call method setAttrib\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: application/forms/BpConfigForm.php
+
+ -
+ message: "#^Cannot call method setValue\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: application/forms/BpConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\BpConfigForm\\:\\:hasDeleteButton\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/BpConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\BpConfigForm\\:\\:onRequest\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/BpConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\BpConfigForm\\:\\:onSetup\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/BpConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\BpConfigForm\\:\\:onSuccess\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/BpConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\BpConfigForm\\:\\:setup\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/BpConfigForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\BpConfigForm\\:\\:shouldBeDeleted\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/BpConfigForm.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 5
+ path: application/forms/BpConfigForm.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\BpConfigForm\\:\\:\\$deleteButtonName has no type specified\\.$#"
+ count: 1
+ path: application/forms/BpConfigForm.php
+
+ -
+ message: "#^Access to an undefined property Zend_Form_Element_File\\:\\:\\$file\\.$#"
+ count: 1
+ path: application/forms/BpUploadForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\BpUploadForm\\:\\:getTempDir\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/BpUploadForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\BpUploadForm\\:\\:getUploadedConfig\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/BpUploadForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\BpUploadForm\\:\\:hasSource\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/BpUploadForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\BpUploadForm\\:\\:onSuccess\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/BpUploadForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\BpUploadForm\\:\\:parseSubmittedSourceCode\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/BpUploadForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\BpUploadForm\\:\\:processUploadedSource\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/BpUploadForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\BpUploadForm\\:\\:setup\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/BpUploadForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\BpUploadForm\\:\\:showDetails\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/BpUploadForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\BpUploadForm\\:\\:showUpload\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/BpUploadForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$filename of function file_get_contents expects string, string\\|false given\\.$#"
+ count: 1
+ path: application/forms/BpUploadForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$filename of function unlink expects string, string\\|false given\\.$#"
+ count: 2
+ path: application/forms/BpUploadForm.php
+
+ -
+ message: "#^Parameter \\#2 \\$options of method Zend_Form_Element_File\\:\\:addFilter\\(\\) expects array\\|string\\|null, string\\|false given\\.$#"
+ count: 1
+ path: application/forms/BpUploadForm.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\BpUploadForm\\:\\:\\$deleteButtonName has no type specified\\.$#"
+ count: 1
+ path: application/forms/BpUploadForm.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\BpUploadForm\\:\\:\\$node has no type specified\\.$#"
+ count: 1
+ path: application/forms/BpUploadForm.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\BpUploadForm\\:\\:\\$objectList has no type specified\\.$#"
+ count: 1
+ path: application/forms/BpUploadForm.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\BpUploadForm\\:\\:\\$processList has no type specified\\.$#"
+ count: 1
+ path: application/forms/BpUploadForm.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\BpUploadForm\\:\\:\\$sourceCode has no type specified\\.$#"
+ count: 1
+ path: application/forms/BpUploadForm.php
+
+ -
+ message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: application/forms/CleanupNodeForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\CleanupNodeForm\\:\\:onSuccess\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/CleanupNodeForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\CleanupNodeForm\\:\\:setup\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/CleanupNodeForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:getNode\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/forms/CleanupNodeForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\DeleteNodeForm\\:\\:onSuccess\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/DeleteNodeForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\DeleteNodeForm\\:\\:setup\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/DeleteNodeForm.php
+
+ -
+ message: "#^Cannot call method getAlias\\(\\) on Icinga\\\\Module\\\\Businessprocess\\\\Node\\|null\\.$#"
+ count: 1
+ path: application/forms/EditNodeForm.php
+
+ -
+ message: "#^Cannot call method getBackend\\(\\) on Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\|null\\.$#"
+ count: 2
+ path: application/forms/EditNodeForm.php
+
+ -
+ message: "#^Cannot call method getChildNames\\(\\) on Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\|null\\.$#"
+ count: 1
+ path: application/forms/EditNodeForm.php
+
+ -
+ message: "#^Cannot call method getMetadata\\(\\) on Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\|null\\.$#"
+ count: 1
+ path: application/forms/EditNodeForm.php
+
+ -
+ message: "#^Cannot call method getName\\(\\) on Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\|null\\.$#"
+ count: 1
+ path: application/forms/EditNodeForm.php
+
+ -
+ message: "#^Cannot call method getName\\(\\) on Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\|null\\.$#"
+ count: 3
+ path: application/forms/EditNodeForm.php
+
+ -
+ message: "#^Cannot call method getName\\(\\) on Icinga\\\\Module\\\\Businessprocess\\\\Node\\|null\\.$#"
+ count: 3
+ path: application/forms/EditNodeForm.php
+
+ -
+ message: "#^Cannot call method getNode\\(\\) on Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\|null\\.$#"
+ count: 2
+ path: application/forms/EditNodeForm.php
+
+ -
+ message: "#^Cannot call method getStateOverrides\\(\\) on Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\|null\\.$#"
+ count: 2
+ path: application/forms/EditNodeForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\EditNodeForm\\:\\:assemble\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/EditNodeForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\EditNodeForm\\:\\:identifyChosenNode\\(\\) should return Icinga\\\\Module\\\\Businessprocess\\\\Node but returns Icinga\\\\Module\\\\Businessprocess\\\\Node\\|null\\.$#"
+ count: 1
+ path: application/forms/EditNodeForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\EditNodeForm\\:\\:onSuccess\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/EditNodeForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$bp of static method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\ProcessChanges\\:\\:construct\\(\\) expects Icinga\\\\Module\\\\Businessprocess\\\\BpConfig, Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\|null given\\.$#"
+ count: 1
+ path: application/forms/EditNodeForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of static method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:joinNodeName\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/forms/EditNodeForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$node of method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\ProcessChanges\\:\\:deleteNode\\(\\) expects Icinga\\\\Module\\\\Businessprocess\\\\Node, Icinga\\\\Module\\\\Businessprocess\\\\Node\\|null given\\.$#"
+ count: 1
+ path: application/forms/EditNodeForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$node of method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\ProcessChanges\\:\\:modifyNode\\(\\) expects Icinga\\\\Module\\\\Businessprocess\\\\Node, Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\|null given\\.$#"
+ count: 1
+ path: application/forms/EditNodeForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$parent of method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\Validator\\\\HostServiceTermValidator\\:\\:setParent\\(\\) expects Icinga\\\\Module\\\\Businessprocess\\\\BpNode, Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\|null given\\.$#"
+ count: 1
+ path: application/forms/EditNodeForm.php
+
+ -
+ message: "#^Parameter \\#3 \\$to of method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\ProcessChanges\\:\\:moveNode\\(\\) expects int, int\\|string\\|false given\\.$#"
+ count: 1
+ path: application/forms/EditNodeForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\MoveNodeForm\\:\\:hasBeenSent\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/MoveNodeForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\MoveNodeForm\\:\\:onSuccess\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/MoveNodeForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\MoveNodeForm\\:\\:setup\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/MoveNodeForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$token of static method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\CsrfToken\\:\\:isValid\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/forms/MoveNodeForm.php
+
+ -
+ message: "#^Parameter \\#2 \\$from of method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\ProcessChanges\\:\\:moveNode\\(\\) expects int, mixed given\\.$#"
+ count: 1
+ path: application/forms/MoveNodeForm.php
+
+ -
+ message: "#^Parameter \\#3 \\$to of method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\ProcessChanges\\:\\:moveNode\\(\\) expects int, mixed given\\.$#"
+ count: 1
+ path: application/forms/MoveNodeForm.php
+
+ -
+ message: "#^Parameter \\#4 \\$newParent of method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\ProcessChanges\\:\\:moveNode\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: application/forms/MoveNodeForm.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\MoveNodeForm\\:\\:\\$parentNode \\(Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\) does not accept Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\|null\\.$#"
+ count: 1
+ path: application/forms/MoveNodeForm.php
+
+ -
+ message: "#^Cannot call method setValue\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 4
+ path: application/forms/ProcessForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\ProcessForm\\:\\:onSuccess\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/ProcessForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\ProcessForm\\:\\:setup\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/ProcessForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$nodeName of method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\ProcessChanges\\:\\:createNode\\(\\) expects Icinga\\\\Module\\\\Businessprocess\\\\Node\\|string, mixed given\\.$#"
+ count: 1
+ path: application/forms/ProcessForm.php
+
+ -
+ message: "#^Cannot access property \\$acknowledged on mixed\\.$#"
+ count: 1
+ path: application/forms/SimulationForm.php
+
+ -
+ message: "#^Cannot access property \\$in_downtime on mixed\\.$#"
+ count: 1
+ path: application/forms/SimulationForm.php
+
+ -
+ message: "#^Cannot access property \\$state on mixed\\.$#"
+ count: 1
+ path: application/forms/SimulationForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\SimulationForm\\:\\:enumStateNames\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: application/forms/SimulationForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\SimulationForm\\:\\:onSuccess\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/SimulationForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\SimulationForm\\:\\:setNode\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/SimulationForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\SimulationForm\\:\\:setNode\\(\\) has parameter \\$node with no type specified\\.$#"
+ count: 1
+ path: application/forms/SimulationForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\SimulationForm\\:\\:setSimulation\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/SimulationForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Forms\\\\SimulationForm\\:\\:setup\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/forms/SimulationForm.php
+
+ -
+ message: "#^Method Zend_View_Helper_FormSimpleNote\\:\\:formSimpleNote\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/views/helpers/FormSimpleNote.php
+
+ -
+ message: "#^Method Zend_View_Helper_FormSimpleNote\\:\\:formSimpleNote\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: application/views/helpers/FormSimpleNote.php
+
+ -
+ message: "#^Method Zend_View_Helper_FormSimpleNote\\:\\:formSimpleNote\\(\\) has parameter \\$value with no type specified\\.$#"
+ count: 1
+ path: application/views/helpers/FormSimpleNote.php
+
+ -
+ message: "#^Method Zend_View_Helper_RenderStateBadges\\:\\:renderStateBadges\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: application/views/helpers/RenderStateBadges.php
+
+ -
+ message: "#^Method Zend_View_Helper_RenderStateBadges\\:\\:renderStateBadges\\(\\) has parameter \\$summary with no type specified\\.$#"
+ count: 1
+ path: application/views/helpers/RenderStateBadges.php
+
+ -
+ message: "#^Cannot access offset 0 on array\\<int, string\\>\\|false\\.$#"
+ count: 2
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Cannot use array destructuring on array\\<int, string\\>\\|false\\.$#"
+ count: 2
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:addNode\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:addRootNode\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:addRootNode\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:beginLoopDetection\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:beginLoopDetection\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:calculateAllStates\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:clearAllStates\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:clearAppliedChanges\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:countSimulations\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:createHost\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:createHost\\(\\) has parameter \\$host with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:createImportedNode\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:createImportedNode\\(\\) has parameter \\$config with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:createImportedNode\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:createMissingBp\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:createMissingBp\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:createService\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:createService\\(\\) has parameter \\$host with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:createService\\(\\) has parameter \\$service with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:endLoopDetection\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:endLoopDetection\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:escapeName\\(\\) should return string but returns string\\|null\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:getBackend\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:getBackendName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:getBpNode\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:getErrors\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:getImportedConfig\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:getImportedConfig\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:getImportedNodes\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:getMissingChildren\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:getNodes\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:getStateType\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:getTitle\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:getWarnings\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:hasBackend\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:hasBackendName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:hasBeenChanged\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:hasBpNode\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:hasNode\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:hasNode\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:hasRootNode\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:hasRootNode\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:hasSimulations\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:hasTitle\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:isReferenced\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:isRootNode\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:isRootNode\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:listBpNodes\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:listInvolvedConfigs\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:listInvolvedConfigs\\(\\) has parameter \\$configs with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:listInvolvedHostNames\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:listInvolvedHostNames\\(\\) has parameter \\$usedConfigs with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:listRootNodes\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:removeNode\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:removeNode\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:removeRootNode\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:removeRootNode\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:setBackend\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:setBackend\\(\\) has parameter \\$backend with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:setName\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:setTitle\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:setTitle\\(\\) has parameter \\$title with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:splitNodeName\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:toArray\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:translate\\(\\) has parameter \\$msg with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:useHardStates\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:useSoftStates\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:usesHardStates\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:usesSoftStates\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:warn\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:warn\\(\\) has parameter \\$msg with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Parameter \\#1 \\$alias of method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:setAlias\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Parameter \\#1 \\$format of function sprintf expects string, mixed given\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Parameter \\#2 \\$values of function vsprintf expects array\\<bool\\|float\\|int\\|string\\|null\\>, array\\<int, mixed\\> given\\.$#"
+ count: 2
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:\\$changeCount has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:\\$errors type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:\\$hosts type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:\\$loopDetection has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:\\$nodes type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:\\$root_nodes type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:\\$simulationCount has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:\\$warnings type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/BpConfig.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:__construct\\(\\) has parameter \\$object with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:assertNumericOperator\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:assertValidOperator\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:assertValidOperator\\(\\) has parameter \\$operator with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:checkForLoops\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:getChildBpNodes\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:getChildByName\\(\\) has parameter \\$childName with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:getChildNames\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:getChildren\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:getChildren\\(\\) has parameter \\$filter with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:getDisplay\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:getHtmlId\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:getInfoUrl\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:getMissingChildren\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:getOperator\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:getProblemTree\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:getProblemTreeBlame\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:getProblematicChildren\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:getState\\(\\) should return int but returns int\\|null\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:getStateOverrides\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:getStateOverrides\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:getStateSummary\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:hasChild\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:hasChild\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:hasChildren\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:hasChildren\\(\\) has parameter \\$filter with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:hasInfoUrl\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:hasProblems\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:invertSortingState\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:invertSortingState\\(\\) has parameter \\$state with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:isEmpty\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:isMissing\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:operatorHtml\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:removeChild\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:removeChild\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:setChildNames\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:setChildNames\\(\\) has parameter \\$names with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:setDisplay\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:setDisplay\\(\\) has parameter \\$display with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:setInfoUrl\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:setInfoUrl\\(\\) has parameter \\$url with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:setOperator\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:setOperator\\(\\) has parameter \\$operator with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:setStateOverrides\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:setStateOverrides\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:setStateOverrides\\(\\) has parameter \\$overrides with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Parameter \\#2 \\$array of function array_key_exists expects array, array\\<Icinga\\\\Module\\\\Businessprocess\\\\Node\\>\\|null given\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:\\$childNames type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:\\$className has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:\\$counters has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:\\$display has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:\\$empty has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:\\$emptyStateSummary has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:\\$missing has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:\\$missingChildren has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:\\$operator has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:\\$sortStateInversionMap has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:\\$stateOverrides has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\:\\:\\$url has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/BpNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Director\\\\ShipConfigFiles\\:\\:fetchFiles\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Director/ShipConfigFiles.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\HostNode\\:\\:__construct\\(\\) has parameter \\$object with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/HostNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\HostNode\\:\\:getHostname\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/HostNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\HostNode\\:\\:getUrl\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/HostNode.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\HostNode\\:\\:\\$className has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/HostNode.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\HostNode\\:\\:\\$hostname has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/HostNode.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\HostNode\\:\\:\\$sortStateToStateMap has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/HostNode.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\HostNode\\:\\:\\$stateNames has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/HostNode.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\HostNode\\:\\:\\$stateToSortStateMap has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/HostNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\IcingaDbObject\\:\\:applyIcingaDbRestrictions\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/IcingaDbObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\IcingaDbObject\\:\\:applyIcingaDbRestrictions\\(\\) has parameter \\$query with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/IcingaDbObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\IcingaDbObject\\:\\:fetchDb\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/IcingaDbObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\IcingaDbObject\\:\\:fetchHosts\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/IcingaDbObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\IcingaDbObject\\:\\:fetchHosts\\(\\) has parameter \\$filter with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/IcingaDbObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\IcingaDbObject\\:\\:fetchServices\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/IcingaDbObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\IcingaDbObject\\:\\:fetchServices\\(\\) has parameter \\$filter with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/IcingaDbObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\IcingaDbObject\\:\\:yieldHostnames\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/IcingaDbObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\IcingaDbObject\\:\\:yieldHostnames\\(\\) has parameter \\$filter with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/IcingaDbObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\IcingaDbObject\\:\\:yieldServicenames\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/IcingaDbObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\IcingaDbObject\\:\\:yieldServicenames\\(\\) has parameter \\$host with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/IcingaDbObject.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\ImportedNode\\:\\:__construct\\(\\) has parameter \\$object with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/ImportedNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\ImportedNode\\:\\:getBpConfig\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/ImportedNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\ImportedNode\\:\\:getChildNames\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/ImportedNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\ImportedNode\\:\\:getIdentifier\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/ImportedNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\ImportedNode\\:\\:getOperator\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/ImportedNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\ImportedNode\\:\\:isMissing\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/ImportedNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:__construct\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:assertKeyExists\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:assertKeyExists\\(\\) has parameter \\$key with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:canModify\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:canRead\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:get\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:get\\(\\) has parameter \\$default with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:get\\(\\) has parameter \\$key with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:getAuth\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:getExtendedTitle\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:getProperties\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:getTitle\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:has\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:has\\(\\) has parameter \\$key with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:hasKey\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:hasKey\\(\\) has parameter \\$key with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:hasOneOfTheAllowedRoles\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:hasRestrictions\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:isInAllowedUserList\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:isInAllowedUserList\\(\\) has parameter \\$username with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:isManuallyOrdered\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:isMemberOfAllowedGroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:isNull\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:isNull\\(\\) has parameter \\$key with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:listAllowedGroups\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:listAllowedRoles\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:listAllowedUsers\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:nameIsPrefixedWithOneOf\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:nameIsPrefixedWithOneOf\\(\\) has parameter \\$prefixes with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:ownerIs\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:ownerIs\\(\\) has parameter \\$username with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:set\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:set\\(\\) has parameter \\$key with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:set\\(\\) has parameter \\$value with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:splitCommaSeparated\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:splitCommaSeparated\\(\\) has parameter \\$string with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:userCanRead\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Metadata\\:\\:\\$properties has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Metadata.php
+
+ -
+ message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeAction.php
+
+ -
+ message: "#^Cannot access offset 'actionName' on mixed\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeAction.php
+
+ -
+ message: "#^Cannot access offset 'nodeName' on mixed\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeAction.php
+
+ -
+ message: "#^Cannot access offset 'properties' on mixed\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeAction\\:\\:create\\(\\) should return static\\(Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeAction\\) but returns object\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeAction\\:\\:error\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeAction\\:\\:hasNode\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeAction\\:\\:serialize\\(\\) should return string but returns string\\|false\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeAction\\:\\:unSerialize\\(\\) has parameter \\$string with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeAction.php
+
+ -
+ message: "#^PHPDoc tag @param has invalid value \\(mixed \\.\\.\\.\\)\\: Unexpected token \"\\\\n \\*\", expected variable at offset 100$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeAction.php
+
+ -
+ message: "#^Parameter \\#1 \\$actionName of static method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeAction\\:\\:create\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeAction.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function ucfirst expects string, mixed given\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeAction.php
+
+ -
+ message: "#^Parameter \\#2 \\$nodeName of static method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeAction\\:\\:create\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeAction.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeAction\\:\\:\\$preserveProperties type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeAction.php
+
+ -
+ message: "#^Cannot use array destructuring on array\\<int, string\\>\\|false\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeAddChildrenAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeAddChildrenAction\\:\\:getChildren\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeAddChildrenAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeAddChildrenAction\\:\\:setChildren\\(\\) has parameter \\$children with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeAddChildrenAction.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeAddChildrenAction\\:\\:\\$children has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeAddChildrenAction.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeAddChildrenAction\\:\\:\\$preserveProperties type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeAddChildrenAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeApplyManualOrderAction\\:\\:applyManualSorting\\(\\) has parameter \\$bpNodes with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeApplyManualOrderAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeApplyManualOrderAction\\:\\:applyManualSorting\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeApplyManualOrderAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeApplyManualOrderAction\\:\\:sort\\(\\) has parameter \\$nodes with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeApplyManualOrderAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeApplyManualOrderAction\\:\\:sort\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeApplyManualOrderAction.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeApplyManualOrderAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeCopyAction\\:\\:applyManualSorting\\(\\) has parameter \\$bpNodes with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeCopyAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeCopyAction\\:\\:applyManualSorting\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeCopyAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeCopyAction\\:\\:sort\\(\\) has parameter \\$nodes with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeCopyAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeCopyAction\\:\\:sort\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeCopyAction.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeCopyAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeCreateAction\\:\\:getProperties\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeCreateAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeCreateAction\\:\\:setParent\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeCreateAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeCreateAction\\:\\:setParentName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeCreateAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeCreateAction\\:\\:setProperties\\(\\) has parameter \\$properties with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeCreateAction.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeCreateAction\\:\\:\\$preserveProperties type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeCreateAction.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeCreateAction\\:\\:\\$properties type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeCreateAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeModifyAction\\:\\:getFormerProperties\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeModifyAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeModifyAction\\:\\:getProperties\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeModifyAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeModifyAction\\:\\:setFormerProperties\\(\\) has parameter \\$properties with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeModifyAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeModifyAction\\:\\:setNodeProperties\\(\\) has parameter \\$properties with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeModifyAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeModifyAction\\:\\:setProperties\\(\\) has parameter \\$properties with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeModifyAction.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeModifyAction\\:\\:\\$formerProperties has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeModifyAction.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeModifyAction\\:\\:\\$preserveProperties type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeModifyAction.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeModifyAction\\:\\:\\$properties has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeModifyAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeMoveAction\\:\\:applyManualSorting\\(\\) has parameter \\$bpNodes with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeMoveAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeMoveAction\\:\\:applyManualSorting\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeMoveAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeMoveAction\\:\\:getFrom\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeMoveAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeMoveAction\\:\\:getNewParent\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeMoveAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeMoveAction\\:\\:getParent\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeMoveAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeMoveAction\\:\\:getTo\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeMoveAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeMoveAction\\:\\:setFrom\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeMoveAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeMoveAction\\:\\:setFrom\\(\\) has parameter \\$from with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeMoveAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeMoveAction\\:\\:setNewParent\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeMoveAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeMoveAction\\:\\:setNewParent\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeMoveAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeMoveAction\\:\\:setParent\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeMoveAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeMoveAction\\:\\:setParent\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeMoveAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeMoveAction\\:\\:setTo\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeMoveAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeMoveAction\\:\\:setTo\\(\\) has parameter \\$to with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeMoveAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeMoveAction\\:\\:sort\\(\\) has parameter \\$nodes with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeMoveAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeMoveAction\\:\\:sort\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeMoveAction.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeMoveAction.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeMoveAction\\:\\:\\$from \\(int\\) does not accept int\\|false\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeMoveAction.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeMoveAction\\:\\:\\$from \\(int\\) does not accept int\\|string\\|false\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeMoveAction.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeMoveAction\\:\\:\\$preserveProperties type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeMoveAction.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeMoveAction\\:\\:\\$to \\(int\\) does not accept int\\|false\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeMoveAction.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeMoveAction\\:\\:\\$to \\(int\\) does not accept int\\|string\\|false\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeMoveAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeRemoveAction\\:\\:setParentName\\(\\) has parameter \\$parentName with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeRemoveAction.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:getNode\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeRemoveAction.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeRemoveAction\\:\\:\\$parentName has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeRemoveAction.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeRemoveAction\\:\\:\\$preserveProperties type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/NodeRemoveAction.php
+
+ -
+ message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/ProcessChanges.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\ProcessChanges\\:\\:addChildrenToNode\\(\\) has parameter \\$children with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/ProcessChanges.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\ProcessChanges\\:\\:copyNode\\(\\) has parameter \\$nodeName with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/ProcessChanges.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\ProcessChanges\\:\\:createNode\\(\\) has parameter \\$properties with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/ProcessChanges.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\ProcessChanges\\:\\:hasBeenModified\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/ProcessChanges.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\ProcessChanges\\:\\:modifyNode\\(\\) has parameter \\$properties with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/ProcessChanges.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\ProcessChanges\\:\\:pop\\(\\) should return bool\\|Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeAction but returns Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeAction\\|null\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/ProcessChanges.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\ProcessChanges\\:\\:serialize\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/ProcessChanges.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\ProcessChanges\\:\\:shift\\(\\) should return bool\\|Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeAction but returns Icinga\\\\Module\\\\Businessprocess\\\\Modification\\\\NodeAction\\|null\\.$#"
+ count: 1
+ path: library/Businessprocess/Modification/ProcessChanges.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\MonitoredNode\\:\\:getLink\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/MonitoredNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\MonitoredNode\\:\\:getUrl\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/MonitoredNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostStatusQuery\\:\\:joinCustomvar\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Monitoring/Backend/Ido/Query/HostStatusQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostStatusQuery\\:\\:joinCustomvar\\(\\) has parameter \\$customvar with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Monitoring/Backend/Ido/Query/HostStatusQuery.php
+
+ -
+ message: "#^Parameter \\#2 \\$array of function array_key_exists expects array, mixed given\\.$#"
+ count: 1
+ path: library/Businessprocess/Monitoring/Backend/Ido/Query/HostStatusQuery.php
+
+ -
+ message: "#^Parameter \\#4 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 1
+ path: library/Businessprocess/Monitoring/Backend/Ido/Query/HostStatusQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\HostStatusQuery\\:\\:\\$customVarsJoinTemplate has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Monitoring/Backend/Ido/Query/HostStatusQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServiceStatusQuery\\:\\:joinCustomvar\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Monitoring/Backend/Ido/Query/ServiceStatusQuery.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServiceStatusQuery\\:\\:joinCustomvar\\(\\) has parameter \\$customvar with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Monitoring/Backend/Ido/Query/ServiceStatusQuery.php
+
+ -
+ message: "#^Parameter \\#2 \\$array of function array_key_exists expects array, mixed given\\.$#"
+ count: 1
+ path: library/Businessprocess/Monitoring/Backend/Ido/Query/ServiceStatusQuery.php
+
+ -
+ message: "#^Parameter \\#4 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 1
+ path: library/Businessprocess/Monitoring/Backend/Ido/Query/ServiceStatusQuery.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Monitoring\\\\Backend\\\\Ido\\\\Query\\\\ServiceStatusQuery\\:\\:\\$customVarsJoinTemplate has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Monitoring/Backend/Ido/Query/ServiceStatusQuery.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\ConnectionInterface\\:\\:getResource\\(\\)\\.$#"
+ count: 1
+ path: library/Businessprocess/Monitoring/DataView/HostStatus.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Monitoring\\\\DataView\\\\HostStatus\\:\\:__construct\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Monitoring/DataView/HostStatus.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Data\\\\ConnectionInterface\\:\\:getResource\\(\\)\\.$#"
+ count: 1
+ path: library/Businessprocess/Monitoring/DataView/ServiceStatus.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Monitoring\\\\DataView\\\\ServiceStatus\\:\\:__construct\\(\\) has parameter \\$columns with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Monitoring/DataView/ServiceStatus.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:addParent\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:countChildren\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:countChildren\\(\\) has parameter \\$filter with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:enumStateNames\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:getBpConfig\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:getChildren\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:getChildren\\(\\) has parameter \\$filter with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:getDuration\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:getIdentifier\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:getLastStateChange\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:getLink\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:getMissingChildren\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:getName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:getObjectClassName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:getOperators\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:getPaths\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:getSortingState\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:getSortingState\\(\\) has parameter \\$state with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:getState\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:getStateName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:getStateName\\(\\) has parameter \\$state with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:hasAlias\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:hasBeenChanged\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:hasChildren\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:hasChildren\\(\\) has parameter \\$filter with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:hasInfoUrl\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:hasMissingChildren\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:hasParentName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:hasParentName\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:hasParents\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:isAcknowledged\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:isEmpty\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:isHandled\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:isInDowntime\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:isMissing\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:isProblem\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:operatorHtml\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:removeParent\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:removeParent\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:setAck\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:setAck\\(\\) has parameter \\$ack with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:setAckIsOk\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:setBpConfig\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:setDowntime\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:setDowntime\\(\\) has parameter \\$downtime with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:setDowntimeIsOk\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:setLastStateChange\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:setLastStateChange\\(\\) has parameter \\$timestamp with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:setMissing\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:setMissing\\(\\) has parameter \\$missing with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:setState\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:setState\\(\\) has parameter \\$state with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:sortStateTostate\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:sortStateTostate\\(\\) has parameter \\$sortState with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:stateToSortState\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:stateToSortState\\(\\) has parameter \\$state with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:toArray\\(\\) has parameter \\$parent with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:toArray\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:\\$className has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:\\$duration has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:\\$empty has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:\\$missing has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:\\$parents type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:\\$sortStateToStateMap has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:\\$stateNames has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Node\\:\\:\\$stateToSortStateMap has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Node.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of static method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:joinNodeName\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Businessprocess/ProvidedHook/Icingadb/HostActions.php
+
+ -
+ message: "#^Cannot access property \\$name on mixed\\.$#"
+ count: 1
+ path: library/Businessprocess/ProvidedHook/Icingadb/ServiceActions.php
+
+ -
+ message: "#^Parameter \\#2 \\$suffix of static method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:joinNodeName\\(\\) expects string\\|null, mixed given\\.$#"
+ count: 1
+ path: library/Businessprocess/ProvidedHook/Icingadb/ServiceActions.php
+
+ -
+ message: "#^Cannot access offset 'icingacli…' on mixed\\.$#"
+ count: 2
+ path: library/Businessprocess/ProvidedHook/Icingadb/ServiceDetailExtension.php
+
+ -
+ message: "#^Cannot access offset 'icingaweb…' on mixed\\.$#"
+ count: 1
+ path: library/Businessprocess/ProvidedHook/Icingadb/ServiceDetailExtension.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\ProvidedHook\\\\Icingadb\\\\ServiceDetailExtension\\:\\:init\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/ProvidedHook/Icingadb/ServiceDetailExtension.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\ProvidedHook\\\\Icingadb\\\\ServiceDetailExtension\\:\\:\\$commandName \\(string\\) does not accept mixed\\.$#"
+ count: 1
+ path: library/Businessprocess/ProvidedHook/Icingadb/ServiceDetailExtension.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\ProvidedHook\\\\Monitoring\\\\DetailviewExtension\\:\\:init\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/ProvidedHook/Monitoring/DetailviewExtension.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\ProvidedHook\\\\Monitoring\\\\DetailviewExtension\\:\\:\\$commandName \\(string\\) does not accept mixed\\.$#"
+ count: 1
+ path: library/Businessprocess/ProvidedHook/Monitoring/DetailviewExtension.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\ProvidedHook\\\\Monitoring\\\\HostActions\\:\\:getActionsForHost\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/ProvidedHook/Monitoring/HostActions.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\ProvidedHook\\\\Monitoring\\\\ServiceActions\\:\\:getActionsForService\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/ProvidedHook/Monitoring/ServiceActions.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\Breadcrumb\\:\\:renderNode\\(\\) has parameter \\$path with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/Breadcrumb.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\Renderer\\:\\:applyManualSorting\\(\\) has parameter \\$bpNodes with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/Renderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\Renderer\\:\\:applyManualSorting\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/Renderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\Renderer\\:\\:createBadge\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/Renderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\Renderer\\:\\:createBadge\\(\\) has parameter \\$state with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/Renderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\Renderer\\:\\:createBadge\\(\\) has parameter \\$summary with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/Renderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\Renderer\\:\\:createBadgeGroup\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/Renderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\Renderer\\:\\:createBadgeGroup\\(\\) has parameter \\$state with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/Renderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\Renderer\\:\\:createBadgeGroup\\(\\) has parameter \\$summary with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/Renderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\Renderer\\:\\:createUnboundParent\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/Renderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\Renderer\\:\\:getCurrentPath\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/Renderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\Renderer\\:\\:getId\\(\\) has parameter \\$path with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/Renderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\Renderer\\:\\:getNodeClasses\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/Renderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\Renderer\\:\\:getPath\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/Renderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\Renderer\\:\\:getUrl\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/Renderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\Renderer\\:\\:isBreadcrumb\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/Renderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\Renderer\\:\\:renderStateBadges\\(\\) has parameter \\$summary with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/Renderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\Renderer\\:\\:renderStateBadges\\(\\) has parameter \\$totalChildren with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/Renderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\Renderer\\:\\:rendersImportedNode\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/Renderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\Renderer\\:\\:setParentNode\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/Renderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\Renderer\\:\\:setPath\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/Renderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\Renderer\\:\\:setPath\\(\\) has parameter \\$path with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/Renderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\Renderer\\:\\:sort\\(\\) has parameter \\$nodes with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/Renderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\Renderer\\:\\:sort\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/Renderer.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/Renderer.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\Renderer\\:\\:\\$parent \\(Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\) does not accept Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\|null\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/Renderer.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\Renderer\\:\\:\\$path type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/Renderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TileRenderer\\:\\:assemble\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TileRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TileRenderer\\\\NodeTile\\:\\:__construct\\(\\) has parameter \\$path with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TileRenderer/NodeTile.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TileRenderer\\\\NodeTile\\:\\:actions\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TileRenderer/NodeTile.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TileRenderer\\\\NodeTile\\:\\:addActionLinks\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TileRenderer/NodeTile.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TileRenderer\\\\NodeTile\\:\\:addActions\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TileRenderer/NodeTile.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TileRenderer\\\\NodeTile\\:\\:addDetailsActions\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TileRenderer/NodeTile.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TileRenderer\\\\NodeTile\\:\\:buildBaseNodeUrl\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TileRenderer/NodeTile.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TileRenderer\\\\NodeTile\\:\\:getMainNodeUrl\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TileRenderer/NodeTile.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TileRenderer\\\\NodeTile\\:\\:makeBpUrl\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TileRenderer/NodeTile.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TileRenderer\\\\NodeTile\\:\\:\\$name has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TileRenderer/NodeTile.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TileRenderer\\\\NodeTile\\:\\:\\$node has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TileRenderer/NodeTile.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TileRenderer\\\\NodeTile\\:\\:\\$path has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TileRenderer/NodeTile.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TileRenderer\\\\NodeTile\\:\\:\\$renderer has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TileRenderer/NodeTile.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TreeRenderer\\:\\:actionIcon\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TreeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TreeRenderer\\:\\:actionIcon\\(\\) has parameter \\$icon with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TreeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TreeRenderer\\:\\:actionIcon\\(\\) has parameter \\$title with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TreeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TreeRenderer\\:\\:actionIcon\\(\\) has parameter \\$url with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TreeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TreeRenderer\\:\\:assemble\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TreeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TreeRenderer\\:\\:createEditAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TreeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TreeRenderer\\:\\:createInfoAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TreeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TreeRenderer\\:\\:createSimulationAction\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TreeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TreeRenderer\\:\\:getActionIcons\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TreeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TreeRenderer\\:\\:getNodeIcons\\(\\) has parameter \\$path with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TreeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TreeRenderer\\:\\:getOverriddenState\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TreeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TreeRenderer\\:\\:getOverriddenState\\(\\) has parameter \\$fakeState with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TreeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TreeRenderer\\:\\:getStateClassNames\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TreeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TreeRenderer\\:\\:renderAddNewNode\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TreeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TreeRenderer\\:\\:renderAddNewNode\\(\\) has parameter \\$parent with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TreeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TreeRenderer\\:\\:renderBp\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TreeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TreeRenderer\\:\\:renderChild\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TreeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TreeRenderer\\:\\:renderChild\\(\\) has parameter \\$bp with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TreeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TreeRenderer\\:\\:renderChild\\(\\) has parameter \\$path with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TreeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TreeRenderer\\:\\:renderNode\\(\\) has parameter \\$path with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TreeRenderer.php
+
+ -
+ message: "#^Parameter \\#1 \\$node of method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\Renderer\\:\\:getSourceUrl\\(\\) expects Icinga\\\\Module\\\\Businessprocess\\\\BpNode, Icinga\\\\Module\\\\Businessprocess\\\\Node given\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TreeRenderer.php
+
+ -
+ message: "#^Parameter \\#2 \\$parent of method Icinga\\\\Module\\\\Businessprocess\\\\Renderer\\\\TreeRenderer\\:\\:renderChild\\(\\) expects Icinga\\\\Module\\\\Businessprocess\\\\BpNode, Icinga\\\\Module\\\\Businessprocess\\\\Node given\\.$#"
+ count: 1
+ path: library/Businessprocess/Renderer/TreeRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\ServiceNode\\:\\:__construct\\(\\) has parameter \\$object with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/ServiceNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\ServiceNode\\:\\:getHostname\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/ServiceNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\ServiceNode\\:\\:getServiceDescription\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/ServiceNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\ServiceNode\\:\\:getUrl\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/ServiceNode.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\ServiceNode\\:\\:\\$className has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/ServiceNode.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\ServiceNode\\:\\:\\$hostname has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/ServiceNode.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\ServiceNode\\:\\:\\$service has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/ServiceNode.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Simulation\\:\\:__construct\\(\\) has parameter \\$simulations with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Simulation.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Simulation\\:\\:create\\(\\) has parameter \\$simulations with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Simulation.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Simulation\\:\\:getNode\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Simulation.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Simulation\\:\\:hasNode\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Simulation.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Simulation\\:\\:remove\\(\\) has parameter \\$node with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Simulation.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Simulation\\:\\:set\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Simulation.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Simulation\\:\\:set\\(\\) has parameter \\$node with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Simulation.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Simulation\\:\\:set\\(\\) has parameter \\$properties with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Simulation.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Simulation\\:\\:setSimulations\\(\\) has parameter \\$simulations with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Simulation.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Simulation\\:\\:simulations\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Simulation.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Simulation\\:\\:\\$simulations \\(array\\) does not accept mixed\\.$#"
+ count: 1
+ path: library/Businessprocess/Simulation.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Simulation\\:\\:\\$simulations type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Simulation.php
+
+ -
+ message: "#^Cannot access property \\$hex_id on mixed\\.$#"
+ count: 8
+ path: library/Businessprocess/State/IcingaDbState.php
+
+ -
+ message: "#^Cannot access property \\$id on mixed\\.$#"
+ count: 6
+ path: library/Businessprocess/State/IcingaDbState.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\State\\\\IcingaDbState\\:\\:apply\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/State/IcingaDbState.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\State\\\\IcingaDbState\\:\\:handleDbRow\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/State/IcingaDbState.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\State\\\\IcingaDbState\\:\\:handleDbRow\\(\\) has parameter \\$row with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/State/IcingaDbState.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\State\\\\IcingaDbState\\:\\:handleDbRow\\(\\) has parameter \\$type with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/State/IcingaDbState.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\State\\\\IcingaDbState\\:\\:reallyRetrieveStatesFromBackend\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/State/IcingaDbState.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\State\\\\IcingaDbState\\:\\:retrieveStatesFromBackend\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/State/IcingaDbState.php
+
+ -
+ message: "#^Parameter \\#1 \\$msg of method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:addError\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Businessprocess/State/IcingaDbState.php
+
+ -
+ message: "#^Parameter \\#2 \\.\\.\\.\\$arrays of function array_merge expects array, mixed given\\.$#"
+ count: 2
+ path: library/Businessprocess/State/IcingaDbState.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\State\\\\MonitoringState\\:\\:apply\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/State/MonitoringState.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\State\\\\MonitoringState\\:\\:handleDbRow\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/State/MonitoringState.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\State\\\\MonitoringState\\:\\:handleDbRow\\(\\) has parameter \\$row with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/State/MonitoringState.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\State\\\\MonitoringState\\:\\:reallyRetrieveStatesFromBackend\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/State/MonitoringState.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\State\\\\MonitoringState\\:\\:retrieveStatesFromBackend\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/State/MonitoringState.php
+
+ -
+ message: "#^Parameter \\#1 \\$msg of method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:addError\\(\\) expects string, mixed given\\.$#"
+ count: 1
+ path: library/Businessprocess/State/MonitoringState.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\ConfigDiff\\:\\:__construct\\(\\) has parameter \\$a with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/ConfigDiff.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\ConfigDiff\\:\\:__construct\\(\\) has parameter \\$b with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/ConfigDiff.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\ConfigDiff\\:\\:create\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/ConfigDiff.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\ConfigDiff\\:\\:create\\(\\) has parameter \\$a with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/ConfigDiff.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\ConfigDiff\\:\\:create\\(\\) has parameter \\$b with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/ConfigDiff.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\ConfigDiff\\:\\:renderHtmlInline\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/ConfigDiff.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\ConfigDiff\\:\\:renderHtmlSideBySide\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/ConfigDiff.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\ConfigDiff\\:\\:renderTextContext\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/ConfigDiff.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\ConfigDiff\\:\\:renderTextUnified\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/ConfigDiff.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\ConfigDiff\\:\\:\\$a has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/ConfigDiff.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\ConfigDiff\\:\\:\\$b has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/ConfigDiff.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\ConfigDiff\\:\\:\\$diff has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/ConfigDiff.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\ConfigDiff\\:\\:\\$opcodes has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/ConfigDiff.php
+
+ -
+ message: "#^Argument of an invalid type array\\<int, string\\>\\|false supplied for foreach, only iterables are supported\\.$#"
+ count: 5
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Cannot use array destructuring on array\\<int, string\\>\\|false\\.$#"
+ count: 7
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:__construct\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:emptyHeader\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:parseDisplay\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:parseDisplay\\(\\) has parameter \\$line with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:parseError\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:parseError\\(\\) has parameter \\$msg with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:parseExtraLine\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:parseExtraLine\\(\\) has parameter \\$line with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:parseExtraLine\\(\\) has parameter \\$typeLength with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:parseFile\\(\\) has parameter \\$filename with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:parseFile\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:parseHeaderLine\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:parseHeaderLine\\(\\) has parameter \\$line with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:parseInfoUrl\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:parseInfoUrl\\(\\) has parameter \\$line with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:parseLine\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:parseLine\\(\\) has parameter \\$line with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:parseStateOverrides\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:parseStateOverrides\\(\\) has parameter \\$line with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:parseString\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:parseString\\(\\) has parameter \\$string with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:readHeaderString\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:readHeaderString\\(\\) has parameter \\$string with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:readMetadataFromFileHeader\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:readMetadataFromFileHeader\\(\\) has parameter \\$filename with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:readMetadataFromFileHeader\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:readMetadataFromString\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:readMetadataFromString\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:readMetadataFromString\\(\\) has parameter \\$string with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:reallyParseFile\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:reallyParseFile\\(\\) has parameter \\$filename with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:resolveMissingNodes\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:splitCommaSeparated\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:splitCommaSeparated\\(\\) has parameter \\$string with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Offset 0 does not exist on string\\|null\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Parameter \\#1 \\$array of function array_shift expects array, array\\<int, string\\>\\|false given\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Parameter \\#1 \\$haystack of function strpos expects string, string\\|null given\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Parameter \\#1 \\$name of method Icinga\\\\Module\\\\Businessprocess\\\\BpConfig\\:\\:getNode\\(\\) expects string, string\\|null given\\.$#"
+ count: 2
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Parameter \\#1 \\$stream of function fclose expects resource, resource\\|false given\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Parameter \\#1 \\$stream of function fgets expects resource, resource\\|false given\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Parameter \\#1 \\$string of function substr expects string, string\\|null given\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Parameter \\#2 \\$subject of function preg_match expects string, string\\|null given\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Parameter \\#2 \\$subject of function preg_split expects string, string\\|null given\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:\\$missingNodes type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigParser\\:\\:\\$name has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigParser.php
+
+ -
+ message: "#^Argument of an invalid type array\\<int, string\\>\\|false supplied for foreach, only iterables are supported\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigRenderer\\:\\:renderStateOverrides\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigRenderer.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigRenderer\\:\\:\\$config has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigRenderer.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyConfigRenderer\\:\\:\\$renderedNodes type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyConfigRenderer.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyStorage\\:\\:deleteProcess\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyStorage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyStorage\\:\\:getConfigDir\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyStorage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyStorage\\:\\:getFilename\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyStorage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyStorage\\:\\:getFilename\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyStorage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyStorage\\:\\:getSource\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyStorage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyStorage\\:\\:getSource\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyStorage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyStorage\\:\\:hasProcess\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyStorage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyStorage\\:\\:listAllProcessNames\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyStorage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyStorage\\:\\:listProcessNames\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyStorage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyStorage\\:\\:listProcesses\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyStorage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyStorage\\:\\:loadFromString\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyStorage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyStorage\\:\\:loadFromString\\(\\) has parameter \\$string with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyStorage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyStorage\\:\\:loadProcess\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyStorage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\LegacyStorage\\:\\:prepareDefaultConfigDir\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/LegacyStorage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\Storage\\:\\:deleteProcess\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/Storage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\Storage\\:\\:getInstance\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/Storage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\Storage\\:\\:hasProcess\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/Storage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\Storage\\:\\:init\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/Storage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\Storage\\:\\:listAllProcessNames\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/Storage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\Storage\\:\\:listProcessNames\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/Storage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\Storage\\:\\:listProcesses\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/Storage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Storage\\\\Storage\\:\\:loadProcess\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Storage/Storage.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Component\\\\BpDashboardTile\\:\\:__construct\\(\\) has parameter \\$attributes with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Component/BpDashboardTile.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Component\\\\BpDashboardTile\\:\\:__construct\\(\\) has parameter \\$description with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Component/BpDashboardTile.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Component\\\\BpDashboardTile\\:\\:__construct\\(\\) has parameter \\$icon with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Component/BpDashboardTile.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Component\\\\BpDashboardTile\\:\\:__construct\\(\\) has parameter \\$title with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Component/BpDashboardTile.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Component\\\\BpDashboardTile\\:\\:__construct\\(\\) has parameter \\$url with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Component/BpDashboardTile.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Component\\\\BpDashboardTile\\:\\:__construct\\(\\) has parameter \\$urlParams with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Component/BpDashboardTile.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Component\\\\DashboardAction\\:\\:__construct\\(\\) has parameter \\$attributes with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Component/DashboardAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Component\\\\DashboardAction\\:\\:__construct\\(\\) has parameter \\$description with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Component/DashboardAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Component\\\\DashboardAction\\:\\:__construct\\(\\) has parameter \\$icon with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Component/DashboardAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Component\\\\DashboardAction\\:\\:__construct\\(\\) has parameter \\$title with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Component/DashboardAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Component\\\\DashboardAction\\:\\:__construct\\(\\) has parameter \\$url with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Component/DashboardAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Component\\\\DashboardAction\\:\\:__construct\\(\\) has parameter \\$urlParams with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Component/DashboardAction.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Component\\\\RenderedProcessActionBar\\:\\:currentProcessParams\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Component/RenderedProcessActionBar.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Component\\\\WtfTabs\\:\\:render\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Component/WtfTabs.php
+
+ -
+ message: "#^Access to an undefined property Zend_Controller_Action_HelperBroker\\:\\:\\$viewRenderer\\.$#"
+ count: 3
+ path: library/Businessprocess/Web/Controller.php
+
+ -
+ message: "#^Call to an undefined method Zend_Controller_Action_HelperBroker\\:\\:layout\\(\\)\\.$#"
+ count: 2
+ path: library/Businessprocess/Web/Controller.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Controller\\:\\:addTitle\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Controller.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Controller\\:\\:addTitle\\(\\) has parameter \\$title with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Controller.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Controller\\:\\:doNotRender\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Controller.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Controller\\:\\:loadBpConfig\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Controller.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Controller\\:\\:loadForm\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Controller.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Controller\\:\\:loadForm\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Controller.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Controller\\:\\:loadModifiedBpConfig\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Controller.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Controller\\:\\:session\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Controller.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Controller\\:\\:setViewScript\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Controller.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Controller\\:\\:setViewScript\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Controller.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Controller\\:\\:singleTab\\(\\) has parameter \\$label with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Controller.php
+
+ -
+ message: "#^Parameter \\#1 \\$key of function array_key_exists expects int\\|string, mixed given\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Controller.php
+
+ -
+ message: "#^Parameter \\#2 \\$values of function vsprintf expects array\\<bool\\|float\\|int\\|string\\|null\\>, array\\<int, mixed\\> given\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Controller.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\FakeRequest\\:\\:getBaseUrl\\(\\) has parameter \\$raw with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/FakeRequest.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\FakeRequest\\:\\:setConfiguredBaseUrl\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/FakeRequest.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\FakeRequest\\:\\:setConfiguredBaseUrl\\(\\) has parameter \\$url with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/FakeRequest.php
+
+ -
+ message: "#^Cannot call method addError\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 2
+ path: library/Businessprocess/Web/Form/BpConfigBaseForm.php
+
+ -
+ message: "#^Cannot call method getUsername\\(\\) on Icinga\\\\User\\|null\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/BpConfigBaseForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\BpConfigBaseForm\\:\\:listAvailableBackends\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/BpConfigBaseForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\BpConfigBaseForm\\:\\:prepareMetadata\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/BpConfigBaseForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\BpConfigBaseForm\\:\\:setPreferredDecorators\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/BpConfigBaseForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\CsrfToken\\:\\:getSessionId\\(\\) should return string but returns string\\|false\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/CsrfToken.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\Element\\\\IplStateOverrides\\:\\:assemble\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/Element/IplStateOverrides.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\Element\\\\IplStateOverrides\\:\\:getOptions\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/Element/IplStateOverrides.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\Element\\\\IplStateOverrides\\:\\:getValues\\(\\) return type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/Element/IplStateOverrides.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\Element\\\\IplStateOverrides\\:\\:registerAttributeCallbacks\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/Element/IplStateOverrides.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\Element\\\\IplStateOverrides\\:\\:setOptions\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/Element/IplStateOverrides.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\Element\\\\IplStateOverrides\\:\\:\\$options type has no value type specified in iterable type array\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/Element/IplStateOverrides.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\FormLoader\\:\\:load\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/FormLoader.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\FormLoader\\:\\:load\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/FormLoader.php
+
+ -
+ message: "#^Parameter \\#1 \\$array of function array_pop expects array, array\\<int, string\\>\\|false given\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/FormLoader.php
+
+ -
+ message: "#^Cannot call method setDecorators\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickBaseForm.php
+
+ -
+ message: "#^Cannot call method setValue\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 2
+ path: library/Businessprocess/Web/Form/QuickBaseForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickBaseForm\\:\\:addHidden\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickBaseForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickBaseForm\\:\\:addHidden\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickBaseForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickBaseForm\\:\\:addHidden\\(\\) has parameter \\$value with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickBaseForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickBaseForm\\:\\:addHtml\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickBaseForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickBaseForm\\:\\:addHtml\\(\\) has parameter \\$html with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickBaseForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickBaseForm\\:\\:addHtml\\(\\) has parameter \\$options with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickBaseForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickBaseForm\\:\\:addHtmlHint\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickBaseForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickBaseForm\\:\\:addHtmlHint\\(\\) has parameter \\$html with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickBaseForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickBaseForm\\:\\:addHtmlHint\\(\\) has parameter \\$options with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickBaseForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickBaseForm\\:\\:addPrefixPathsForBusinessprocess\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickBaseForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickBaseForm\\:\\:addPrefixPathsForModule\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickBaseForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickBaseForm\\:\\:callZfConstructor\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickBaseForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickBaseForm\\:\\:callZfConstructor\\(\\) has parameter \\$options with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickBaseForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickBaseForm\\:\\:handleOptions\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickBaseForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickBaseForm\\:\\:handleOptions\\(\\) has parameter \\$options with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickBaseForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickBaseForm\\:\\:initializePrefixPaths\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickBaseForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickBaseForm\\:\\:loadForm\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickBaseForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickBaseForm\\:\\:loadForm\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickBaseForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickBaseForm\\:\\:optionalEnum\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickBaseForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickBaseForm\\:\\:optionalEnum\\(\\) has parameter \\$enum with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickBaseForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickBaseForm\\:\\:optionalEnum\\(\\) has parameter \\$nullLabel with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickBaseForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickBaseForm\\:\\:setIcingaModule\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickBaseForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickBaseForm\\:\\:translate\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickBaseForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickBaseForm\\:\\:translate\\(\\) has parameter \\$string with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickBaseForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickBaseForm\\:\\:valueIsEmpty\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickBaseForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickBaseForm\\:\\:valueIsEmpty\\(\\) has parameter \\$value with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickBaseForm.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickBaseForm\\:\\:\\$hintCount has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickBaseForm.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickBaseForm\\:\\:\\$icingaModuleName has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickBaseForm.php
+
+ -
+ message: "#^Cannot call method setHttpResponseCode\\(\\) on Zend_Controller_Response_Abstract\\|null\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Cannot call method setIgnore\\(\\) on Zend_Form_Element\\|null\\.$#"
+ count: 2
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:addException\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:addException\\(\\) has parameter \\$elementName with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:addSimpleDisplayGroup\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:addSimpleDisplayGroup\\(\\) has parameter \\$elements with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:addSimpleDisplayGroup\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:addSimpleDisplayGroup\\(\\) has parameter \\$options with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:addSubmitButtonIfSet\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:beforeSetup\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:beforeValidation\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:beforeValidation\\(\\) has parameter \\$data with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:createIdElement\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:detectName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:getActionFromRequest\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:getSentValue\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:getSentValue\\(\\) has parameter \\$default with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:getSentValue\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:getSubmitLabel\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:getSuccessMessage\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:getSuccessMessage\\(\\) has parameter \\$message with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:getSuccessUrl\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:handleRequest\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:hasBeenSent\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:hasBeenSubmitted\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:hasSubmitButton\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:isApiRequest\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:notifyError\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:notifyError\\(\\) has parameter \\$message with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:notifySuccess\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:notifySuccess\\(\\) has parameter \\$message with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:onFailure\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:onRequest\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:onSetup\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:onSuccess\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:prepareElements\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:pressedButton\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:pressedButton\\(\\) has parameter \\$label with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:pressedButton\\(\\) has parameter \\$name with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:redirectAndExit\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:redirectAndExit\\(\\) has parameter \\$url with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:redirectOnSuccess\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:redirectOnSuccess\\(\\) has parameter \\$message with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:regenerateCsrfToken\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:removeCsrfToken\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:setApiRequest\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:setApiRequest\\(\\) has parameter \\$isApiRequest with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:setHttpResponseCode\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:setHttpResponseCode\\(\\) has parameter \\$code with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:setPreferredDecorators\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:setRequest\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:setSubmitLabel\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:setSubmitLabel\\(\\) has parameter \\$label with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:setSuccessMessage\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:setSuccessMessage\\(\\) has parameter \\$message with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:setSuccessUrl\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:setSuccessUrl\\(\\) has parameter \\$params with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:setSuccessUrl\\(\\) has parameter \\$url with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:setup\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Parameter \\#1 \\$array of function array_pop expects array, array\\<int, string\\>\\|false given\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:\\$deleteButtonName has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:\\$didSetup has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:\\$fakeSubmitButtonName has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:\\$formName has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:\\$hasBeenSent has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:\\$hasBeenSubmitted has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:\\$isApiRequest has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:\\$request has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:\\$submitButtonName has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:\\$submitLabel has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Property Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Form\\\\QuickForm\\:\\:\\$successMessage has no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/QuickForm.php
+
+ -
+ message: "#^Call to an undefined method Icinga\\\\Module\\\\Businessprocess\\\\BpNode\\|Icinga\\\\Module\\\\Businessprocess\\\\MonitoredNode\\:\\:addChild\\(\\)\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/Validator/HostServiceTermValidator.php
+
+ -
+ message: "#^Parameter \\#1 \\$label of method ipl\\\\Web\\\\FormElement\\\\TermInput\\\\Term\\:\\:setLabel\\(\\) expects string, string\\|null given\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Form/Validator/HostServiceTermValidator.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Navigation\\\\Renderer\\\\ProcessProblemsBadge\\:\\:getBpConfigName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Navigation/Renderer/ProcessProblemsBadge.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Navigation\\\\Renderer\\\\ProcessProblemsBadge\\:\\:setBpConfigName\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Navigation/Renderer/ProcessProblemsBadge.php
+
+ -
+ message: "#^Method Icinga\\\\Module\\\\Businessprocess\\\\Web\\\\Navigation\\\\Renderer\\\\ProcessProblemsBadge\\:\\:setBpConfigName\\(\\) has parameter \\$bpConfigName with no type specified\\.$#"
+ count: 1
+ path: library/Businessprocess/Web/Navigation/Renderer/ProcessProblemsBadge.php
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..8c09b51
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,31 @@
+includes:
+ - phpstan-baseline.neon
+
+parameters:
+ level: max
+
+ checkFunctionNameCase: true
+ checkInternalClassCaseSensitivity: true
+ treatPhpDocTypesAsCertain: false
+
+ paths:
+ - application
+ - library
+
+ scanDirectories:
+ - vendor
+
+ excludePaths:
+ - library/Businessprocess/Test
+
+ ignoreErrors:
+ -
+ messages:
+ - '#Unsafe usage of new static\(\)#'
+ - '#. but return statement is missing#'
+ reportUnmatched: false
+
+ universalObjectCratesClasses:
+ - Icinga\Web\View
+ - ipl\Orm\Model
+ - Icinga\Module\Monitoring\Object\MonitoredObject
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..5eaf639
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit backupGlobals="false"
+ backupStaticAttributes="false"
+ colors="true"
+ convertErrorsToExceptions="true"
+ convertNoticesToExceptions="true"
+ convertWarningsToExceptions="true"
+ processIsolation="false"
+ stopOnFailure="false"
+ bootstrap="test/bootstrap.php"
+ >
+ <testsuites>
+ <testsuite name="Businessprocess PHP Unit tests">
+ <directory suffix=".php">test/php</directory>
+ </testsuite>
+ </testsuites>
+ <filter>
+ <whitelist processUncoveredFilesFromWhitelist="true">
+ <directory suffix=".php">library/Businessprocess</directory>
+ <exclude>
+ <directory suffix=".php">library/Businessprocess/Director</directory>
+ </exclude>
+ <exclude>
+ <directory suffix=".php">library/Businessprocess/ProvidedHook</directory>
+ </exclude>
+ </whitelist>
+ </filter>
+</phpunit>
diff --git a/public/css/module.less b/public/css/module.less
new file mode 100644
index 0000000..f048863
--- /dev/null
+++ b/public/css/module.less
@@ -0,0 +1,996 @@
+a:focus {
+ outline: none;
+ text-decoration: underline;
+ &::before {
+ text-decoration: none;
+ }
+}
+
+.action-bar {
+ float: left;
+ display: flex;
+ align-items: center;
+ font-size: 1.3em;
+ color: @icinga-blue;
+
+ > a {
+ &:hover::before {
+ text-decoration: none;
+ }
+
+ &:not(:last-child) {
+ margin-right: 1em;
+ }
+
+ &.button-link {
+ color: @text-color-on-icinga-blue;
+ background: @icinga-blue;
+
+ &:active, &:focus {
+ text-decoration: none;
+ }
+
+ &:last-child {
+ margin-left: auto;
+ }
+ }
+ }
+
+ > div.view-toggle {
+ margin-right: 1em;
+
+ span {
+ color: @gray;
+ margin-right: .5em;
+ }
+
+ a {
+ display: inline-block;
+
+ i {
+ padding: .25em .5em;
+ border: 1px solid @icinga-blue;
+
+ &:before {
+ margin-right: 0;
+ }
+
+ &.active {
+ color: @text-color-on-icinga-blue;
+ background-color: @icinga-blue;
+ }
+
+ &:first-of-type {
+ border-top-left-radius: .25em;
+ border-bottom-left-radius: .25em;
+ }
+ &:last-of-type {
+ border-top-right-radius: .25em;
+ border-bottom-right-radius: .25em;
+ }
+ }
+ }
+ }
+
+ span.disabled {
+ color: @gray;
+ }
+}
+
+.controls {
+ &.sort-control,
+ &.want-fullscreen > a {
+ float: right;
+ }
+}
+
+form a {
+ color: @icinga-blue;
+}
+
+div.bp {
+ margin-bottom: 4px;
+}
+
+div.bp.sortable > .sortable-ghost {
+ opacity: 0.5;
+}
+
+
+/* TreeView */
+
+@vertical-tree-item-gap: .5em;
+
+ul.bp {
+ margin: 0;
+ padding: 0;
+ list-style-type: none;
+
+ .action-link {
+ font-size: 1.3em;
+ line-height: 1;
+ }
+
+ // cursors!!!1
+ &:not([data-sortable-disabled="true"]) {
+ .movable {
+ cursor: grab;
+
+ &.sortable-chosen {
+ cursor: grabbing;
+ }
+ }
+
+ &.progress .movable {
+ cursor: wait;
+ }
+ }
+ &[data-sortable-disabled="true"] {
+ li.process summary {
+ cursor: pointer;
+ }
+ }
+
+ li {
+ > .icon,
+ summary > .icon {
+ opacity: .75;
+ }
+
+ span.state-ball ~ i:last-of-type {
+ margin-right: 0;
+ }
+ }
+
+ // ghost style
+ &.sortable > li.sortable-ghost {
+ > details {
+ position: relative;
+ overflow: hidden;
+ max-height: 30em;
+ background-color: @gray-lighter;
+ border: .2em dotted @gray-light;
+ border-left-width: 0;
+ border-right-width: 0;
+ }
+
+ &.process > .details:after {
+ // TODO: Only apply if content overflows?
+ content: " ";
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ height: 50%;
+ background: linear-gradient(transparent, @body-bg-color);
+ }
+ }
+
+ // header style
+ li.process summary {
+ padding: .291666667em 0;
+ border-bottom: 1px solid @gray-light;
+ user-select: none;
+
+ > .icon:nth-child(1),
+ > .icon:nth-child(2) {
+ min-width: 1.3em; // So that process icons align with their node's icons
+ color: @gray;
+ }
+
+ > span {
+ font-size: 1.25em;
+
+ &.op {
+ padding: .1em .5em;
+ border-radius: .5em;
+ background-color: @gray-semilight;
+ font-weight: bold;
+ font-size: 1em;
+ color: @text-color-on-icinga-blue;
+ }
+ }
+ }
+
+ li.process.sortable-ghost details:not([open]) > summary {
+ border-bottom: none;
+ }
+
+ // TODO: Remove once support for Icinga Web 2.10.x is dropped
+ li.process details:not(.collapsible) {
+ &[open] > summary .expand-icon {
+ display: none;
+ }
+
+ &:not([open]) > summary .collapse-icon {
+ display: none;
+ }
+ }
+
+ // subprocess style
+ li.process > details ul {
+ padding-left: 2em;
+ list-style-type: none;
+
+ &.sortable {
+ min-height: 1em; // Required to be able to move items back to an otherwise empty list
+ }
+ }
+
+ // vertical layout
+ > li {
+ padding: @vertical-tree-item-gap 0;
+
+ &:first-child {
+ margin-top: @vertical-tree-item-gap;
+ }
+
+ &.process {
+ padding-bottom: 0;
+
+ &:first-child {
+ margin-top: 0;
+ padding-top: 0;
+ }
+ }
+ }
+
+ // horizontal layout
+ li.process summary,
+ li:not(.process) {
+ display: flex;
+ align-items: center;
+ padding-left: .25em;
+
+ > * {
+ margin-right: .5em;
+ }
+
+ > :not(.overridden-state) + a.action-link {
+ margin-left: auto; // Let the first action link move everything to the right
+
+ & + a.action-link {
+ margin-left: 0; // But really only the first one
+ }
+ }
+
+ .overridden-state {
+ margin-left: auto;
+ margin-right: 1em;
+
+ i.icon {
+ font-size: 0.75em;
+ line-height: 0.08333em;
+ vertical-align: 0.125em;
+
+ &::before {
+ margin: 0 .3em;
+ }
+ }
+ }
+ }
+
+ // collapse handling
+ li.process details:not([open]) {
+ margin-bottom: (@vertical-tree-item-gap * 2);
+
+ > ul.bp {
+ display: none;
+ }
+ }
+
+ // hover style
+ li.process:hover summary {
+ background-color: @tr-active-color;
+ }
+ li:not(.process):hover {
+ background-color: @tr-active-color;
+ }
+
+ li.process summary > .state-ball,
+ li:not(.process) > .state-ball {
+ border: .15em solid @body-bg-color;
+
+ &.size-s {
+ width: 7em/6em;
+ height: 7em/6em;
+ line-height: 7em/6em;
+ }
+ }
+}
+
+// ** Node inspect broken files **/
+ul.broken-files {
+ .rounded-corners();
+ padding: 1em;
+ margin: 1em 0;
+ border: 2px solid @state-warning;
+ font-size: 1.25em;
+ list-style: none;
+
+ li {
+ padding-left: 1em;
+ font-weight: bold;
+ }
+}
+// ** END Node inspect broken files **/
+
+/** BEGIN Dashboard **/
+.overview-dashboard {
+ .header {
+ font-weight: bold;
+ display: block;
+ font-size: 1.25em;
+ }
+
+ i {
+ float: left;
+ font-size: 2.5em;
+ margin-top: -0.1em;
+ margin-bottom: 2em;
+ color: inherit;
+ }
+
+ .bp-root-tiles {
+ margin-left: 3em;
+ }
+
+ .dashboard-tile {
+ cursor: pointer;
+ padding: 1em;
+
+ &:hover {
+ background-color: @tr-hover-color;
+ }
+
+ .bp-link {
+ > a {
+ text-decoration: none;
+ color: @gray;
+ vertical-align: middle;
+ word-wrap: break-word;
+ width: 100%;
+ overflow: hidden;
+
+ > span.header {
+ color: @text-color;
+ }
+ }
+ }
+ }
+
+ .dashboard-tile,
+ div.action {
+ width: 20em;
+ display: inline-block;
+ vertical-align: top;
+ }
+
+ .action {
+ > a {
+ text-decoration: none;
+ color: @gray;
+ vertical-align: middle;
+ display: block;
+ padding: 1em;
+ word-wrap: break-word;
+ width: 100%;
+ overflow: hidden;
+ box-sizing: border-box;
+
+ &.addnew:hover {
+ background-color: @tr-hover-color;
+ }
+
+ > span.header {
+ color: @text-color;
+ }
+ }
+ }
+}
+/** END Dashboard **/
+
+// State summary badges
+.state-badges {
+ .state-badges();
+
+ &.state-badges li > ul > li:last-child {
+ margin-left: 0;
+ }
+
+ li > ul > li:first-child:not(:last-child) .state-badge {
+ border-right: 0;
+ }
+}
+
+// Node children count
+.item-count {
+ font-size: 1em;
+ text-align: center;
+ color: @text-color-inverted;
+}
+
+div.bp .state-badges {
+ display: inline-block;
+ padding-top: 0;
+}
+
+td > a > .state-badges {
+ background-color: transparent;
+}
+
+.state-badge {
+ font-size: .8em;
+ border: 1px solid @body-bg-color;
+
+ &.state-missing {
+ background: @gray-semilight;
+ color: @text-color-on-icinga-blue;
+ }
+
+ &.state-critical.handled, &.state-down.handled { background: @color-critical-handled; opacity: 1; }
+ &.state-unknown.handled { background-color: @color-unknown-handled; opacity: 1; }
+ &.state-warning.handled { background: @color-warning-handled; opacity: 1; }
+}
+
+/** END Badges **/
+
+/** BEGIN Tiles **/
+.tiles:after {
+ content:'';
+ display:block;
+ clear: both;
+}
+
+.tiles.sortable > .sortable-ghost {
+ opacity: 0.5;
+ border: .2em dashed @gray;
+}
+
+.tiles > div {
+ color: @text-color-on-icinga-blue;
+ width: 12em;
+ display: inline-block;
+ float: left;
+ vertical-align: top;
+ margin-right: 0.2em;
+ margin-bottom: 0.2em;
+ height: 6em;
+ cursor: pointer;
+ position: relative;
+
+ .item-count {
+ margin-right: .5em;
+ }
+
+ .state-badges {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ margin: 0.5em;
+ text-align: center;
+ font-size: 0.5em;
+ }
+
+ .overridden-state {
+ font-size: .75em;
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ margin: .5em;
+ border: 1px solid @body-bg-color;
+ }
+
+ > a {
+ display: block;
+ text-decoration: none;
+ font-size: 0.7em;
+ text-align: center;
+ padding: 1em 1em 0;
+ font-weight: bold;
+ word-wrap: break-word;
+ }
+
+ &:hover {
+ box-shadow: 0 0 .2em @gray;
+ }
+
+ .actions {
+ opacity: 0.8;
+ margin: 0.5em 0.5em 0 0.5em;
+ font-size: 0.75em;
+ height: 1.8em;
+
+ i {
+ float: none;
+ display: block;
+ width: 100%;
+ font-size: 1em;
+ line-height: normal;
+ margin: 0;
+ padding: 0 0 0 0.25em;
+
+ &.handled-icon {
+ display: inline-block;
+ margin-top: 0.15em;
+ float: right;
+ width: 1.5em;
+ height: 1.5em;
+ }
+ }
+ a {
+ margin: 0;
+ padding: 0;
+ display: inline-block;
+ width: 1.5em;
+ height: 1.5em;
+ border-radius: 0.3em;
+ }
+
+ a:hover {
+ background-color: @body-bg-color;
+ color: @text-color;
+ }
+
+ > .node-info {
+ margin-right: 0.3em;
+ float: right;
+ }
+ }
+}
+
+.tiles.sortable:not([data-sortable-disabled="true"]) {
+ > div {
+ cursor: grab;
+
+ &.sortable-chosen {
+ cursor: grabbing;
+ }
+ }
+
+ &.progress > div {
+ cursor: wait;
+ }
+}
+
+.tiles > div.parent::before {
+ content: '&';
+ position: absolute;
+ font-size: 1.2em;
+}
+
+.tiles > div.parent {
+ width: 100%;
+ height: 2em;
+}
+
+.tiles {
+ > .critical { background-color: @color-critical; > a { text-shadow: 0 0 1px mix(#000, @color-critical, 40%); }}
+ > .critical.handled { background-color: @color-critical-handled; > a { text-shadow: 0 0 1px mix(#000, @color-critical-handled, 40%); }}
+ > .down { background-color: @color-critical; > a { text-shadow: 0 0 1px mix(#000, @color-critical, 40%); }}
+ > .down.handled { background-color: @color-critical-handled; > a { text-shadow: 0 0 1px mix(#000, @color-critical-handled, 40%); }}
+ > .unknown { background-color: @color-unknown; > a { text-shadow: 0 0 1px mix(#000, @color-unknown, 40%); }}
+ > .unknown.handled { background-color: @color-unknown-handled; > a { text-shadow: 0 0 1px mix(#000, @color-unknown-handled, 40%); }}
+ > .unreachable { background-color: @color-unknown; > a { text-shadow: 0 0 1px mix(#000, @color-unknown, 40%); }}
+ > .unreachable.handled { background-color: @color-unknown-handled; > a { text-shadow: 0 0 1px mix(#000, @color-unknown-handled, 40%); }}
+ > .warning { background-color: @color-warning; > a { text-shadow: 0 0 1px mix(#000, @color-warning, 40%); }}
+ > .warning.handled { background-color: @color-warning-handled; > a { text-shadow: 0 0 1px mix(#000, @color-warning-handled, 40%); }}
+ > .ok { background-color: @color-ok; > a { text-shadow: 0 0 1px mix(#000, @color-ok, 40%); }}
+ > .up { background-color: @color-ok; > a { text-shadow: 0 0 1px mix(#000, @color-ok, 40%); }}
+ > .pending { background-color: @color-pending; > a { text-shadow: 0 0 1px mix(#000, @color-pending, 40%); }}
+ > .missing { background-color: @gray-semilight; > a { color: @text-color-on-icinga-blue; }}
+ > .empty { background-color: @gray-semilight; > a { color: @text-color-on-icinga-blue; }}
+}
+
+.tiles.few { font-size: 2.5em; }
+.tiles.normal { font-size: 2.1em; }
+.tiles.many { font-size: 1.8em; }
+
+#layout.twocols, #layout.layout-minimal, div.compact {
+ .tiles.few { font-size: 1.8em; }
+ .tiles.normal { font-size: 1.8em; }
+ .tiles.many { font-size: 1.8em; }
+}
+
+#layout.fullscreen-layout .controls {
+ padding: 0 1em;
+}
+
+/** END of tiles **/
+
+.content.restricted {
+ h1 {
+ font-size: 2em;
+ }
+
+ p > a {
+ margin-left: 1em;
+ }
+}
+
+/** BEGIN breadcrumb **/
+
+.breadcrumb {
+ list-style: none;
+ overflow: hidden;
+ padding: 0;
+}
+
+.breadcrumb:after {
+ content:'';
+ display:block;
+ clear: both;
+}
+.breadcrumb li {
+ float: left;
+ cursor: pointer;
+ user-select: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+
+}
+.breadcrumb li a {
+ color: @icinga-blue;
+ margin: 0;
+ font-size: 1.2em;
+ text-decoration: none;
+ padding-left: 2em;
+ line-height: 2.5em;
+ position: relative;
+ display: block;
+ float: left;
+ &:focus {
+ outline: none;
+ }
+
+ > .state-ball {
+ margin-right: .5em;
+ border: .15em solid @body-bg-color;
+
+ &.size-s {
+ width: 7em/6em;
+ height: 7em/6em;
+ line-height: 7em/6em;
+ }
+ }
+}
+.breadcrumb li {
+ border: 1px solid @gray-lighter;
+
+ &:first-of-type {
+ border-radius: .25em;
+ }
+
+ &:last-of-type {
+ border-radius: .25em;
+ border: 1px solid transparent;
+ background: @icinga-blue;
+ color: @text-color-on-icinga-blue;
+ padding-right: 1.2em;
+
+ a {
+ color: @text-color-on-icinga-blue;
+ }
+ }
+}
+
+.breadcrumb li:not(:last-of-type) a:before, .breadcrumb li:not(:last-of-type) a:after {
+ content: " ";
+ display: block;
+ width: 0;
+ height: 0;
+ border-top: 1.3em solid transparent;
+ border-bottom: 1.2em solid transparent;
+ position: absolute;
+ margin-top: -1.2em;
+ top: 50%;
+ left: 100%;
+}
+
+.breadcrumb li:not(:last-of-type) a:before {
+ border-left: 1.2em solid @gray-lighter;
+ margin-left: 1px;
+ z-index: 1;
+}
+
+.breadcrumb li:not(:last-of-type) a:after {
+ border-left: 1.2em solid @body-bg-color;
+ z-index: 2;
+}
+
+&.impact {
+ .breadcrumb li:not(:last-of-type) a:after {
+ .transition(border-left-color 2s 0.66s linear ~'!important');
+ border-left-color: @gray-lighter;
+ }
+
+ .breadcrumb li:not(:last-of-type) a:before {
+ .transition(border-left-color 2s 1s linear ~'!important');
+ border-left-color: @gray-light;
+ }
+
+ .breadcrumb li:not(:last-of-type) {
+ .transition(border-color 2s 1s linear ~'!important');
+ border-color: @gray-light;
+ }
+ .breadcrumb li:not(:last-of-type) a:hover {
+ background-color: transparent !important;
+ color: @icinga-blue;
+ }
+}
+
+.tabs > .dropdown-nav-item > ul {
+ z-index: 100;
+}
+
+.breadcrumb li:first-child a {
+ padding-left: 1em;
+ padding-right: 0.5em;
+}
+
+.breadcrumb li:not(:last-child) a:hover { background: @icinga-blue; color: @text-color-on-icinga-blue; }
+.breadcrumb li:not(:last-child) a:hover:after { border-left-color: @icinga-blue; }
+.breadcrumb li:last-child:hover, .breadcrumb li:last-child a:hover { background: @icinga-blue; border-color: @icinga-blue; }
+
+.breadcrumb li a:focus {
+ text-decoration: underline;
+}
+
+#layout.twocols, #layout.layout-minimal, div.compact {
+ .breadcrumb {
+ font-size: 0.833em;
+ }
+}
+
+/** END of breadcrumb **/
+
+
+ul.error, ul.warning {
+ padding: 0;
+ list-style-type: none;
+ background-color: @color-critical;
+
+ li {
+ font-weight: bold;
+ color: @text-color-on-icinga-blue;
+ padding: 0.3em 0.8em;
+ }
+
+ li a,
+ li .link-button {
+ color: inherit;
+ text-decoration: underline;
+
+ &:hover {
+ text-decoration: none;
+ }
+ }
+}
+
+
+ul.warning {
+ background-color: @color-warning;
+}
+
+table.sourcecode {
+ font-family: monospace;
+ white-space: pre-wrap;
+
+ th {
+ vertical-align: top;
+ padding-right: 0.5em;
+ user-select: none;
+ -moz-user-select: none;
+ -o-user-select: none;
+ -ms-user-select: none;
+ -webkit-user-select: none;
+ font-weight: bold;
+ }
+ td {
+ vertical-align: top;
+ }
+}
+
+/** Forms stolen from director **/
+.content form {
+ margin-bottom: 2em;
+}
+
+.content form.inline {
+ margin: 0;
+}
+
+.invisible {
+ position: absolute;
+ left: -100%;
+}
+
+form.bp-form {
+ input[type=file] {
+ padding-right: 1em;
+ }
+
+ input[type=submit]:first-of-type {
+ border-width: 2px;
+ }
+
+ p.description {
+ padding: 1em 1em;
+ margin: 0;
+ font-style: italic;
+ width: 100%;
+ }
+
+ ul.form-errors {
+ margin-bottom: 0.5em;
+
+ ul.errors li {
+ background: @color-critical;
+ font-weight: bold;
+ padding: 0.5em 1em;
+ color: @text-color-on-icinga-blue;
+ }
+ }
+
+ input[type=text], input[type=password], input[type=file], textarea, select {
+ max-width: 36em;
+ min-width: 20em;
+ width: 100%;
+ }
+
+ label {
+ line-height: 2em;
+ }
+
+ dl {
+ margin: 0;
+ padding: 0;
+ }
+
+ select option {
+ padding-left: 0.5em;
+ }
+
+ dt label {
+ width: auto;
+ font-weight: normal;
+ font-size: inherit;
+
+ &.required {
+ &::after {
+ content: '*'
+ }
+ }
+
+ &:hover {
+ text-decoration: underline;
+ cursor: pointer;
+ }
+ }
+
+ fieldset {
+ min-width: 36em;
+ }
+
+ dd input.related-action[type='submit'] {
+ display: none;
+ }
+
+ dd.active li.active input.related-action[type='submit'] {
+ display: inline-block;
+ }
+
+ dd.active {
+ p.description {
+ color: inherit;
+ font-style: normal;
+ }
+ }
+
+ dd {
+ padding: 0.3em 0.5em;
+ margin: 0;
+ }
+
+ dt {
+ padding: 0.5em 0.5em;
+ margin: 0;
+ }
+
+ dt.active, dd.active {
+ background-color: @tr-active-color;
+ }
+
+ dt {
+ display: inline-block;
+ vertical-align: top;
+ min-width: 12em;
+ min-height: 2.5em;
+ width: 30%;
+ &.errors label {
+ color: @color-critical;
+ }
+ }
+
+ .errors label {
+ color: @color-critical;
+ }
+
+ dd {
+ display: inline-block;
+ width: 63%;
+ min-height: 2.5em;
+ vertical-align: top;
+ margin: 0;
+ &.errors {
+ input[type=text], select {
+ border-color: @color-critical;
+ }
+ }
+
+ &.full-width {
+ padding: 0.5em;
+ width: 100%;
+ }
+ }
+
+ dd:after {
+ display: block;
+ content: '';
+ }
+
+ textarea {
+ height: auto;
+ }
+
+ dd ul.errors {
+ list-style-type: none;
+ padding-left: 0.3em;
+
+ li {
+ color: @color-critical;
+ padding: 0.3em;
+ }
+ }
+
+
+ #_FAKE_SUBMIT {
+ position: absolute;
+ left: -100%;
+ }
+}
+
+/** END of forms **/
+
+/* Form fallback styles, remove once <=2.9.5 support is dropped */
+
+.icinga-controls {
+ input[type="file"] {
+ background-color: @low-sat-blue;
+ }
+
+ button[type="button"] {
+ background-color: @low-sat-blue;
+ }
+}
+
+form.icinga-form {
+ input[type="file"] {
+ flex: 1 1 auto;
+ width: 0;
+ }
+
+ button[type="button"] {
+ line-height: normal;
+ }
+}
+
+/* Form fallback styles end */
+
+/** Custom font styling **/
+textarea.smaller {
+ font-size: 0.833em;
+ max-width: 60em;
+}
+/** END of custom font styling **/
diff --git a/public/img/ack.gif b/public/img/ack.gif
new file mode 100644
index 0000000..cda95a8
--- /dev/null
+++ b/public/img/ack.gif
Binary files differ
diff --git a/public/img/downtime.gif b/public/img/downtime.gif
new file mode 100644
index 0000000..1687798
--- /dev/null
+++ b/public/img/downtime.gif
Binary files differ
diff --git a/public/img/help.gif b/public/img/help.gif
new file mode 100644
index 0000000..9226497
--- /dev/null
+++ b/public/img/help.gif
Binary files differ
diff --git a/public/img/icon_collapse.png b/public/img/icon_collapse.png
new file mode 100644
index 0000000..0c7f37b
--- /dev/null
+++ b/public/img/icon_collapse.png
Binary files differ
diff --git a/public/img/icon_expand.png b/public/img/icon_expand.png
new file mode 100644
index 0000000..19862cf
--- /dev/null
+++ b/public/img/icon_expand.png
Binary files differ
diff --git a/public/js/behavior/sortable.js b/public/js/behavior/sortable.js
new file mode 100644
index 0000000..8f32ab7
--- /dev/null
+++ b/public/js/behavior/sortable.js
@@ -0,0 +1,47 @@
+/*! Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+(function(Icinga, $) {
+
+ 'use strict';
+
+ Icinga.Behaviors = Icinga.Behaviors || {};
+
+ var Sortable = function (icinga) {
+ Icinga.EventListener.call(this, icinga);
+ this.on('rendered', this.onRendered, this);
+ };
+
+ Sortable.prototype = new Icinga.EventListener();
+
+ Sortable.prototype.onRendered = function(e) {
+ $(e.target).find('.sortable').each(function() {
+ var $el = $(this);
+ var options = {
+ scroll: $el.closest('.container')[0],
+ onMove: function (/**Event*/ event, /**Event*/ originalEvent) {
+ if (typeof this.options['filter'] !== 'undefined' && $(event.related).is(this.options['filter'])) {
+ // Assumes the filtered item is either at the very start or end of the list and prevents the
+ // user from dropping other items before (if at the very start) or after it.
+ return false;
+ }
+ }
+ };
+
+ $.each($el.data(), function (i, v) {
+ if (i.length > 8 && i.substring(0, 8) === 'sortable') {
+ options[i.charAt(8).toLowerCase() + i.substr(9)] = v;
+ }
+ });
+
+ if (typeof options.group !== 'undefined' && typeof options.group.put === 'string' && options.group.put.substring(0, 9) === 'function:') {
+ var module = icinga.module($el.closest('.icinga-module').data('icingaModule'));
+ options.group.put = module.object[options.group.put.substr(9)];
+ }
+
+ $(this).sortable(options);
+ });
+ };
+
+ Icinga.Behaviors.Sortable = Sortable;
+
+})(Icinga, jQuery);
diff --git a/public/js/module.js b/public/js/module.js
new file mode 100644
index 0000000..4855c9c
--- /dev/null
+++ b/public/js/module.js
@@ -0,0 +1,287 @@
+
+(function(Icinga) {
+
+ var Bp = function(module) {
+ /**
+ * YES, we need Icinga
+ */
+ this.module = module;
+
+ this.idCache = {};
+
+ this.initialize();
+
+ this.module.icinga.logger.debug('BP module loaded');
+ };
+
+ Bp.prototype = {
+
+ initialize: function()
+ {
+ /**
+ * Tell Icinga about our event handlers
+ */
+ this.module.on('rendered', this.onRendered);
+
+ this.module.on('focus', 'form input, form textarea, form select', this.formElementFocus);
+
+ this.module.on('click', 'li.process summary:not(.collapsible-control)', this.processHeaderClick);
+ this.module.on('end', 'ul.sortable', this.rowDropped);
+
+ this.module.on('click', 'div.tiles > div', this.tileClick);
+ this.module.on('click', '.dashboard-tile', this.dashboardTileClick);
+ this.module.on('end', 'div.tiles.sortable', this.tileDropped);
+
+ this.module.on('choose', '.sortable', this.suspendAutoRefresh);
+ this.module.on('unchoose', '.sortable', this.resumeAutoRefresh);
+
+ this.module.icinga.logger.debug('BP module initialized');
+ },
+
+ onRendered: function (event) {
+ var $container = $(event.currentTarget);
+ this.fixFullscreen($container);
+ this.restoreCollapsedBps(event.target);
+ this.highlightFormErrors($container);
+ this.hideInactiveFormDescriptions($container);
+ this.fixTileLinksOnDashboard($container);
+ },
+
+ // TODO: Remove once support for Icinga Web 2.10.x is dropped
+ processHeaderClick: function (event) {
+ event.stopPropagation();
+ event.preventDefault();
+
+ let details = event.currentTarget.parentNode;
+ details.open = ! details.open;
+
+ let bpUl = event.currentTarget.closest('.content > ul.bp');
+ if (! bpUl || ! ('isRootConfig' in bpUl.dataset)) {
+ return;
+ }
+
+ let bpName = bpUl.id;
+ if (typeof this.idCache[bpName] === 'undefined') {
+ this.idCache[bpName] = [];
+ }
+
+ let li = details.parentNode;
+ let index = this.idCache[bpName].indexOf(li.id);
+ if (! details.open) {
+ if (index === -1) {
+ this.idCache[bpName].push(li.id);
+ }
+ } else if (index !== -1) {
+ this.idCache[bpName].splice(index, 1);
+ }
+ },
+
+ hideInactiveFormDescriptions: function($container) {
+ $container.find('dd').not('.active').find('p.description').hide();
+ },
+
+ tileClick: function(event) {
+ $(event.currentTarget).find('> a').first().trigger('click');
+ },
+
+ dashboardTileClick: function(event) {
+ $(event.currentTarget).find('> .bp-link > a').first().trigger('click');
+ },
+
+ suspendAutoRefresh: function(event) {
+ // TODO: If there is a better approach some time, let me know
+ $(event.originalEvent.from).closest('.container').data('lastUpdate', (new Date()).getTime() + 3600 * 1000);
+ event.stopPropagation();
+ },
+
+ resumeAutoRefresh: function(event) {
+ var $container = $(event.originalEvent.from).closest('.container');
+ $container.data('lastUpdate', (new Date()).getTime() - ($container.data('icingaRefresh') || 10) * 1000);
+ event.stopPropagation();
+ },
+
+ tileDropped: function(event) {
+ var evt = event.originalEvent;
+ if (evt.oldIndex !== evt.newIndex) {
+ var $source = $(evt.from);
+ $source.addClass('progress')
+ .data('sortable').option('disabled', true);
+
+ var data = {
+ csrfToken: $source.data('csrfToken'),
+ movenode: 'movenode', // That's the submit button..
+ parent: $(evt.to).data('nodeName') || '',
+ from: evt.oldIndex,
+ to: evt.newIndex
+ };
+
+ var actionUrl = [
+ $source.data('actionUrl'),
+ 'action=move',
+ 'movenode=' + $(evt.item).data('nodeName')
+ ].join('&');
+
+ var $container = $source.closest('.container');
+ icinga.loader.loadUrl(actionUrl, $container, data, 'POST');
+ }
+ },
+
+ rowDropped: function(event) {
+ var evt = event.originalEvent,
+ $source = $(evt.from),
+ $target = $(evt.to);
+
+ if (evt.oldIndex !== evt.newIndex || !$target.is($source)) {
+ var $root = $target.closest('.content > ul.bp');
+ $root.addClass('progress')
+ .find('ul.bp')
+ .add($root)
+ .each(function() {
+ $(this).data('sortable').option('disabled', true);
+ });
+
+ var data = {
+ csrfToken: $target.data('csrfToken'),
+ movenode: 'movenode', // That's the submit button..
+ parent: $target.closest('.process').data('nodeName') || '',
+ from: evt.oldIndex,
+ to: evt.newIndex
+ };
+
+ var actionUrl = [
+ $source.data('actionUrl'),
+ 'action=move',
+ 'movenode=' + $(evt.item).data('nodeName')
+ ].join('&');
+
+ var $container = $target.closest('.container');
+ icinga.loader.loadUrl(actionUrl, $container, data, 'POST');
+ event.stopPropagation();
+ }
+ },
+
+ /**
+ * Called by Sortable.js while in Tree-View
+ *
+ * See group option on the sortable elements.
+ *
+ * @param to
+ * @param from
+ * @param item
+ * @param event
+ * @returns boolean
+ */
+ rowPutAllowed: function(to, from, item, event) {
+ if (to.options.group.name === 'root') {
+ return $(item).is('.process');
+ }
+
+ // Otherwise we're facing a nesting error next
+ var $item = $(item),
+ childrenNames = $item.find('.process').map(function () {
+ return $(this).data('nodeName');
+ }).get();
+ childrenNames.push($item.data('nodeName'));
+ var loopDetected = $(to.el).parents('.process').toArray().some(function (parent) {
+ return childrenNames.indexOf($(parent).data('nodeName')) !== -1;
+ });
+
+ return !loopDetected;
+ },
+
+ fixTileLinksOnDashboard: function($container) {
+ if ($container.closest('div.dashboard').length) {
+ $container.find('div.tiles').data('baseTarget', '_next');
+ }
+ },
+
+ fixFullscreen: function($container) {
+ var $controls = $container.find('div.controls');
+ var $layout = $('#layout');
+ var icinga = this.module.icinga;
+ if ($controls.hasClass('want-fullscreen')) {
+ if (!$layout.hasClass('fullscreen-layout')) {
+
+ $layout.addClass('fullscreen-layout');
+ $controls.removeAttr('style');
+ $container.find('.fake-controls').remove();
+ icinga.ui.currentLayout = 'fullscreen';
+ }
+ } else if (! $container.parent('.dashboard').length) {
+ if ($layout.hasClass('fullscreen-layout')) {
+ $layout.removeClass('fullscreen-layout');
+ icinga.ui.layoutHasBeenChanged();
+ icinga.ui.initializeControls($container);
+ }
+ }
+ },
+
+ // TODO: Remove once support for Icinga Web 2.10.x is dropped
+ restoreCollapsedBps: function(container) {
+ let bpUl = container.querySelector('.content > ul.bp');
+ if (! bpUl || ! ('isRootConfig' in bpUl.dataset)) {
+ return;
+ }
+
+ let bpName = bpUl.id;
+ if (typeof this.idCache[bpName] === 'undefined') {
+ return;
+ }
+
+ bpUl.querySelectorAll('li.process').forEach(li => {
+ if (this.idCache[bpName].indexOf(li.id) !== -1) {
+ li.querySelector(':scope > details').open = false;
+ }
+ });
+ },
+
+ /** BEGIN Form handling, borrowed from Director **/
+ formElementFocus: function(ev)
+ {
+ var $input = $(ev.currentTarget);
+ var $dd = $input.closest('dd');
+ $dd.find('p.description').show();
+ if ($dd.attr('id') && $dd.attr('id').match(/button/)) {
+ return;
+ }
+ var $li = $input.closest('li');
+ var $dt = $dd.prev();
+ var $form = $dd.closest('form');
+
+ $form.find('dt, dd, li').removeClass('active');
+ $li.addClass('active');
+ $dt.addClass('active');
+ $dd.addClass('active');
+ $dd.find('p.description.fading-out')
+ .stop(true)
+ .removeClass('fading-out')
+ .fadeIn('fast');
+
+ $form.find('dd').not($dd)
+ .find('p.description')
+ .not('.fading-out')
+ .addClass('fading-out')
+ .delay(2000)
+ .fadeOut('slow', function() {
+ $(this).removeClass('fading-out').hide()
+ });
+ },
+
+ highlightFormErrors: function($container)
+ {
+ $container.find('dd ul.errors').each(function(idx, ul) {
+ var $ul = $(ul);
+ var $dd = $ul.closest('dd');
+ var $dt = $dd.prev();
+
+ $dt.addClass('errors');
+ $dd.addClass('errors');
+ });
+ }
+ /** END Form handling **/
+ };
+
+ Icinga.availableModules.businessprocess = Bp;
+
+}(Icinga));
+
diff --git a/public/js/vendor/Sortable.js b/public/js/vendor/Sortable.js
new file mode 100644
index 0000000..edb4e1c
--- /dev/null
+++ b/public/js/vendor/Sortable.js
@@ -0,0 +1,2349 @@
+/**!
+ * Sortable
+ * @author RubaXa <trash@rubaxa.org>
+ * @author owenm <owen23355@gmail.com>
+ * @license MIT
+ */
+
+(function sortableModule(factory) {
+ "use strict";
+
+ if (typeof define === "function" && define.amd) {
+ define(factory);
+ }
+ else if (typeof module != "undefined" && typeof module.exports != "undefined") {
+ module.exports = factory();
+ }
+ else {
+ /* jshint sub:true */
+ window["Sortable"] = factory();
+ }
+})(function sortableFactory() {
+ "use strict";
+
+ if (typeof window === "undefined" || !window.document) {
+ return function sortableError() {
+ throw new Error("Sortable.js requires a window with a document");
+ };
+ }
+
+ var dragEl,
+ parentEl,
+ ghostEl,
+ cloneEl,
+ rootEl,
+ nextEl,
+ lastDownEl,
+
+ scrollEl,
+ scrollParentEl,
+ scrollCustomFn,
+
+ oldIndex,
+ newIndex,
+
+ activeGroup,
+ putSortable,
+
+ autoScrolls = [],
+ scrolling = false,
+
+ awaitingDragStarted = false,
+ ignoreNextClick = false,
+ sortables = [],
+
+ pointerElemChangedInterval,
+ lastPointerElemX,
+ lastPointerElemY,
+
+ tapEvt,
+ touchEvt,
+
+ moved,
+
+
+ lastTarget,
+ lastDirection,
+ pastFirstInvertThresh = false,
+ isCircumstantialInvert = false,
+ lastMode, // 'swap' or 'insert'
+
+ targetMoveDistance,
+
+
+ forRepaintDummy,
+ realDragElRect, // dragEl rect after current animation
+
+ /** @const */
+ R_SPACE = /\s+/g,
+
+ expando = 'Sortable' + (new Date).getTime(),
+
+ win = window,
+ document = win.document,
+ parseInt = win.parseInt,
+ setTimeout = win.setTimeout,
+
+ $ = win.jQuery || win.Zepto,
+ Polymer = win.Polymer,
+
+ captureMode = {
+ capture: false,
+ passive: false
+ },
+
+ IE11OrLess = !!navigator.userAgent.match(/(?:Trident.*rv[ :]?11\.|msie|iemobile)/i),
+ Edge = !!navigator.userAgent.match(/Edge/i),
+ // FireFox = !!navigator.userAgent.match(/firefox/i),
+
+ CSSFloatProperty = Edge || IE11OrLess ? 'cssFloat' : 'float',
+
+ // This will not pass for IE9, because IE9 DnD only works on anchors
+ supportDraggable = ('draggable' in document.createElement('div')),
+
+ supportCssPointerEvents = (function() {
+ // false when <= IE11
+ if (IE11OrLess) {
+ return false;
+ }
+ var el = document.createElement('x');
+ el.style.cssText = 'pointer-events:auto';
+ return el.style.pointerEvents === 'auto';
+ })(),
+
+ _silent = false,
+ _alignedSilent = false,
+
+ abs = Math.abs,
+ min = Math.min,
+
+ savedInputChecked = [],
+
+ _detectDirection = function(el, options) {
+ var elCSS = _css(el),
+ elWidth = parseInt(elCSS.width),
+ child1 = _getChild(el, 0, options),
+ child2 = _getChild(el, 1, options),
+ firstChildCSS = child1 && _css(child1),
+ secondChildCSS = child2 && _css(child2),
+ firstChildWidth = firstChildCSS && parseInt(firstChildCSS.marginLeft) + parseInt(firstChildCSS.marginRight) + _getRect(child1).width,
+ secondChildWidth = secondChildCSS && parseInt(secondChildCSS.marginLeft) + parseInt(secondChildCSS.marginRight) + _getRect(child2).width;
+ if (elCSS.display === 'flex') {
+ return elCSS.flexDirection === 'column' || elCSS.flexDirection === 'column-reverse'
+ ? 'vertical' : 'horizontal';
+ }
+ if (child1 && firstChildCSS.float !== 'none') {
+ var touchingSideChild2 = firstChildCSS.float === 'left' ? 'left' : 'right';
+
+ return child2 && (secondChildCSS.clear === 'both' || secondChildCSS.clear === touchingSideChild2) ?
+ 'vertical' : 'horizontal';
+ }
+ return (child1 &&
+ (
+ firstChildCSS.display === 'block' ||
+ firstChildCSS.display === 'flex' ||
+ firstChildCSS.display === 'table' ||
+ firstChildCSS.display === 'grid' ||
+ firstChildWidth >= elWidth &&
+ elCSS[CSSFloatProperty] === 'none' ||
+ child2 &&
+ elCSS[CSSFloatProperty] === 'none' &&
+ firstChildWidth + secondChildWidth > elWidth
+ ) ?
+ 'vertical' : 'horizontal'
+ );
+ },
+
+ /**
+ * Detects first nearest empty sortable to X and Y position using emptyInsertThreshold.
+ * @param {Number} x X position
+ * @param {Number} y Y position
+ * @return {HTMLElement} Element of the first found nearest Sortable
+ */
+ _detectNearestEmptySortable = function(x, y) {
+ for (var i = 0; i < sortables.length; i++) {
+ if (sortables[i].children.length) continue;
+
+ var rect = _getRect(sortables[i]),
+ threshold = sortables[i][expando].options.emptyInsertThreshold,
+ insideHorizontally = x >= (rect.left - threshold) && x <= (rect.right + threshold),
+ insideVertically = y >= (rect.top - threshold) && y <= (rect.bottom + threshold);
+
+ if (insideHorizontally && insideVertically) {
+ return sortables[i];
+ }
+ }
+ },
+
+ _isClientInRowColumn = function(x, y, el, axis, options) {
+ var targetRect = _getRect(el),
+ targetS1Opp = axis === 'vertical' ? targetRect.left : targetRect.top,
+ targetS2Opp = axis === 'vertical' ? targetRect.right : targetRect.bottom,
+ mouseOnOppAxis = axis === 'vertical' ? x : y;
+
+ return targetS1Opp < mouseOnOppAxis && mouseOnOppAxis < targetS2Opp;
+ },
+
+ _isElInRowColumn = function(el1, el2, axis) {
+ var el1Rect = el1 === dragEl && realDragElRect || _getRect(el1),
+ el2Rect = el2 === dragEl && realDragElRect || _getRect(el2),
+ el1S1Opp = axis === 'vertical' ? el1Rect.left : el1Rect.top,
+ el1S2Opp = axis === 'vertical' ? el1Rect.right : el1Rect.bottom,
+ el1OppLength = axis === 'vertical' ? el1Rect.width : el1Rect.height,
+ el2S1Opp = axis === 'vertical' ? el2Rect.left : el2Rect.top,
+ el2S2Opp = axis === 'vertical' ? el2Rect.right : el2Rect.bottom,
+ el2OppLength = axis === 'vertical' ? el2Rect.width : el2Rect.height;
+
+ return (
+ el1S1Opp === el2S1Opp ||
+ el1S2Opp === el2S2Opp ||
+ (el1S1Opp + el1OppLength / 2) === (el2S1Opp + el2OppLength / 2)
+ );
+ },
+
+ _getParentAutoScrollElement = function(el, includeSelf) {
+ // skip to window
+ if (!el || !el.getBoundingClientRect) return win;
+
+ var elem = el;
+ var gotSelf = false;
+ do {
+ // we don't need to get elem css if it isn't even overflowing in the first place (performance)
+ if (elem.clientWidth < elem.scrollWidth || elem.clientHeight < elem.scrollHeight) {
+ var elemCSS = _css(elem);
+ if (
+ elem.clientWidth < elem.scrollWidth && (elemCSS.overflowX == 'auto' || elemCSS.overflowX == 'scroll') ||
+ elem.clientHeight < elem.scrollHeight && (elemCSS.overflowY == 'auto' || elemCSS.overflowY == 'scroll')
+ ) {
+ if (!elem || !elem.getBoundingClientRect || elem === document.body) return win;
+
+ if (gotSelf || includeSelf) return elem;
+ gotSelf = true;
+ }
+ }
+ /* jshint boss:true */
+ } while (elem = elem.parentNode);
+
+ return win;
+ },
+
+ _autoScroll = _throttle(function (/**Event*/evt, /**Object*/options, /**HTMLElement*/rootEl, /**Boolean*/isFallback) {
+ // Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=505521
+ if (options.scroll) {
+ var _this = rootEl ? rootEl[expando] : window,
+ sens = options.scrollSensitivity,
+ speed = options.scrollSpeed,
+
+ x = evt.clientX,
+ y = evt.clientY,
+
+ winWidth = window.innerWidth,
+ winHeight = window.innerHeight,
+
+ scrollThisInstance = false;
+
+ // Detect scrollEl
+ if (scrollParentEl !== rootEl) {
+ _clearAutoScrolls();
+
+ scrollEl = options.scroll;
+ scrollCustomFn = options.scrollFn;
+
+ if (scrollEl === true) {
+ scrollEl = _getParentAutoScrollElement(rootEl, true);
+ scrollParentEl = scrollEl;
+ }
+ }
+
+
+ var layersOut = 0;
+ var currentParent = scrollEl;
+ do {
+ var el = currentParent,
+ rect = _getRect(el),
+
+ top = rect.top,
+ bottom = rect.bottom,
+ left = rect.left,
+ right = rect.right,
+
+ width = rect.width,
+ height = rect.height,
+
+ scrollWidth,
+ scrollHeight,
+
+ css,
+
+ vx,
+ vy,
+
+ canScrollX,
+ canScrollY,
+
+ scrollPosX,
+ scrollPosY;
+
+
+ if (el !== win) {
+ scrollWidth = el.scrollWidth;
+ scrollHeight = el.scrollHeight;
+
+ css = _css(el);
+
+ canScrollX = width < scrollWidth && (css.overflowX === 'auto' || css.overflowX === 'scroll');
+ canScrollY = height < scrollHeight && (css.overflowY === 'auto' || css.overflowY === 'scroll');
+
+ scrollPosX = el.scrollLeft;
+ scrollPosY = el.scrollTop;
+ } else {
+ scrollWidth = document.documentElement.scrollWidth;
+ scrollHeight = document.documentElement.scrollHeight;
+
+ css = _css(document.documentElement);
+
+ canScrollX = width < scrollWidth && (css.overflowX === 'auto' || css.overflowX === 'scroll' || css.overflowX === 'visible');
+ canScrollY = height < scrollHeight && (css.overflowY === 'auto' || css.overflowY === 'scroll' || css.overflowY === 'visible');
+
+ scrollPosX = document.documentElement.scrollLeft;
+ scrollPosY = document.documentElement.scrollTop;
+ }
+
+ vx = canScrollX && (abs(right - x) <= sens && (scrollPosX + width) < scrollWidth) - (abs(left - x) <= sens && !!scrollPosX);
+
+ vy = canScrollY && (abs(bottom - y) <= sens && (scrollPosY + height) < scrollHeight) - (abs(top - y) <= sens && !!scrollPosY);
+
+
+ if (!autoScrolls[layersOut]) {
+ for (var i = 0; i <= layersOut; i++) {
+ if (!autoScrolls[i]) {
+ autoScrolls[i] = {};
+ }
+ }
+ }
+
+ if (autoScrolls[layersOut].vx != vx || autoScrolls[layersOut].vy != vy || autoScrolls[layersOut].el !== el) {
+ autoScrolls[layersOut].el = el;
+ autoScrolls[layersOut].vx = vx;
+ autoScrolls[layersOut].vy = vy;
+
+ clearInterval(autoScrolls[layersOut].pid);
+
+ if (el && (vx != 0 || vy != 0)) {
+ scrollThisInstance = true;
+ /* jshint loopfunc:true */
+ autoScrolls[layersOut].pid = setInterval((function () {
+ // emulate drag over during autoscroll (fallback), emulating native DnD behaviour
+ if (isFallback && this.layer === 0) {
+ Sortable.active._emulateDragOver(true);
+ }
+ var scrollOffsetY = autoScrolls[this.layer].vy ? autoScrolls[this.layer].vy * speed : 0;
+ var scrollOffsetX = autoScrolls[this.layer].vx ? autoScrolls[this.layer].vx * speed : 0;
+
+ if ('function' === typeof(scrollCustomFn)) {
+ if (scrollCustomFn.call(_this, scrollOffsetX, scrollOffsetY, evt, touchEvt, autoScrolls[this.layer].el) !== 'continue') {
+ return;
+ }
+ }
+ if (autoScrolls[this.layer].el === win) {
+ win.scrollTo(win.pageXOffset + scrollOffsetX, win.pageYOffset + scrollOffsetY);
+ } else {
+ autoScrolls[this.layer].el.scrollTop += scrollOffsetY;
+ autoScrolls[this.layer].el.scrollLeft += scrollOffsetX;
+ }
+ }).bind({layer: layersOut}), 24);
+ }
+ }
+ layersOut++;
+ } while (options.bubbleScroll && currentParent !== win && (currentParent = _getParentAutoScrollElement(currentParent, false)));
+ scrolling = scrollThisInstance; // in case another function catches scrolling as false in between when it is not
+ }
+ }, 30),
+
+ _clearAutoScrolls = function () {
+ autoScrolls.forEach(function(autoScroll) {
+ clearInterval(autoScroll.pid);
+ });
+ autoScrolls = [];
+ },
+
+ _prepareGroup = function (options) {
+ function toFn(value, pull) {
+ return function(to, from, dragEl, evt) {
+ var sameGroup = to.options.group.name &&
+ from.options.group.name &&
+ to.options.group.name === from.options.group.name;
+
+ if (value == null && (pull || sameGroup)) {
+ // Default pull value
+ // Default pull and put value if same group
+ return true;
+ } else if (value == null || value === false) {
+ return false;
+ } else if (pull && value === 'clone') {
+ return value;
+ } else if (typeof value === 'function') {
+ return toFn(value(to, from, dragEl, evt), pull)(to, from, dragEl, evt);
+ } else {
+ var otherGroup = (pull ? to : from).options.group.name;
+
+ return (value === true ||
+ (typeof value === 'string' && value === otherGroup) ||
+ (value.join && value.indexOf(otherGroup) > -1));
+ }
+ };
+ }
+
+ var group = {};
+ var originalGroup = options.group;
+
+ if (!originalGroup || typeof originalGroup != 'object') {
+ originalGroup = {name: originalGroup};
+ }
+
+ group.name = originalGroup.name;
+ group.checkPull = toFn(originalGroup.pull, true);
+ group.checkPut = toFn(originalGroup.put);
+ group.revertClone = originalGroup.revertClone;
+
+ options.group = group;
+ },
+
+ _checkAlignment = function(evt) {
+ if (!dragEl || !dragEl.parentNode) return;
+ dragEl.parentNode[expando] && dragEl.parentNode[expando]._computeIsAligned(evt);
+ },
+
+ _isTrueParentSortable = function(el, target) {
+ var trueParent = target;
+ while (!trueParent[expando]) {
+ trueParent = trueParent.parentNode;
+ }
+
+ return el === trueParent;
+ },
+
+ _artificalBubble = function(sortable, originalEvt, method) {
+ // Artificial IE bubbling
+ var nextParent = sortable.parentNode;
+ while (nextParent && !nextParent[expando]) {
+ nextParent = nextParent.parentNode;
+ }
+
+ if (nextParent) {
+ nextParent[expando][method](_extend(originalEvt, {
+ artificialBubble: true
+ }));
+ }
+ },
+
+ _hideGhostForTarget = function() {
+ if (!supportCssPointerEvents && ghostEl) {
+ _css(ghostEl, 'display', 'none');
+ }
+ },
+
+ _unhideGhostForTarget = function() {
+ if (!supportCssPointerEvents && ghostEl) {
+ _css(ghostEl, 'display', '');
+ }
+ };
+
+
+ // #1184 fix - Prevent click event on fallback if dragged but item not changed position
+ document.addEventListener('click', function(evt) {
+ if (ignoreNextClick) {
+ evt.preventDefault();
+ evt.stopPropagation && evt.stopPropagation();
+ evt.stopImmediatePropagation && evt.stopImmediatePropagation();
+ ignoreNextClick = false;
+ return false;
+ }
+ }, true);
+
+ var nearestEmptyInsertDetectEvent = function(evt) {
+ evt = evt.touches ? evt.touches[0] : evt;
+ if (dragEl) {
+ var nearest = _detectNearestEmptySortable(evt.clientX, evt.clientY);
+
+ if (nearest) {
+ nearest[expando]._onDragOver({
+ clientX: evt.clientX,
+ clientY: evt.clientY,
+ target: nearest,
+ rootEl: nearest
+ });
+ }
+ }
+ };
+ // We do not want this to be triggered if completed (bubbling canceled), so only define it here
+ _on(document, 'dragover', nearestEmptyInsertDetectEvent);
+ _on(document, 'mousemove', nearestEmptyInsertDetectEvent);
+ _on(document, 'touchmove', nearestEmptyInsertDetectEvent);
+
+ /**
+ * @class Sortable
+ * @param {HTMLElement} el
+ * @param {Object} [options]
+ */
+ function Sortable(el, options) {
+ if (!(el && el.nodeType && el.nodeType === 1)) {
+ throw 'Sortable: `el` must be HTMLElement, not ' + {}.toString.call(el);
+ }
+
+ this.el = el; // root element
+ this.options = options = _extend({}, options);
+
+
+ // Export instance
+ el[expando] = this;
+
+ // Default options
+ var defaults = {
+ group: null,
+ sort: true,
+ disabled: false,
+ store: null,
+ handle: null,
+ scroll: true,
+ scrollSensitivity: 30,
+ scrollSpeed: 10,
+ bubbleScroll: true,
+ draggable: /[uo]l/i.test(el.nodeName) ? '>li' : '>*',
+ swapThreshold: 1, // percentage; 0 <= x <= 1
+ invertSwap: false, // invert always
+ invertedSwapThreshold: null, // will be set to same as swapThreshold if default
+ removeCloneOnHide: true,
+ direction: function() {
+ return _detectDirection(el, this.options);
+ },
+ ghostClass: 'sortable-ghost',
+ chosenClass: 'sortable-chosen',
+ dragClass: 'sortable-drag',
+ ignore: 'a, img',
+ filter: null,
+ preventOnFilter: true,
+ animation: 0,
+ easing: null,
+ setData: function (dataTransfer, dragEl) {
+ dataTransfer.setData('Text', dragEl.textContent);
+ },
+ dropBubble: false,
+ dragoverBubble: false,
+ dataIdAttr: 'data-id',
+ delay: 0,
+ touchStartThreshold: parseInt(window.devicePixelRatio, 10) || 1,
+ forceFallback: false,
+ fallbackClass: 'sortable-fallback',
+ fallbackOnBody: false,
+ fallbackTolerance: 0,
+ fallbackOffset: {x: 0, y: 0},
+ supportPointer: Sortable.supportPointer !== false && (
+ ('PointerEvent' in window) ||
+ window.navigator && ('msPointerEnabled' in window.navigator) // microsoft
+ ),
+ emptyInsertThreshold: 5
+ };
+
+
+ // Set default options
+ for (var name in defaults) {
+ !(name in options) && (options[name] = defaults[name]);
+ }
+
+ _prepareGroup(options);
+
+ // Bind all private methods
+ for (var fn in this) {
+ if (fn.charAt(0) === '_' && typeof this[fn] === 'function') {
+ this[fn] = this[fn].bind(this);
+ }
+ }
+
+ // Setup drag mode
+ this.nativeDraggable = options.forceFallback ? false : supportDraggable;
+
+ // Bind events
+ if (options.supportPointer) {
+ _on(el, 'pointerdown', this._onTapStart);
+ } else {
+ _on(el, 'mousedown', this._onTapStart);
+ _on(el, 'touchstart', this._onTapStart);
+ }
+
+ if (this.nativeDraggable) {
+ _on(el, 'dragover', this);
+ _on(el, 'dragenter', this);
+ }
+
+ sortables.push(this.el);
+
+ // Restore sorting
+ options.store && options.store.get && this.sort(options.store.get(this) || []);
+ }
+
+ Sortable.prototype = /** @lends Sortable.prototype */ {
+ constructor: Sortable,
+
+ _computeIsAligned: function(evt) {
+ var target;
+
+ if (ghostEl && !supportCssPointerEvents) {
+ _hideGhostForTarget();
+ target = document.elementFromPoint(evt.clientX, evt.clientY);
+ _unhideGhostForTarget();
+ } else {
+ target = evt.target;
+ }
+
+ target = _closest(target, this.options.draggable, this.el, false);
+ if (_alignedSilent) return;
+ if (!dragEl || dragEl.parentNode !== this.el) return;
+
+ var children = this.el.children;
+ for (var i = 0; i < children.length; i++) {
+ // Don't change for target in case it is changed to aligned before onDragOver is fired
+ if (_closest(children[i], this.options.draggable, this.el, false) && children[i] !== target) {
+ children[i].sortableMouseAligned = _isClientInRowColumn(evt.clientX, evt.clientY, children[i], this._getDirection(evt, null), this.options);
+ }
+ }
+ // Used for nulling last target when not in element, nothing to do with checking if aligned
+ if (!_closest(target, this.options.draggable, this.el, true)) {
+ lastTarget = null;
+ }
+
+ _alignedSilent = true;
+ setTimeout(function() {
+ _alignedSilent = false;
+ }, 30);
+
+ },
+
+ _getDirection: function(evt, target) {
+ return (typeof this.options.direction === 'function') ? this.options.direction.call(this, evt, target, dragEl) : this.options.direction;
+ },
+
+ _onTapStart: function (/** Event|TouchEvent */evt) {
+ if (!evt.cancelable) return;
+ var _this = this,
+ el = this.el,
+ options = this.options,
+ preventOnFilter = options.preventOnFilter,
+ type = evt.type,
+ touch = evt.touches && evt.touches[0],
+ target = (touch || evt).target,
+ originalTarget = evt.target.shadowRoot && ((evt.path && evt.path[0]) || (evt.composedPath && evt.composedPath()[0])) || target,
+ filter = options.filter,
+ startIndex;
+
+ _saveInputCheckedState(el);
+
+
+ // IE: Calls events in capture mode if event element is nested. This ensures only correct element's _onTapStart goes through.
+ // This process is also done in _onDragOver
+ if (IE11OrLess && !evt.artificialBubble && !_isTrueParentSortable(el, target)) {
+ return;
+ }
+
+ // Don't trigger start event when an element is been dragged, otherwise the evt.oldindex always wrong when set option.group.
+ if (dragEl) {
+ return;
+ }
+
+ if (/mousedown|pointerdown/.test(type) && evt.button !== 0 || options.disabled) {
+ return; // only left button and enabled
+ }
+
+ // cancel dnd if original target is content editable
+ if (originalTarget.isContentEditable) {
+ return;
+ }
+
+ target = _closest(target, options.draggable, el, false);
+
+ if (!target) {
+ if (IE11OrLess) {
+ _artificalBubble(el, evt, '_onTapStart');
+ }
+ return;
+ }
+
+ if (lastDownEl === target) {
+ // Ignoring duplicate `down`
+ return;
+ }
+
+ // Get the index of the dragged element within its parent
+ startIndex = _index(target, options.draggable);
+
+ // Check filter
+ if (typeof filter === 'function') {
+ if (filter.call(this, evt, target, this)) {
+ _dispatchEvent(_this, originalTarget, 'filter', target, el, el, startIndex);
+ preventOnFilter && evt.cancelable && evt.preventDefault();
+ return; // cancel dnd
+ }
+ }
+ else if (filter) {
+ filter = filter.split(',').some(function (criteria) {
+ criteria = _closest(originalTarget, criteria.trim(), el, false);
+
+ if (criteria) {
+ _dispatchEvent(_this, criteria, 'filter', target, el, el, startIndex);
+ return true;
+ }
+ });
+
+ if (filter) {
+ preventOnFilter && evt.cancelable && evt.preventDefault();
+ return; // cancel dnd
+ }
+ }
+
+ if (options.handle && !_closest(originalTarget, options.handle, el, false)) {
+ return;
+ }
+
+ // Prepare `dragstart`
+ this._prepareDragStart(evt, touch, target, startIndex);
+ },
+
+
+ _handleAutoScroll: function(evt, fallback) {
+ if (!dragEl || !this.options.scroll) return;
+ var x = evt.clientX,
+ y = evt.clientY,
+
+ elem = document.elementFromPoint(x, y),
+ _this = this;
+
+ // IE does not seem to have native autoscroll,
+ // Edge's autoscroll seems too conditional,
+ // Firefox and Chrome are good
+ if (fallback || Edge || IE11OrLess) {
+ _autoScroll(evt, _this.options, elem, fallback);
+
+ // Listener for pointer element change
+ var ogElemScroller = _getParentAutoScrollElement(elem, true);
+ if (
+ scrolling &&
+ (
+ !pointerElemChangedInterval ||
+ x !== lastPointerElemX ||
+ y !== lastPointerElemY
+ )
+ ) {
+
+ pointerElemChangedInterval && clearInterval(pointerElemChangedInterval);
+ // Detect for pointer elem change, emulating native DnD behaviour
+ pointerElemChangedInterval = setInterval(function() {
+ if (!dragEl) return;
+ // could also check if scroll direction on newElem changes due to parent autoscrolling
+ var newElem = _getParentAutoScrollElement(document.elementFromPoint(x, y), true);
+ if (newElem !== ogElemScroller) {
+ ogElemScroller = newElem;
+ _clearAutoScrolls();
+ _autoScroll(evt, _this.options, ogElemScroller, fallback);
+ }
+ }, 10);
+ lastPointerElemX = x;
+ lastPointerElemY = y;
+ }
+
+ } else {
+ // if DnD is enabled (and browser has good autoscrolling), first autoscroll will already scroll, so get parent autoscroll of first autoscroll
+ if (!_this.options.bubbleScroll || _getParentAutoScrollElement(elem, true) === window) {
+ _clearAutoScrolls();
+ return;
+ }
+ _autoScroll(evt, _this.options, _getParentAutoScrollElement(elem, false), false);
+ }
+ },
+
+ _prepareDragStart: function (/** Event */evt, /** Touch */touch, /** HTMLElement */target, /** Number */startIndex) {
+ var _this = this,
+ el = _this.el,
+ options = _this.options,
+ ownerDocument = el.ownerDocument,
+ dragStartFn;
+
+ if (target && !dragEl && (target.parentNode === el)) {
+ rootEl = el;
+ dragEl = target;
+ parentEl = dragEl.parentNode;
+ nextEl = dragEl.nextSibling;
+ lastDownEl = target;
+ activeGroup = options.group;
+ oldIndex = startIndex;
+
+ tapEvt = {
+ target: dragEl,
+ clientX: (touch || evt).clientX,
+ clientY: (touch || evt).clientY
+ };
+
+ this._lastX = (touch || evt).clientX;
+ this._lastY = (touch || evt).clientY;
+
+ dragEl.style['will-change'] = 'all';
+ // undo animation if needed
+ dragEl.style.transition = '';
+ dragEl.style.transform = '';
+
+ dragStartFn = function () {
+ // Delayed drag has been triggered
+ // we can re-enable the events: touchmove/mousemove
+ _this._disableDelayedDrag();
+
+ // Make the element draggable
+ dragEl.draggable = _this.nativeDraggable;
+
+ // Bind the events: dragstart/dragend
+ _this._triggerDragStart(evt, touch);
+
+ // Drag start event
+ _dispatchEvent(_this, rootEl, 'choose', dragEl, rootEl, rootEl, oldIndex);
+
+ // Chosen item
+ _toggleClass(dragEl, options.chosenClass, true);
+ };
+
+ // Disable "draggable"
+ options.ignore.split(',').forEach(function (criteria) {
+ _find(dragEl, criteria.trim(), _disableDraggable);
+ });
+
+ if (options.supportPointer) {
+ _on(ownerDocument, 'pointerup', _this._onDrop);
+ } else {
+ _on(ownerDocument, 'mouseup', _this._onDrop);
+ _on(ownerDocument, 'touchend', _this._onDrop);
+ _on(ownerDocument, 'touchcancel', _this._onDrop);
+ }
+
+ if (options.delay) {
+ // If the user moves the pointer or let go the click or touch
+ // before the delay has been reached:
+ // disable the delayed drag
+ _on(ownerDocument, 'mouseup', _this._disableDelayedDrag);
+ _on(ownerDocument, 'touchend', _this._disableDelayedDrag);
+ _on(ownerDocument, 'touchcancel', _this._disableDelayedDrag);
+ _on(ownerDocument, 'mousemove', _this._delayedDragTouchMoveHandler);
+ _on(ownerDocument, 'touchmove', _this._delayedDragTouchMoveHandler);
+ options.supportPointer && _on(ownerDocument, 'pointermove', _this._delayedDragTouchMoveHandler);
+
+ _this._dragStartTimer = setTimeout(dragStartFn, options.delay);
+ } else {
+ dragStartFn();
+ }
+ }
+ },
+
+ _delayedDragTouchMoveHandler: function (/** TouchEvent|PointerEvent **/e) {
+ var touch = e.touches ? e.touches[0] : e;
+ if (min(abs(touch.clientX - this._lastX), abs(touch.clientY - this._lastY))
+ >= this.options.touchStartThreshold
+ ) {
+ this._disableDelayedDrag();
+ }
+ },
+
+ _disableDelayedDrag: function () {
+ var ownerDocument = this.el.ownerDocument;
+
+ clearTimeout(this._dragStartTimer);
+ _off(ownerDocument, 'mouseup', this._disableDelayedDrag);
+ _off(ownerDocument, 'touchend', this._disableDelayedDrag);
+ _off(ownerDocument, 'touchcancel', this._disableDelayedDrag);
+ _off(ownerDocument, 'mousemove', this._delayedDragTouchMoveHandler);
+ _off(ownerDocument, 'touchmove', this._delayedDragTouchMoveHandler);
+ _off(ownerDocument, 'pointermove', this._delayedDragTouchMoveHandler);
+ },
+
+ _triggerDragStart: function (/** Event */evt, /** Touch */touch) {
+ touch = touch || (evt.pointerType == 'touch' ? evt : null);
+
+ if (!this.nativeDraggable || touch) {
+ if (this.options.supportPointer) {
+ _on(document, 'pointermove', this._onTouchMove);
+ } else if (touch) {
+ _on(document, 'touchmove', this._onTouchMove);
+ } else {
+ _on(document, 'mousemove', this._onTouchMove);
+ }
+ } else {
+ _on(dragEl, 'dragend', this);
+ _on(rootEl, 'dragstart', this._onDragStart);
+ }
+
+ try {
+ if (document.selection) {
+ // Timeout neccessary for IE9
+ _nextTick(function () {
+ document.selection.empty();
+ });
+ } else {
+ window.getSelection().removeAllRanges();
+ }
+ } catch (err) {
+ }
+ },
+
+ _dragStarted: function (fallback) {
+ awaitingDragStarted = false;
+ if (rootEl && dragEl) {
+ if (this.nativeDraggable) {
+ _on(document, 'dragover', this._handleAutoScroll);
+ _on(document, 'dragover', _checkAlignment);
+ }
+ var options = this.options;
+
+ // Apply effect
+ !fallback && _toggleClass(dragEl, options.dragClass, false);
+ _toggleClass(dragEl, options.ghostClass, true);
+
+ // In case dragging an animated element
+ _css(dragEl, 'transform', '');
+
+ Sortable.active = this;
+
+ fallback && this._appendGhost();
+
+ // Drag start event
+ _dispatchEvent(this, rootEl, 'start', dragEl, rootEl, rootEl, oldIndex);
+ } else {
+ this._nulling();
+ }
+ },
+
+ _emulateDragOver: function (bypassLastTouchCheck) {
+ if (touchEvt) {
+ if (this._lastX === touchEvt.clientX && this._lastY === touchEvt.clientY && !bypassLastTouchCheck) {
+ return;
+ }
+ this._lastX = touchEvt.clientX;
+ this._lastY = touchEvt.clientY;
+
+ _hideGhostForTarget();
+
+ var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY);
+ var parent = target;
+
+ while (target && target.shadowRoot) {
+ target = target.shadowRoot.elementFromPoint(touchEvt.clientX, touchEvt.clientY);
+ parent = target;
+ }
+
+ if (parent) {
+ do {
+ if (parent[expando]) {
+ var inserted;
+
+ inserted = parent[expando]._onDragOver({
+ clientX: touchEvt.clientX,
+ clientY: touchEvt.clientY,
+ target: target,
+ rootEl: parent
+ });
+
+ if (inserted && !this.options.dragoverBubble) {
+ break;
+ }
+ }
+
+ target = parent; // store last element
+ }
+ /* jshint boss:true */
+ while (parent = parent.parentNode);
+ }
+ dragEl.parentNode[expando]._computeIsAligned(touchEvt);
+
+ _unhideGhostForTarget();
+ }
+ },
+
+
+ _onTouchMove: function (/**TouchEvent*/evt) {
+ if (tapEvt) {
+ var options = this.options,
+ fallbackTolerance = options.fallbackTolerance,
+ fallbackOffset = options.fallbackOffset,
+ touch = evt.touches ? evt.touches[0] : evt,
+ matrix = ghostEl && _matrix(ghostEl),
+ scaleX = ghostEl && matrix && matrix.a,
+ scaleY = ghostEl && matrix && matrix.d,
+ dx = ((touch.clientX - tapEvt.clientX) + fallbackOffset.x) / (scaleX ? scaleX : 1),
+ dy = ((touch.clientY - tapEvt.clientY) + fallbackOffset.y) / (scaleY ? scaleY : 1),
+ translate3d = evt.touches ? 'translate3d(' + dx + 'px,' + dy + 'px,0)' : 'translate(' + dx + 'px,' + dy + 'px)';
+
+
+ // only set the status to dragging, when we are actually dragging
+ if (!Sortable.active && !awaitingDragStarted) {
+ if (fallbackTolerance &&
+ min(abs(touch.clientX - this._lastX), abs(touch.clientY - this._lastY)) < fallbackTolerance
+ ) {
+ return;
+ }
+ this._onDragStart(evt, true);
+ }
+
+ this._handleAutoScroll(touch, true);
+
+
+ moved = true;
+ touchEvt = touch;
+
+
+ _css(ghostEl, 'webkitTransform', translate3d);
+ _css(ghostEl, 'mozTransform', translate3d);
+ _css(ghostEl, 'msTransform', translate3d);
+ _css(ghostEl, 'transform', translate3d);
+
+ evt.cancelable && evt.preventDefault();
+ }
+ },
+
+ _appendGhost: function () {
+ if (!ghostEl) {
+ var rect = _getRect(dragEl, this.options.fallbackOnBody ? document.body : rootEl, true),
+ css = _css(dragEl),
+ options = this.options;
+
+ ghostEl = dragEl.cloneNode(true);
+
+ _toggleClass(ghostEl, options.ghostClass, false);
+ _toggleClass(ghostEl, options.fallbackClass, true);
+ _toggleClass(ghostEl, options.dragClass, true);
+
+ _css(ghostEl, 'box-sizing', 'border-box');
+ _css(ghostEl, 'margin', 0);
+ _css(ghostEl, 'top', rect.top);
+ _css(ghostEl, 'left', rect.left);
+ _css(ghostEl, 'width', rect.width);
+ _css(ghostEl, 'height', rect.height);
+ _css(ghostEl, 'opacity', '0.8');
+ _css(ghostEl, 'position', 'fixed');
+ _css(ghostEl, 'zIndex', '100000');
+ _css(ghostEl, 'pointerEvents', 'none');
+
+ options.fallbackOnBody && document.body.appendChild(ghostEl) || rootEl.appendChild(ghostEl);
+ }
+ },
+
+ _onDragStart: function (/**Event*/evt, /**boolean*/fallback) {
+ var _this = this;
+ var dataTransfer = evt.dataTransfer;
+ var options = _this.options;
+
+ // Setup clone
+ cloneEl = _clone(dragEl);
+
+ cloneEl.draggable = false;
+ cloneEl.style['will-change'] = '';
+
+ this._hideClone();
+
+ _toggleClass(cloneEl, _this.options.chosenClass, false);
+
+
+ // #1143: IFrame support workaround
+ _this._cloneId = _nextTick(function () {
+ if (!_this.options.removeCloneOnHide) {
+ rootEl.insertBefore(cloneEl, dragEl);
+ }
+ _dispatchEvent(_this, rootEl, 'clone', dragEl);
+ });
+
+
+ !fallback && _toggleClass(dragEl, options.dragClass, true);
+
+ // Set proper drop events
+ if (fallback) {
+ ignoreNextClick = true;
+ _this._loopId = setInterval(_this._emulateDragOver, 50);
+ } else {
+ // Undo what was set in _prepareDragStart before drag started
+ _off(document, 'mouseup', _this._onDrop);
+ _off(document, 'touchend', _this._onDrop);
+ _off(document, 'touchcancel', _this._onDrop);
+
+ if (dataTransfer) {
+ dataTransfer.effectAllowed = 'move';
+ options.setData && options.setData.call(_this, dataTransfer, dragEl);
+ }
+
+ _on(document, 'drop', _this);
+
+ // #1276 fix:
+ _css(dragEl, 'transform', 'translateZ(0)');
+ }
+
+ awaitingDragStarted = true;
+
+ _this._dragStartId = _nextTick(_this._dragStarted.bind(_this, fallback));
+ _on(document, 'selectstart', _this);
+ },
+
+ // Returns true - if no further action is needed (either inserted or another condition)
+ _onDragOver: function (/**Event*/evt) {
+ var el = this.el,
+ target = evt.target,
+ dragRect,
+ targetRect,
+ revert,
+ options = this.options,
+ group = options.group,
+ activeSortable = Sortable.active,
+ isOwner = (activeGroup === group),
+ canSort = options.sort,
+ _this = this;
+
+ if (_silent) return;
+
+ // IE event order fix
+ if (IE11OrLess && !evt.rootEl && !evt.artificialBubble && !_isTrueParentSortable(el, target)) {
+ return;
+ }
+
+ // Return invocation when no further action is needed in another sortable
+ function completed() {
+ if (activeSortable) {
+ // Set ghost class to new sortable's ghost class
+ _toggleClass(dragEl, putSortable ? putSortable.options.ghostClass : activeSortable.options.ghostClass, false);
+ _toggleClass(dragEl, options.ghostClass, true);
+ }
+
+ if (putSortable !== _this && _this !== Sortable.active) {
+ putSortable = _this;
+ } else if (_this === Sortable.active) {
+ putSortable = null;
+ }
+
+
+ // Null lastTarget if it is not inside a previously swapped element
+ if ((target === dragEl && !dragEl.animated) || (target === el && !target.animated)) {
+ lastTarget = null;
+ }
+ // no bubbling and not fallback
+ if (!options.dragoverBubble && !evt.rootEl && target !== document) {
+ _this._handleAutoScroll(evt);
+ dragEl.parentNode[expando]._computeIsAligned(evt);
+ }
+
+ !options.dragoverBubble && evt.stopPropagation && evt.stopPropagation();
+
+ return true;
+ }
+
+ // Call when dragEl has been inserted
+ function changed() {
+ _dispatchEvent(_this, rootEl, 'change', target, el, rootEl, oldIndex, _index(dragEl, options.draggable), evt);
+ }
+
+
+ if (evt.preventDefault !== void 0) {
+ evt.cancelable && evt.preventDefault();
+ }
+
+
+ moved = true;
+
+ target = _closest(target, options.draggable, el, true);
+
+ // target is dragEl or target is animated
+ if (!!_closest(evt.target, null, dragEl, true) || target.animated) {
+ return completed();
+ }
+
+ if (target !== dragEl) {
+ ignoreNextClick = false;
+ }
+
+ if (activeSortable && !options.disabled &&
+ (isOwner
+ ? canSort || (revert = !rootEl.contains(dragEl)) // Reverting item into the original list
+ : (
+ putSortable === this ||
+ (
+ (this.lastPutMode = activeGroup.checkPull(this, activeSortable, dragEl, evt)) &&
+ group.checkPut(this, activeSortable, dragEl, evt)
+ )
+ )
+ )
+ ) {
+ var axis = this._getDirection(evt, target);
+
+ dragRect = _getRect(dragEl);
+
+ if (revert) {
+ this._hideClone();
+ parentEl = rootEl; // actualization
+
+ if (nextEl) {
+ rootEl.insertBefore(dragEl, nextEl);
+ } else {
+ rootEl.appendChild(dragEl);
+ }
+
+ return completed();
+ }
+
+ if ((el.children.length === 0) || (el.children[0] === ghostEl) ||
+ _ghostIsLast(evt, axis, el) && !dragEl.animated
+ ) {
+ //assign target only if condition is true
+ if (el.children.length !== 0 && el.children[0] !== ghostEl && el === evt.target) {
+ target = _lastChild(el);
+ }
+
+ if (target) {
+ targetRect = _getRect(target);
+ }
+
+ if (isOwner) {
+ activeSortable._hideClone();
+ } else {
+ activeSortable._showClone(this);
+ }
+
+ if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, !!target) !== false) {
+ el.appendChild(dragEl);
+ parentEl = el; // actualization
+ realDragElRect = null;
+
+ changed();
+ this._animate(dragRect, dragEl);
+ target && this._animate(targetRect, target);
+ return completed();
+ }
+ }
+ else if (target && target !== dragEl && target.parentNode === el) {
+ var direction = 0,
+ targetBeforeFirstSwap,
+ aligned = target.sortableMouseAligned,
+ differentLevel = dragEl.parentNode !== el,
+ scrolledPastTop = _isScrolledPast(target, axis === 'vertical' ? 'top' : 'left');
+
+ if (lastTarget !== target) {
+ lastMode = null;
+ targetBeforeFirstSwap = _getRect(target)[axis === 'vertical' ? 'top' : 'left'];
+ pastFirstInvertThresh = false;
+ }
+
+ // Reference: https://www.lucidchart.com/documents/view/10fa0e93-e362-4126-aca2-b709ee56bd8b/0
+ if (
+ _isElInRowColumn(dragEl, target, axis) && aligned ||
+ differentLevel ||
+ scrolledPastTop ||
+ options.invertSwap ||
+ lastMode === 'insert' ||
+ // Needed, in the case that we are inside target and inserted because not aligned... aligned will stay false while inside
+ // and lastMode will change to 'insert', but we must swap
+ lastMode === 'swap'
+ ) {
+ // New target that we will be inside
+ if (lastMode !== 'swap') {
+ isCircumstantialInvert = options.invertSwap || differentLevel || scrolling || scrolledPastTop;
+ }
+
+ direction = _getSwapDirection(evt, target, axis,
+ options.swapThreshold, options.invertedSwapThreshold == null ? options.swapThreshold : options.invertedSwapThreshold,
+ isCircumstantialInvert,
+ lastTarget === target);
+ lastMode = 'swap';
+ } else {
+ // Insert at position
+ direction = _getInsertDirection(target, options);
+ lastMode = 'insert';
+ }
+ if (direction === 0) return completed();
+
+ realDragElRect = null;
+ lastTarget = target;
+
+ lastDirection = direction;
+
+ targetRect = _getRect(target);
+
+ var nextSibling = target.nextElementSibling,
+ after = false;
+
+ after = direction === 1;
+
+ var moveVector = _onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, after);
+
+ if (moveVector !== false) {
+ if (moveVector === 1 || moveVector === -1) {
+ after = (moveVector === 1);
+ }
+
+ _silent = true;
+ setTimeout(_unsilent, 30);
+
+ if (isOwner) {
+ activeSortable._hideClone();
+ } else {
+ activeSortable._showClone(this);
+ }
+
+ if (after && !nextSibling) {
+ el.appendChild(dragEl);
+ } else {
+ target.parentNode.insertBefore(dragEl, after ? nextSibling : target);
+ }
+
+ parentEl = dragEl.parentNode; // actualization
+
+ // must be done before animation
+ if (targetBeforeFirstSwap !== undefined && !isCircumstantialInvert) {
+ targetMoveDistance = abs(targetBeforeFirstSwap - _getRect(target)[axis === 'vertical' ? 'top' : 'left']);
+ }
+ changed();
+ !differentLevel && this._animate(targetRect, target);
+ this._animate(dragRect, dragEl);
+
+ return completed();
+ }
+ }
+
+ if (el.contains(dragEl)) {
+ return completed();
+ }
+ }
+
+ if (IE11OrLess && !evt.rootEl) {
+ _artificalBubble(el, evt, '_onDragOver');
+ }
+
+ return false;
+ },
+
+ _animate: function (prevRect, target) {
+ var ms = this.options.animation;
+
+ if (ms) {
+ var currentRect = _getRect(target);
+
+ if (target === dragEl) {
+ realDragElRect = currentRect;
+ }
+
+ if (prevRect.nodeType === 1) {
+ prevRect = _getRect(prevRect);
+ }
+
+ // Check if actually moving position
+ if ((prevRect.left + prevRect.width / 2) !== (currentRect.left + currentRect.width / 2)
+ || (prevRect.top + prevRect.height / 2) !== (currentRect.top + currentRect.height / 2)
+ ) {
+ var matrix = _matrix(this.el),
+ scaleX = matrix && matrix.a,
+ scaleY = matrix && matrix.d;
+
+ _css(target, 'transition', 'none');
+ _css(target, 'transform', 'translate3d('
+ + (prevRect.left - currentRect.left) / (scaleX ? scaleX : 1) + 'px,'
+ + (prevRect.top - currentRect.top) / (scaleY ? scaleY : 1) + 'px,0)'
+ );
+
+ forRepaintDummy = target.offsetWidth; // repaint
+ _css(target, 'transition', 'transform ' + ms + 'ms' + (this.options.easing ? ' ' + this.options.easing : ''));
+ _css(target, 'transform', 'translate3d(0,0,0)');
+ }
+
+ (typeof target.animated === 'number') && clearTimeout(target.animated);
+ target.animated = setTimeout(function () {
+ _css(target, 'transition', '');
+ _css(target, 'transform', '');
+ target.animated = false;
+ }, ms);
+ }
+ },
+
+ _offUpEvents: function () {
+ var ownerDocument = this.el.ownerDocument;
+
+ _off(document, 'touchmove', this._onTouchMove);
+ _off(document, 'pointermove', this._onTouchMove);
+ _off(ownerDocument, 'mouseup', this._onDrop);
+ _off(ownerDocument, 'touchend', this._onDrop);
+ _off(ownerDocument, 'pointerup', this._onDrop);
+ _off(ownerDocument, 'touchcancel', this._onDrop);
+ _off(document, 'selectstart', this);
+ },
+
+ _onDrop: function (/**Event*/evt) {
+ var el = this.el,
+ options = this.options;
+ awaitingDragStarted = false;
+ scrolling = false;
+ isCircumstantialInvert = false;
+ pastFirstInvertThresh = false;
+
+ clearInterval(this._loopId);
+
+ clearInterval(pointerElemChangedInterval);
+ _clearAutoScrolls();
+ _cancelThrottle();
+
+ clearTimeout(this._dragStartTimer);
+
+ _cancelNextTick(this._cloneId);
+ _cancelNextTick(this._dragStartId);
+
+ // Unbind events
+ _off(document, 'mousemove', this._onTouchMove);
+
+
+ if (this.nativeDraggable) {
+ _off(document, 'drop', this);
+ _off(el, 'dragstart', this._onDragStart);
+ _off(document, 'dragover', this._handleAutoScroll);
+ _off(document, 'dragover', _checkAlignment);
+ }
+
+ this._offUpEvents();
+
+ if (evt) {
+ if (moved) {
+ evt.cancelable && evt.preventDefault();
+ !options.dropBubble && evt.stopPropagation();
+ }
+
+ ghostEl && ghostEl.parentNode && ghostEl.parentNode.removeChild(ghostEl);
+
+ if (rootEl === parentEl || (putSortable && putSortable.lastPutMode !== 'clone')) {
+ // Remove clone
+ cloneEl && cloneEl.parentNode && cloneEl.parentNode.removeChild(cloneEl);
+ }
+
+ if (dragEl) {
+ if (this.nativeDraggable) {
+ _off(dragEl, 'dragend', this);
+ }
+
+ _disableDraggable(dragEl);
+ dragEl.style['will-change'] = '';
+
+ // Remove class's
+ _toggleClass(dragEl, putSortable ? putSortable.options.ghostClass : this.options.ghostClass, false);
+ _toggleClass(dragEl, this.options.chosenClass, false);
+
+ // Drag stop event
+ _dispatchEvent(this, rootEl, 'unchoose', dragEl, parentEl, rootEl, oldIndex, null, evt);
+
+ if (rootEl !== parentEl) {
+ newIndex = _index(dragEl, options.draggable);
+
+ if (newIndex >= 0) {
+ // Add event
+ _dispatchEvent(null, parentEl, 'add', dragEl, parentEl, rootEl, oldIndex, newIndex, evt);
+
+ // Remove event
+ _dispatchEvent(this, rootEl, 'remove', dragEl, parentEl, rootEl, oldIndex, newIndex, evt);
+
+ // drag from one list and drop into another
+ _dispatchEvent(null, parentEl, 'sort', dragEl, parentEl, rootEl, oldIndex, newIndex, evt);
+ _dispatchEvent(this, rootEl, 'sort', dragEl, parentEl, rootEl, oldIndex, newIndex, evt);
+ }
+
+ putSortable && putSortable.save();
+ }
+ else {
+ if (dragEl.nextSibling !== nextEl) {
+ // Get the index of the dragged element within its parent
+ newIndex = _index(dragEl, options.draggable);
+
+ if (newIndex >= 0) {
+ // drag & drop within the same list
+ _dispatchEvent(this, rootEl, 'update', dragEl, parentEl, rootEl, oldIndex, newIndex, evt);
+ _dispatchEvent(this, rootEl, 'sort', dragEl, parentEl, rootEl, oldIndex, newIndex, evt);
+ }
+ }
+ }
+
+ if (Sortable.active) {
+ /* jshint eqnull:true */
+ if (newIndex == null || newIndex === -1) {
+ newIndex = oldIndex;
+ }
+
+ _dispatchEvent(this, rootEl, 'end', dragEl, parentEl, rootEl, oldIndex, newIndex, evt);
+
+ // Save sorting
+ this.save();
+ }
+ }
+
+ }
+ this._nulling();
+ },
+
+ _nulling: function() {
+ rootEl =
+ dragEl =
+ parentEl =
+ ghostEl =
+ nextEl =
+ cloneEl =
+ lastDownEl =
+
+ scrollEl =
+ scrollParentEl =
+ autoScrolls.length =
+
+ pointerElemChangedInterval =
+ lastPointerElemX =
+ lastPointerElemY =
+
+ tapEvt =
+ touchEvt =
+
+ moved =
+ newIndex =
+ oldIndex =
+
+ lastTarget =
+ lastDirection =
+
+ forRepaintDummy =
+ realDragElRect =
+
+ putSortable =
+ activeGroup =
+ Sortable.active = null;
+
+ savedInputChecked.forEach(function (el) {
+ el.checked = true;
+ });
+
+ savedInputChecked.length = 0;
+ },
+
+ handleEvent: function (/**Event*/evt) {
+ switch (evt.type) {
+ case 'drop':
+ case 'dragend':
+ this._onDrop(evt);
+ break;
+
+ case 'dragenter':
+ case 'dragover':
+ if (dragEl) {
+ this._onDragOver(evt);
+ _globalDragOver(evt);
+ }
+ break;
+
+ case 'selectstart':
+ evt.preventDefault();
+ break;
+ }
+ },
+
+
+ /**
+ * Serializes the item into an array of string.
+ * @returns {String[]}
+ */
+ toArray: function () {
+ var order = [],
+ el,
+ children = this.el.children,
+ i = 0,
+ n = children.length,
+ options = this.options;
+
+ for (; i < n; i++) {
+ el = children[i];
+ if (_closest(el, options.draggable, this.el, false)) {
+ order.push(el.getAttribute(options.dataIdAttr) || _generateId(el));
+ }
+ }
+
+ return order;
+ },
+
+
+ /**
+ * Sorts the elements according to the array.
+ * @param {String[]} order order of the items
+ */
+ sort: function (order) {
+ var items = {}, rootEl = this.el;
+
+ this.toArray().forEach(function (id, i) {
+ var el = rootEl.children[i];
+
+ if (_closest(el, this.options.draggable, rootEl, false)) {
+ items[id] = el;
+ }
+ }, this);
+
+ order.forEach(function (id) {
+ if (items[id]) {
+ rootEl.removeChild(items[id]);
+ rootEl.appendChild(items[id]);
+ }
+ });
+ },
+
+
+ /**
+ * Save the current sorting
+ */
+ save: function () {
+ var store = this.options.store;
+ store && store.set && store.set(this);
+ },
+
+
+ /**
+ * For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree.
+ * @param {HTMLElement} el
+ * @param {String} [selector] default: `options.draggable`
+ * @returns {HTMLElement|null}
+ */
+ closest: function (el, selector) {
+ return _closest(el, selector || this.options.draggable, this.el, false);
+ },
+
+
+ /**
+ * Set/get option
+ * @param {string} name
+ * @param {*} [value]
+ * @returns {*}
+ */
+ option: function (name, value) {
+ var options = this.options;
+
+ if (value === void 0) {
+ return options[name];
+ } else {
+ options[name] = value;
+
+ if (name === 'group') {
+ _prepareGroup(options);
+ }
+ }
+ },
+
+
+ /**
+ * Destroy
+ */
+ destroy: function () {
+ var el = this.el;
+
+ el[expando] = null;
+
+ _off(el, 'mousedown', this._onTapStart);
+ _off(el, 'touchstart', this._onTapStart);
+ _off(el, 'pointerdown', this._onTapStart);
+
+ if (this.nativeDraggable) {
+ _off(el, 'dragover', this);
+ _off(el, 'dragenter', this);
+ }
+ // Remove draggable attributes
+ Array.prototype.forEach.call(el.querySelectorAll('[draggable]'), function (el) {
+ el.removeAttribute('draggable');
+ });
+
+ this._onDrop();
+
+ sortables.splice(sortables.indexOf(this.el), 1);
+
+ this.el = el = null;
+ },
+
+ _hideClone: function() {
+ if (!cloneEl.cloneHidden) {
+ _css(cloneEl, 'display', 'none');
+ cloneEl.cloneHidden = true;
+ if (cloneEl.parentNode && this.options.removeCloneOnHide) {
+ cloneEl.parentNode.removeChild(cloneEl);
+ }
+ }
+ },
+
+ _showClone: function(putSortable) {
+ if (putSortable.lastPutMode !== 'clone') {
+ this._hideClone();
+ return;
+ }
+
+ if (cloneEl.cloneHidden) {
+ // show clone at dragEl or original position
+ if (rootEl.contains(dragEl) && !this.options.group.revertClone) {
+ rootEl.insertBefore(cloneEl, dragEl);
+ } else if (nextEl) {
+ rootEl.insertBefore(cloneEl, nextEl);
+ } else {
+ rootEl.appendChild(cloneEl);
+ }
+
+ if (this.options.group.revertClone) {
+ this._animate(dragEl, cloneEl);
+ }
+ _css(cloneEl, 'display', '');
+ cloneEl.cloneHidden = false;
+ }
+ }
+ };
+
+ function _closest(/**HTMLElement*/el, /**String*/selector, /**HTMLElement*/ctx, includeCTX) {
+ if (el) {
+ ctx = ctx || document;
+
+ do {
+ if (
+ selector != null &&
+ (
+ selector[0] === '>' && el.parentNode === ctx && _matches(el, selector.substring(1)) ||
+ _matches(el, selector)
+ ) ||
+ includeCTX && el === ctx
+ ) {
+ return el;
+ }
+
+ if (el === ctx) break;
+ /* jshint boss:true */
+ } while (el = _getParentOrHost(el));
+ }
+
+ return null;
+ }
+
+
+ function _getParentOrHost(el) {
+ return (el.host && el !== document && el.host.nodeType)
+ ? el.host
+ : el.parentNode;
+ }
+
+
+ function _globalDragOver(/**Event*/evt) {
+ if (evt.dataTransfer) {
+ evt.dataTransfer.dropEffect = 'move';
+ }
+ evt.cancelable && evt.preventDefault();
+ }
+
+
+ function _on(el, event, fn) {
+ el.addEventListener(event, fn, captureMode);
+ }
+
+
+ function _off(el, event, fn) {
+ el.removeEventListener(event, fn, captureMode);
+ }
+
+
+ function _toggleClass(el, name, state) {
+ if (el && name) {
+ if (el.classList) {
+ el.classList[state ? 'add' : 'remove'](name);
+ }
+ else {
+ var className = (' ' + el.className + ' ').replace(R_SPACE, ' ').replace(' ' + name + ' ', ' ');
+ el.className = (className + (state ? ' ' + name : '')).replace(R_SPACE, ' ');
+ }
+ }
+ }
+
+
+ function _css(el, prop, val) {
+ var style = el && el.style;
+
+ if (style) {
+ if (val === void 0) {
+ if (document.defaultView && document.defaultView.getComputedStyle) {
+ val = document.defaultView.getComputedStyle(el, '');
+ }
+ else if (el.currentStyle) {
+ val = el.currentStyle;
+ }
+
+ return prop === void 0 ? val : val[prop];
+ }
+ else {
+ if (!(prop in style) && prop.indexOf('webkit') === -1) {
+ prop = '-webkit-' + prop;
+ }
+
+ style[prop] = val + (typeof val === 'string' ? '' : 'px');
+ }
+ }
+ }
+
+ function _matrix(el) {
+ var appliedTransforms = '';
+ do {
+ var transform = _css(el, 'transform');
+
+ if (transform && transform !== 'none') {
+ appliedTransforms = transform + ' ' + appliedTransforms;
+ }
+ /* jshint boss:true */
+ } while (el = el.parentNode);
+
+ if (window.DOMMatrix) {
+ return new DOMMatrix(appliedTransforms);
+ } else if (window.WebKitCSSMatrix) {
+ return new WebKitCSSMatrix(appliedTransforms);
+ } else if (window.CSSMatrix) {
+ return new CSSMatrix(appliedTransforms);
+ }
+ }
+
+
+ function _find(ctx, tagName, iterator) {
+ if (ctx) {
+ var list = ctx.getElementsByTagName(tagName), i = 0, n = list.length;
+
+ if (iterator) {
+ for (; i < n; i++) {
+ iterator(list[i], i);
+ }
+ }
+
+ return list;
+ }
+
+ return [];
+ }
+
+
+
+ function _dispatchEvent(sortable, rootEl, name, targetEl, toEl, fromEl, startIndex, newIndex, originalEvt) {
+ sortable = (sortable || rootEl[expando]);
+ var evt,
+ options = sortable.options,
+ onName = 'on' + name.charAt(0).toUpperCase() + name.substr(1);
+ // Support for new CustomEvent feature
+ if (window.CustomEvent && !IE11OrLess && !Edge) {
+ evt = new CustomEvent(name, {
+ bubbles: true,
+ cancelable: true
+ });
+ } else {
+ evt = document.createEvent('Event');
+ evt.initEvent(name, true, true);
+ }
+
+ evt.to = toEl || rootEl;
+ evt.from = fromEl || rootEl;
+ evt.item = targetEl || rootEl;
+ evt.clone = cloneEl;
+
+ evt.oldIndex = startIndex;
+ evt.newIndex = newIndex;
+
+ evt.originalEvent = originalEvt;
+
+ if (rootEl) {
+ rootEl.dispatchEvent(evt);
+ }
+
+ if (options[onName]) {
+ options[onName].call(sortable, evt);
+ }
+ }
+
+
+ function _onMove(fromEl, toEl, dragEl, dragRect, targetEl, targetRect, originalEvt, willInsertAfter) {
+ var evt,
+ sortable = fromEl[expando],
+ onMoveFn = sortable.options.onMove,
+ retVal;
+ // Support for new CustomEvent feature
+ if (window.CustomEvent && !IE11OrLess && !Edge) {
+ evt = new CustomEvent('move', {
+ bubbles: true,
+ cancelable: true
+ });
+ } else {
+ evt = document.createEvent('Event');
+ evt.initEvent('move', true, true);
+ }
+
+ evt.to = toEl;
+ evt.from = fromEl;
+ evt.dragged = dragEl;
+ evt.draggedRect = dragRect;
+ evt.related = targetEl || toEl;
+ evt.relatedRect = targetRect || _getRect(toEl);
+ evt.willInsertAfter = willInsertAfter;
+
+ evt.originalEvent = originalEvt;
+
+ fromEl.dispatchEvent(evt);
+
+ if (onMoveFn) {
+ retVal = onMoveFn.call(sortable, evt, originalEvt);
+ }
+
+ return retVal;
+ }
+
+ function _disableDraggable(el) {
+ el.draggable = false;
+ }
+
+ function _unsilent() {
+ _silent = false;
+ }
+
+ /**
+ * Gets nth child of el, ignoring hidden children, sortable's elements (does not ignore clone if it's visible)
+ * and non-draggable elements
+ * @param {HTMLElement} el The parent element
+ * @param {Number} childNum The index of the child
+ * @param {Object} options Parent Sortable's options
+ * @return {HTMLElement} The child at index childNum, or null if not found
+ */
+ function _getChild(el, childNum, options) {
+ var currentChild = 0,
+ i = 0,
+ children = el.children;
+
+ while (i < children.length) {
+ if (
+ children[i].style.display !== 'none' &&
+ children[i] !== ghostEl &&
+ children[i] !== dragEl &&
+ _closest(children[i], options.draggable, el, false)
+ ) {
+ if (currentChild === childNum) {
+ return children[i];
+ }
+ currentChild++;
+ }
+
+ i++;
+ }
+ return null;
+ }
+
+ /**
+ * Gets the last child in the el, ignoring ghostEl or invisible elements (clones)
+ * @param {HTMLElement} el Parent element
+ * @return {HTMLElement} The last child, ignoring ghostEl
+ */
+ function _lastChild(el) {
+ var last = el.lastElementChild;
+
+ while (last === ghostEl || last.style.display === 'none') {
+ last = last.previousElementSibling;
+
+ if (!last) break;
+ }
+
+ return last || null;
+ }
+
+ function _ghostIsLast(evt, axis, el) {
+ var elRect = _getRect(_lastChild(el)),
+ mouseOnAxis = axis === 'vertical' ? evt.clientY : evt.clientX,
+ mouseOnOppAxis = axis === 'vertical' ? evt.clientX : evt.clientY,
+ targetS2 = axis === 'vertical' ? elRect.bottom : elRect.right,
+ targetS1Opp = axis === 'vertical' ? elRect.left : elRect.top,
+ targetS2Opp = axis === 'vertical' ? elRect.right : elRect.bottom,
+ spacer = 10;
+
+ return (
+ axis === 'vertical' ?
+ (mouseOnOppAxis > targetS2Opp + spacer || mouseOnOppAxis <= targetS2Opp && mouseOnAxis > targetS2 && mouseOnOppAxis >= targetS1Opp) :
+ (mouseOnAxis > targetS2 && mouseOnOppAxis > targetS1Opp || mouseOnAxis <= targetS2 && mouseOnOppAxis > targetS2Opp + spacer)
+ );
+ }
+
+ function _getSwapDirection(evt, target, axis, swapThreshold, invertedSwapThreshold, invertSwap, isLastTarget) {
+ var targetRect = _getRect(target),
+ mouseOnAxis = axis === 'vertical' ? evt.clientY : evt.clientX,
+ targetLength = axis === 'vertical' ? targetRect.height : targetRect.width,
+ targetS1 = axis === 'vertical' ? targetRect.top : targetRect.left,
+ targetS2 = axis === 'vertical' ? targetRect.bottom : targetRect.right,
+ dragRect = _getRect(dragEl),
+ invert = false;
+
+
+ if (!invertSwap) {
+ // Never invert or create dragEl shadow when target movemenet causes mouse to move past the end of regular swapThreshold
+ if (isLastTarget && targetMoveDistance < targetLength * swapThreshold) { // multiplied only by swapThreshold because mouse will already be inside target by (1 - threshold) * targetLength / 2
+ // check if past first invert threshold on side opposite of lastDirection
+ if (!pastFirstInvertThresh &&
+ (lastDirection === 1 ?
+ (
+ mouseOnAxis > targetS1 + targetLength * invertedSwapThreshold / 2
+ ) :
+ (
+ mouseOnAxis < targetS2 - targetLength * invertedSwapThreshold / 2
+ )
+ )
+ )
+ {
+ // past first invert threshold, do not restrict inverted threshold to dragEl shadow
+ pastFirstInvertThresh = true;
+ }
+
+ if (!pastFirstInvertThresh) {
+ var dragS1 = axis === 'vertical' ? dragRect.top : dragRect.left,
+ dragS2 = axis === 'vertical' ? dragRect.bottom : dragRect.right;
+ // dragEl shadow (target move distance shadow)
+ if (
+ lastDirection === 1 ?
+ (
+ mouseOnAxis < targetS1 + targetMoveDistance // over dragEl shadow
+ ) :
+ (
+ mouseOnAxis > targetS2 - targetMoveDistance
+ )
+ )
+ {
+ return lastDirection * -1;
+ }
+ } else {
+ invert = true;
+ }
+ } else {
+ // Regular
+ if (
+ mouseOnAxis > targetS1 + (targetLength * (1 - swapThreshold) / 2) &&
+ mouseOnAxis < targetS2 - (targetLength * (1 - swapThreshold) / 2)
+ ) {
+ return ((mouseOnAxis > targetS1 + targetLength / 2) ? -1 : 1);
+ }
+ }
+ }
+
+ invert = invert || invertSwap;
+
+ if (invert) {
+ // Invert of regular
+ if (
+ mouseOnAxis < targetS1 + (targetLength * invertedSwapThreshold / 2) ||
+ mouseOnAxis > targetS2 - (targetLength * invertedSwapThreshold / 2)
+ )
+ {
+ return ((mouseOnAxis > targetS1 + targetLength / 2) ? 1 : -1);
+ }
+ }
+
+ return 0;
+ }
+
+ /**
+ * Gets the direction dragEl must be swapped relative to target in order to make it
+ * seem that dragEl has been "inserted" into that element's position
+ * @param {HTMLElement} target The target whose position dragEl is being inserted at
+ * @param {Object} options options of the parent sortable
+ * @return {Number} Direction dragEl must be swapped
+ */
+ function _getInsertDirection(target, options) {
+ var dragElIndex = _index(dragEl, options.draggable),
+ targetIndex = _index(target, options.draggable);
+
+ if (dragElIndex < targetIndex) {
+ return 1;
+ } else {
+ return -1;
+ }
+ }
+
+
+ /**
+ * Generate id
+ * @param {HTMLElement} el
+ * @returns {String}
+ * @private
+ */
+ function _generateId(el) {
+ var str = el.tagName + el.className + el.src + el.href + el.textContent,
+ i = str.length,
+ sum = 0;
+
+ while (i--) {
+ sum += str.charCodeAt(i);
+ }
+
+ return sum.toString(36);
+ }
+
+ /**
+ * Returns the index of an element within its parent for a selected set of
+ * elements
+ * @param {HTMLElement} el
+ * @param {selector} selector
+ * @return {number}
+ */
+ function _index(el, selector) {
+ var index = 0;
+
+ if (!el || !el.parentNode) {
+ return -1;
+ }
+
+ while (el && (el = el.previousElementSibling)) {
+ if ((el.nodeName.toUpperCase() !== 'TEMPLATE') && el !== cloneEl) {
+ index++;
+ }
+ }
+
+ return index;
+ }
+
+ function _matches(/**HTMLElement*/el, /**String*/selector) {
+ if (el) {
+ try {
+ if (el.matches) {
+ return el.matches(selector);
+ } else if (el.msMatchesSelector) {
+ return el.msMatchesSelector(selector);
+ } else if (el.webkitMatchesSelector) {
+ return el.webkitMatchesSelector(selector);
+ }
+ } catch(_) {
+ return false;
+ }
+ }
+
+ return false;
+ }
+
+ var _throttleTimeout;
+ function _throttle(callback, ms) {
+ return function () {
+ if (!_throttleTimeout) {
+ var args = arguments,
+ _this = this;
+
+ _throttleTimeout = setTimeout(function () {
+ if (args.length === 1) {
+ callback.call(_this, args[0]);
+ } else {
+ callback.apply(_this, args);
+ }
+
+ _throttleTimeout = void 0;
+ }, ms);
+ }
+ };
+ }
+
+ function _cancelThrottle() {
+ clearTimeout(_throttleTimeout);
+ _throttleTimeout = void 0;
+ }
+
+ function _extend(dst, src) {
+ if (dst && src) {
+ for (var key in src) {
+ if (src.hasOwnProperty(key)) {
+ dst[key] = src[key];
+ }
+ }
+ }
+
+ return dst;
+ }
+
+ function _clone(el) {
+ if (Polymer && Polymer.dom) {
+ return Polymer.dom(el).cloneNode(true);
+ }
+ else if ($) {
+ return $(el).clone(true)[0];
+ }
+ else {
+ return el.cloneNode(true);
+ }
+ }
+
+ function _saveInputCheckedState(root) {
+ savedInputChecked.length = 0;
+
+ var inputs = root.getElementsByTagName('input');
+ var idx = inputs.length;
+
+ while (idx--) {
+ var el = inputs[idx];
+ el.checked && savedInputChecked.push(el);
+ }
+ }
+
+ function _nextTick(fn) {
+ return setTimeout(fn, 0);
+ }
+
+ function _cancelNextTick(id) {
+ return clearTimeout(id);
+ }
+
+
+ /**
+ * Returns the "bounding client rect" of given element
+ * @param {HTMLElement} el The element whose boundingClientRect is wanted
+ * @param {[HTMLElement]} container the parent the element will be placed in
+ * @param {[Boolean]} adjustForTransform Whether the rect should compensate for parent's transform
+ * (used for fixed positioning on el)
+ * @return {Object} The boundingClientRect of el
+ */
+ function _getRect(el, container, adjustForTransform) {
+ if (!el.getBoundingClientRect && el !== win) return;
+
+ var elRect,
+ top,
+ left,
+ bottom,
+ right,
+ height,
+ width;
+
+ if (el !== win) {
+ elRect = el.getBoundingClientRect();
+ top = elRect.top;
+ left = elRect.left;
+ bottom = elRect.bottom;
+ right = elRect.right;
+ height = elRect.height;
+ width = elRect.width;
+ } else {
+ top = 0;
+ left = 0;
+ bottom = window.innerHeight;
+ right = window.innerWidth;
+ height = window.innerHeight;
+ width = window.innerWidth;
+ }
+
+ if (adjustForTransform && el !== win) {
+ // Adjust for translate()
+ container = container || el.parentNode;
+
+ // solves #1123 (see: https://stackoverflow.com/a/37953806/6088312)
+ // Not needed on <= IE11
+ if (!IE11OrLess) {
+ do {
+ if (container && container.getBoundingClientRect && _css(container, 'transform') !== 'none') {
+ var containerRect = container.getBoundingClientRect();
+
+ // Set relative to edges of padding box of container
+ top -= containerRect.top + parseInt(_css(container, 'border-top-width'));
+ left -= containerRect.left + parseInt(_css(container, 'border-left-width'));
+ bottom = top + elRect.height;
+ right = left + elRect.width;
+
+ break;
+ }
+ /* jshint boss:true */
+ } while (container = container.parentNode);
+ }
+
+ // Adjust for scale()
+ var matrix = _matrix(el),
+ scaleX = matrix && matrix.a,
+ scaleY = matrix && matrix.d;
+
+ if (matrix) {
+ top /= scaleY;
+ left /= scaleX;
+
+ width /= scaleX;
+ height /= scaleY;
+
+ bottom = top + height;
+ right = left + width;
+ }
+ }
+
+ return {
+ top: top,
+ left: left,
+ bottom: bottom,
+ right: right,
+ width: width,
+ height: height
+ };
+ }
+
+
+ /**
+ * Checks if a side of an element is scrolled past a side of it's parents
+ * @param {HTMLElement} el The element who's side being scrolled out of view is in question
+ * @param {String} side Side of the element in question ('top', 'left', 'right', 'bottom')
+ * @return {Boolean} Whether the element is overflowing the viewport on the given side of it's parent
+ */
+ function _isScrolledPast(el, side) {
+ var parent = _getParentAutoScrollElement(parent, true),
+ elSide = _getRect(el)[side];
+
+ /* jshint boss:true */
+ while (parent) {
+ var parentSide = _getRect(parent)[side],
+ visible;
+
+ if (side === 'top' || side === 'left') {
+ visible = elSide >= parentSide;
+ } else {
+ visible = elSide <= parentSide;
+ }
+
+ if (!visible) return true;
+
+ if (parent === win) break;
+
+ parent = _getParentAutoScrollElement(parent, false);
+ }
+
+ return false;
+ }
+
+ // Fixed #973:
+ _on(document, 'touchmove', function(evt) {
+ if ((Sortable.active || awaitingDragStarted) && evt.cancelable) {
+ evt.preventDefault();
+ }
+ });
+
+
+ // Export utils
+ Sortable.utils = {
+ on: _on,
+ off: _off,
+ css: _css,
+ find: _find,
+ is: function (el, selector) {
+ return !!_closest(el, selector, el, false);
+ },
+ extend: _extend,
+ throttle: _throttle,
+ closest: _closest,
+ toggleClass: _toggleClass,
+ clone: _clone,
+ index: _index,
+ nextTick: _nextTick,
+ cancelNextTick: _cancelNextTick,
+ detectDirection: _detectDirection,
+ getChild: _getChild
+ };
+
+
+ /**
+ * Create sortable instance
+ * @param {HTMLElement} el
+ * @param {Object} [options]
+ */
+ Sortable.create = function (el, options) {
+ return new Sortable(el, options);
+ };
+
+
+ // Export
+ Sortable.version = '1.8.3';
+ return Sortable;
+}); \ No newline at end of file
diff --git a/public/js/vendor/jquery.fn.sortable.js b/public/js/vendor/jquery.fn.sortable.js
new file mode 100644
index 0000000..cd5189a
--- /dev/null
+++ b/public/js/vendor/jquery.fn.sortable.js
@@ -0,0 +1,76 @@
+(function (factory) {
+ "use strict";
+ var sortable,
+ jq,
+ _this = this
+ ;
+
+ if (typeof define === "function" && define.amd) {
+ try {
+ define(["sortablejs", "jquery"], function(Sortable, $) {
+ sortable = Sortable;
+ jq = $;
+ checkErrors();
+ factory(Sortable, $);
+ });
+ } catch(err) {
+ checkErrors();
+ }
+ return;
+ } else if (typeof exports === 'object') {
+ try {
+ sortable = require('sortablejs');
+ jq = require('jquery');
+ } catch(err) { }
+ }
+
+ if (typeof jQuery === 'function' || typeof $ === 'function') {
+ jq = jQuery || $;
+ }
+
+ if (typeof Sortable !== 'undefined') {
+ sortable = Sortable;
+ }
+
+ function checkErrors() {
+ if (!jq) {
+ throw new Error('jQuery is required for jquery-sortablejs');
+ }
+
+ if (!sortable) {
+ throw new Error('SortableJS is required for jquery-sortablejs (https://github.com/SortableJS/Sortable)');
+ }
+ }
+ checkErrors();
+ factory(sortable, jq);
+})(function (Sortable, $) {
+ "use strict";
+
+ $.fn.sortable = function (options) {
+ var retVal,
+ args = arguments;
+
+ this.each(function () {
+ var $el = $(this),
+ sortable = $el.data('sortable');
+
+ if (!sortable && (options instanceof Object || !options)) {
+ sortable = new Sortable(this, options);
+ $el.data('sortable', sortable);
+ } else if (sortable) {
+ if (options === 'destroy') {
+ sortable.destroy();
+ $el.removeData('sortable');
+ } else if (options === 'widget') {
+ retVal = sortable;
+ } else if (typeof sortable[options] === 'function') {
+ retVal = sortable[options].apply(sortable, [].slice.call(args, 1));
+ } else if (options in sortable.options) {
+ retVal = sortable.option.apply(sortable, args);
+ }
+ }
+ });
+
+ return (retVal === void 0) ? this : retVal;
+ };
+}); \ No newline at end of file
diff --git a/run.php b/run.php
new file mode 100644
index 0000000..6cb81f8
--- /dev/null
+++ b/run.php
@@ -0,0 +1,10 @@
+<?php
+
+$this->provideHook('monitoring/HostActions');
+$this->provideHook('monitoring/ServiceActions');
+$this->provideHook('monitoring/DetailviewExtension');
+$this->provideHook('icingadb/HostActions');
+$this->provideHook('icingadb/ServiceActions');
+$this->provideHook('icingadb/icingadbSupport');
+$this->provideHook('icingadb/ServiceDetailExtension');
+//$this->provideHook('director/shipConfigFiles');
diff --git a/test/bootstrap.php b/test/bootstrap.php
new file mode 100644
index 0000000..e12df22
--- /dev/null
+++ b/test/bootstrap.php
@@ -0,0 +1,16 @@
+<?php
+
+use Icinga\Module\Businessprocess\Test\Bootstrap;
+
+call_user_func(function () {
+ $basedir = dirname(__DIR__);
+ if (! class_exists('PHPUnit_Framework_TestCase')) {
+ require_once __DIR__ . '/phpunit-compat.php';
+ }
+
+ $include_path = $basedir . '/vendor' . PATH_SEPARATOR . ini_get('include_path');
+ ini_set('include_path', $include_path);
+
+ require_once $basedir . '/library/Businessprocess/Test/Bootstrap.php';
+ Bootstrap::cli($basedir);
+});
diff --git a/test/config/authentication.ini b/test/config/authentication.ini
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/config/authentication.ini
diff --git a/test/config/config.ini b/test/config/config.ini
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/config/config.ini
diff --git a/test/config/modules/businessprocess/processes/also-with-semicolons.conf b/test/config/modules/businessprocess/processes/also-with-semicolons.conf
new file mode 100644
index 0000000..a023aaf
--- /dev/null
+++ b/test/config/modules/businessprocess/processes/also-with-semicolons.conf
@@ -0,0 +1,8 @@
+############################################
+#
+# Title: Also With Semicolons
+#
+############################################
+
+b\;ar =
+display 1;b\;ar;Bar
diff --git a/test/config/modules/businessprocess/processes/broken_wrong-operator.conf b/test/config/modules/businessprocess/processes/broken_wrong-operator.conf
new file mode 100644
index 0000000..9a58f23
--- /dev/null
+++ b/test/config/modules/businessprocess/processes/broken_wrong-operator.conf
@@ -0,0 +1 @@
+hostsAnd = host1;Hoststatus + host2;Hoststatus \ No newline at end of file
diff --git a/test/config/modules/businessprocess/processes/combined.conf b/test/config/modules/businessprocess/processes/combined.conf
new file mode 100644
index 0000000..3b0fc5d
--- /dev/null
+++ b/test/config/modules/businessprocess/processes/combined.conf
@@ -0,0 +1 @@
+all = @simple_with-header:top & @simple_without-header:minTwo & @simple_with-header:minTwo \ No newline at end of file
diff --git a/test/config/modules/businessprocess/processes/simple_with-header.conf b/test/config/modules/businessprocess/processes/simple_with-header.conf
new file mode 100644
index 0000000..802fbb2
--- /dev/null
+++ b/test/config/modules/businessprocess/processes/simple_with-header.conf
@@ -0,0 +1,13 @@
+############################################
+#
+# Title: Simple with header
+#
+############################################
+
+hostsAnd = host1;Hoststatus & host2;Hoststatus
+servicesOr = host1;ping | host2;ping | host3;ping
+singleHost = host1;Hoststatus
+minTwo = 2 of: hostsAnd + servicesOr + singleHost
+top = minTwo & hostsAnd & servicesOr
+display 1;top;Top Node
+info_url top;https://top.example.com/
diff --git a/test/config/modules/businessprocess/processes/simple_without-header.conf b/test/config/modules/businessprocess/processes/simple_without-header.conf
new file mode 100644
index 0000000..7d4efc6
--- /dev/null
+++ b/test/config/modules/businessprocess/processes/simple_without-header.conf
@@ -0,0 +1,6 @@
+hostsAnd = host1;Hoststatus & host2;Hoststatus
+servicesOr = host1;ping | host2;ping | host3;ping
+singleHost = host1;Hoststatus
+minTwo = 2 of: hostsAnd + servicesOr + singleHost
+top = minTwo & hostsAnd & servicesOr
+display 1;top;Top Node \ No newline at end of file
diff --git a/test/config/modules/businessprocess/processes/with-semicolons.conf b/test/config/modules/businessprocess/processes/with-semicolons.conf
new file mode 100644
index 0000000..310d473
--- /dev/null
+++ b/test/config/modules/businessprocess/processes/with-semicolons.conf
@@ -0,0 +1,14 @@
+############################################
+#
+# Title: With Semicolons
+#
+############################################
+
+hostsAnd = host\;1;Hoststatus & host2;Hoststatus
+servicesOr = host\;1;pi;ng | host2;ping | host3;ping
+singleHost = host\;1;Hoststatus & to\;p & @also-with-semicolons:b\;ar
+to\;p = hostsAnd & servicesOr & singleHost
+display 1;to\;p;Top Node
+info_url to\;p;https://top.example.com/
+no\;alias =
+display 1;no\;alias;no;alias
diff --git a/test/php/library/Businessprocess/BpConfigTest.php b/test/php/library/Businessprocess/BpConfigTest.php
new file mode 100644
index 0000000..f42c58c
--- /dev/null
+++ b/test/php/library/Businessprocess/BpConfigTest.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Tests\Icinga\Module\Businessprocess;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\Test\BaseTestCase;
+
+class BpConfigTest extends BaseTestCase
+{
+ public function testJoinNodeName()
+ {
+ $this->assertSame(
+ 'foo;bar',
+ BpConfig::joinNodeName('foo', 'bar')
+ );
+ $this->assertSame(
+ 'foo\;bar',
+ BpConfig::joinNodeName('foo;bar')
+ );
+ $this->assertSame(
+ 'foo\;bar;baroof',
+ BpConfig::joinNodeName('foo;bar', 'baroof')
+ );
+ $this->assertSame(
+ 'foo\;bar;bar;oof',
+ BpConfig::joinNodeName('foo;bar', 'bar;oof')
+ );
+ }
+
+ public function testSplitNodeName()
+ {
+ $this->assertSame(
+ ['foo', 'bar'],
+ BpConfig::splitNodeName('foo;bar')
+ );
+ $this->assertSame(
+ ['foo;bar', null],
+ BpConfig::splitNodeName('foo\;bar')
+ );
+ $this->assertSame(
+ ['foo;bar', 'baroof'],
+ BpConfig::splitNodeName('foo\;bar;baroof')
+ );
+ $this->assertSame(
+ ['foo;bar', 'bar;oof'],
+ BpConfig::splitNodeName('foo\;bar;bar;oof')
+ );
+ }
+}
diff --git a/test/php/library/Businessprocess/BpNodeTest.php b/test/php/library/Businessprocess/BpNodeTest.php
new file mode 100644
index 0000000..c3da723
--- /dev/null
+++ b/test/php/library/Businessprocess/BpNodeTest.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Tests\Icinga\Module\Businessprocess;
+
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\Test\BaseTestCase;
+
+class BpNodeTest extends BaseTestCase
+{
+ public function testThrowsNestingErrorWhenCheckedForLoops()
+ {
+ $this->expectException(\Icinga\Module\Businessprocess\Exception\NestingError::class);
+
+ /** @var BpNode $bpNode */
+ $bpNode = $this->makeLoop()->getNode('d');
+ $bpNode->checkForLoops();
+ }
+
+ public function testNestingErrorReportsFullLoop()
+ {
+ $this->expectException(\Icinga\Module\Businessprocess\Exception\NestingError::class);
+ $this->expectExceptionMessage('d -> a -> b -> c -> a');
+
+ /** @var BpNode $bpNode */
+ $bpNode = $this->makeLoop()->getNode('d');
+ $bpNode->checkForLoops();
+ }
+
+ public function testStateForALoopGivesUnknown()
+ {
+ $loop = $this->makeLoop();
+ /** @var BpNode $bpNode */
+ $bpNode = $loop->getNode('d');
+ $this->assertEquals(
+ 'UNKNOWN',
+ $bpNode->getStateName()
+ );
+ }
+}
diff --git a/test/php/library/Businessprocess/HostNodeTest.php b/test/php/library/Businessprocess/HostNodeTest.php
new file mode 100644
index 0000000..ef4155d
--- /dev/null
+++ b/test/php/library/Businessprocess/HostNodeTest.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Tests\Icinga\Module\Businessprocess;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\HostNode;
+use Icinga\Module\Businessprocess\Test\BaseTestCase;
+
+class HostNodeTest extends BaseTestCase
+{
+ public function testReturnsCorrectHostName()
+ {
+ $this->assertEquals(
+ 'localhost',
+ $this->localhost()->getHostname()
+ );
+ }
+
+ public function testReturnsCorrectIdentifierWhenCastedToString()
+ {
+ $this->assertEquals(
+ 'localhost;Hoststatus',
+ $this->localhost()->getName()
+ );
+ }
+
+ public function testReturnsCorrectAlias()
+ {
+ $this->assertEquals(
+ 'localhost',
+ $this->localhost()->getAlias()
+ );
+ }
+
+ public function testRendersCorrectLink()
+ {
+ $this->assertEquals(
+ '<a href="/icingaweb2/businessprocess/host/show?host=localhost">'
+ . 'localhost</a>',
+ $this->localhost()->getLink()->render()
+ );
+ }
+
+ public function testSettingAnInvalidStateFails()
+ {
+ $this->expectException(\Icinga\Exception\ProgrammingError::class);
+ $bp = new BpConfig();
+ $host = $bp->createHost('localhost')->setState(98);
+ $bp->createBp('p')->addChild($host)->getState();
+ }
+
+ /**
+ * @return HostNode
+ */
+ protected function localhost()
+ {
+ $bp = new BpConfig();
+ return (new HostNode((object) array(
+ 'hostname' => 'localhost',
+ 'state' => 0,
+ )))->setBpConfig($bp)->setAlias('localhost');
+ }
+}
diff --git a/test/php/library/Businessprocess/MetadataTest.php b/test/php/library/Businessprocess/MetadataTest.php
new file mode 100644
index 0000000..765caf8
--- /dev/null
+++ b/test/php/library/Businessprocess/MetadataTest.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Tests\Icinga\Module\Businessprocess;
+
+use Icinga\Module\Businessprocess\Metadata;
+use Icinga\Module\Businessprocess\Test\BaseTestCase;
+
+class MetadataTest extends BaseTestCase
+{
+ public function testDetectsMatchingPrefixes()
+ {
+ $meta = new Metadata('matchme');
+ $this->assertFalse(
+ $meta->nameIsPrefixedWithOneOf(array())
+ );
+ $this->assertFalse(
+ $meta->nameIsPrefixedWithOneOf(array('matchr', 'atchme'))
+ );
+ $this->assertTrue(
+ $meta->nameIsPrefixedWithOneOf(array('not', 'mat', 'yes'))
+ );
+ $this->assertTrue(
+ $meta->nameIsPrefixedWithOneOf(array('m'))
+ );
+ $this->assertTrue(
+ $meta->nameIsPrefixedWithOneOf(array('matchme'))
+ );
+ $this->assertFalse(
+ $meta->nameIsPrefixedWithOneOf(array('matchmenot'))
+ );
+ }
+}
diff --git a/test/php/library/Businessprocess/Operators/AndOperatorTest.php b/test/php/library/Businessprocess/Operators/AndOperatorTest.php
new file mode 100644
index 0000000..9e87cf1
--- /dev/null
+++ b/test/php/library/Businessprocess/Operators/AndOperatorTest.php
@@ -0,0 +1,214 @@
+<?php
+
+namespace Tests\Icinga\Module\Businessprocess\Operator;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\Test\BaseTestCase;
+use Icinga\Module\Businessprocess\Storage\LegacyStorage;
+
+class AndOperatorTest extends BaseTestCase
+{
+ public function testTheOperatorCanBeParsed()
+ {
+ $storage = new LegacyStorage($this->emptyConfigSection());
+ $expressions = array(
+ 'a = b;c',
+ 'a = b;c & c;d & d;e',
+ );
+
+ foreach ($expressions as $expression) {
+ $this->assertInstanceOf(
+ 'Icinga\\Module\\Businessprocess\\BpConfig',
+ $storage->loadFromString('dummy', $expression)
+ );
+ }
+ }
+
+ public function testThreeTimesCriticalIsCritical()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 2);
+ $bp->setNodeState('c;d', 2);
+ $bp->setNodeState('d;e', 2);
+
+ $this->assertEquals(
+ 'CRITICAL',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testTwoTimesCriticalAndOkIsCritical()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 2);
+ $bp->setNodeState('c;d', 0);
+ $bp->setNodeState('d;e', 2);
+
+ $this->assertEquals(
+ 'CRITICAL',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testCriticalAndWarningAndOkIsCritical()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 2);
+ $bp->setNodeState('c;d', 1);
+ $bp->setNodeState('d;e', 0);
+
+ $this->assertEquals(
+ 'CRITICAL',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testUnknownAndWarningAndOkIsUnknown()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 0);
+ $bp->setNodeState('c;d', 1);
+ $bp->setNodeState('d;e', 3);
+
+ $this->assertEquals(
+ 'UNKNOWN',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testTwoTimesWarningAndOkIsWarning()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 0);
+ $bp->setNodeState('c;d', 1);
+ $bp->setNodeState('d;e', 1);
+
+ $this->assertEquals(
+ 'WARNING',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testThreeTimesOkIsOk()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 0);
+ $bp->setNodeState('c;d', 0);
+ $bp->setNodeState('d;e', 0);
+
+ $this->assertEquals(
+ 'OK',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testSimpleAndOperationWorksCorrectly()
+ {
+ $bp = new BpConfig();
+ $bp->throwErrors();
+ $host = $bp->createHost('localhost')->setState(1);
+ $service = $bp->createService('localhost', 'ping')->setState(1);
+ $p = $bp->createBp('p');
+ $p->addChild($host);
+ $p->addChild($service);
+
+ $this->assertEquals(
+ 'DOWN',
+ $host->getStateName()
+ );
+
+ $this->assertEquals(
+ 'WARNING',
+ $service->getStateName()
+ );
+
+ $this->assertEquals(
+ 'CRITICAL',
+ $p->getStateName()
+ );
+ }
+
+ public function testSimpleOrOperationWorksCorrectly()
+ {
+ $bp = new BpConfig();
+ $bp->throwErrors();
+ $host = $bp->createHost('localhost')->setState(1);
+ $service = $bp->createService('localhost', 'ping')->setState(1);
+ $p = $bp->createBp('p', '|');
+ $p->addChild($host);
+ $p->addChild($service);
+
+ $this->assertEquals('DOWN', $host->getStateName());
+ $this->assertEquals('WARNING', $service->getStateName());
+ $this->assertEquals('WARNING', $p->getStateName());
+ }
+
+ public function testPendingIsAccepted()
+ {
+ $bp = new BpConfig();
+ $host = $bp->createHost('localhost')->setState(99);
+ $service = $bp->createService('localhost', 'ping')->setState(99);
+ $p = $bp->createBp('p')
+ ->addChild($host)
+ ->addChild($service);
+
+ $this->assertEquals(
+ 'PENDING',
+ $p->getStateName()
+ );
+ }
+
+ public function testWhetherWarningIsWorseThanPending()
+ {
+ $bp = new BpConfig();
+ $host = $bp->createHost('localhost')->setState(99);
+ $service = $bp->createService('localhost', 'ping')->setState(1);
+ $p = $bp->createBp('p')
+ ->addChild($host)
+ ->addChild($service);
+
+ $this->assertEquals(
+ 'WARNING',
+ $p->getStateName()
+ );
+ }
+
+ public function testPendingIsWorseThanUpOrOk()
+ {
+ $bp = new BpConfig();
+ $host = $bp->createHost('localhost')->setState(99);
+ $service = $bp->createService('localhost', 'ping')->setState(0);
+ $p = $bp->createBp('p')
+ ->addChild($host)
+ ->addChild($service);
+
+ $this->assertEquals(
+ 'PENDING',
+ $p->getStateName()
+ );
+
+ $p->clearState();
+ $host->setState(0);
+ $service->setState(99);
+
+ $this->assertEquals(
+ 'PENDING',
+ $p->getStateName()
+ );
+ }
+
+ /**
+ * @return BpConfig
+ */
+ protected function getBp()
+ {
+ $storage = new LegacyStorage($this->emptyConfigSection());
+ $expression = 'a = b;c & c;d & d;e';
+ $bp = $storage->loadFromString('dummy', $expression);
+ $bp->createBp('b');
+ $bp->createBp('c');
+ $bp->createBp('d');
+
+ return $bp;
+ }
+}
diff --git a/test/php/library/Businessprocess/Operators/DegradedOperatorTest.php b/test/php/library/Businessprocess/Operators/DegradedOperatorTest.php
new file mode 100644
index 0000000..72ed5e5
--- /dev/null
+++ b/test/php/library/Businessprocess/Operators/DegradedOperatorTest.php
@@ -0,0 +1,159 @@
+<?php
+
+namespace Tests\Icinga\Module\Businessprocess\Operator;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\Test\BaseTestCase;
+use Icinga\Module\Businessprocess\Storage\LegacyStorage;
+
+class DegradedOperatorTest extends BaseTestCase
+{
+ public function testDegradedOperatorCanBeParsed()
+ {
+ $storage = new LegacyStorage($this->emptyConfigSection());
+ $expressions = [
+ 'a = b;c',
+ 'a = b;c % c;d % d;e',
+ ];
+
+ foreach ($expressions as $expression) {
+ $this->assertInstanceOf(
+ 'Icinga\\Module\\Businessprocess\\BpConfig',
+ $storage->loadFromString('dummy', $expression)
+ );
+ }
+ }
+
+ public function testThreeTimesCriticalIsWarning()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 2);
+ $bp->setNodeState('c;d', 2);
+ $bp->setNodeState('d;e', 2);
+
+ $this->assertEquals(
+ 'WARNING',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testTwoTimesCriticalAndOkIsWarning()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 2);
+ $bp->setNodeState('c;d', 0);
+ $bp->setNodeState('d;e', 2);
+
+ $this->assertEquals(
+ 'WARNING',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testCriticalAndWarningAndOkIsWarning()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 2);
+ $bp->setNodeState('c;d', 1);
+ $bp->setNodeState('d;e', 0);
+
+ $this->assertEquals(
+ 'WARNING',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testUnknownAndWarningAndOkIsUnknown()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 0);
+ $bp->setNodeState('c;d', 1);
+ $bp->setNodeState('d;e', 3);
+
+ $this->assertEquals(
+ 'UNKNOWN',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testTwoTimesWarningAndOkIsWarning()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 0);
+ $bp->setNodeState('c;d', 1);
+ $bp->setNodeState('d;e', 1);
+
+ $this->assertEquals(
+ 'WARNING',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testUnknownAndWarningAndCriticalIsWarning()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 2);
+ $bp->setNodeState('c;d', 1);
+ $bp->setNodeState('d;e', 3);
+
+ $this->assertEquals(
+ 'WARNING',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testThreeTimesOkIsOk()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 0);
+ $bp->setNodeState('c;d', 0);
+ $bp->setNodeState('d;e', 0);
+
+ $this->assertEquals(
+ 'OK',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testSimpleDegOperationWorksCorrectly()
+ {
+ $bp = new BpConfig();
+ $bp->throwErrors();
+ $host = $bp->createHost('localhost')->setState(0);
+ $service = $bp->createService('localhost', 'ping')->setState(2);
+ $p = $bp->createBp('p');
+ $p->setOperator('%');
+ $p->addChild($host);
+ $p->addChild($service);
+
+ $this->assertEquals(
+ 'UP',
+ $host->getStateName()
+ );
+
+ $this->assertEquals(
+ 'CRITICAL',
+ $service->getStateName()
+ );
+
+ $this->assertEquals(
+ 'WARNING',
+ $p->getStateName()
+ );
+ }
+
+ /**
+ * @return BpConfig
+ */
+ protected function getBp()
+ {
+ $storage = new LegacyStorage($this->emptyConfigSection());
+ $expression = 'a = b;c % c;d % d;e';
+ $bp = $storage->loadFromString('dummy', $expression);
+ $bp->createBp('b');
+ $bp->createBp('c');
+ $bp->createBp('d');
+
+ return $bp;
+ }
+}
diff --git a/test/php/library/Businessprocess/Operators/MinOperatorTest.php b/test/php/library/Businessprocess/Operators/MinOperatorTest.php
new file mode 100644
index 0000000..986589a
--- /dev/null
+++ b/test/php/library/Businessprocess/Operators/MinOperatorTest.php
@@ -0,0 +1,174 @@
+<?php
+
+namespace Tests\Icinga\Module\Businessprocess\Operator;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\Test\BaseTestCase;
+use Icinga\Module\Businessprocess\Storage\LegacyStorage;
+
+class MinOperatorTest extends BaseTestCase
+{
+ public function testTheOperatorCanBeParsed()
+ {
+ $storage = new LegacyStorage($this->emptyConfigSection());
+ $expressions = array(
+ 'a = 1 of: b;c',
+ 'a = 2 of: b;c + c;d + d;e',
+ );
+ $this->getName();
+ foreach ($expressions as $expression) {
+ $this->assertInstanceOf(
+ 'Icinga\\Module\\Businessprocess\\BpConfig',
+ $storage->loadFromString('dummy', $expression)
+ );
+ }
+ }
+ public function testTwoOfThreeTimesCriticalAreAtLeastCritical()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 2);
+ $bp->setNodeState('c;d', 2);
+ $bp->setNodeState('d;e', 2);
+
+ $this->assertEquals(
+ 'CRITICAL',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testTwoOfTwoTimesCriticalAndUnknownAreAtLeastCritical()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 2);
+ $bp->setNodeState('c;d', 3);
+ $bp->setNodeState('d;e', 2);
+
+ $this->assertEquals(
+ 'CRITICAL',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testTwoOfCriticalAndWarningAndOkAreAtLeastCritical()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 2);
+ $bp->setNodeState('c;d', 1);
+ $bp->setNodeState('d;e', 0);
+
+ $this->assertEquals(
+ 'CRITICAL',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testTwoOfUnknownAndWarningAndCriticalAreAtLeastCritical()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 2);
+ $bp->setNodeState('c;d', 1);
+ $bp->setNodeState('d;e', 3);
+
+ $this->assertEquals(
+ 'CRITICAL',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testTwoOfTwoTimesWarningAndUnknownAreAtLeastUnknown()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 3);
+ $bp->setNodeState('c;d', 1);
+ $bp->setNodeState('d;e', 1);
+
+ $this->assertEquals(
+ 'UNKNOWN',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testTwoOfThreeTimesOkAreAtLeastOk()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 0);
+ $bp->setNodeState('c;d', 0);
+ $bp->setNodeState('d;e', 0);
+
+ $this->assertEquals(
+ 'OK',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testTenWithAllOk()
+ {
+ $bp = $this->getBp(10, 9, 0);
+
+ $this->assertEquals(
+ 'OK',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testTenWithOnlyTwoCritical()
+ {
+ $bp = $this->getBp(10, 8, 0);
+ $bp->setNodeState('b;c', 2);
+ $bp->setNodeState('c;d', 2);
+
+ $this->assertEquals(
+ 'OK',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testTenWithThreeCritical()
+ {
+ $bp = $this->getBp(10, 8, 0);
+ $bp->setNodeState('b;c', 2);
+ $bp->setNodeState('c;d', 2);
+ $bp->setNodeState('d;e', 2);
+
+ $this->assertEquals(
+ 'CRITICAL',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testTenWithThreeWarning()
+ {
+ $bp = $this->getBp(10, 8, 0);
+ $bp->setNodeState('b;c', 1);
+ $bp->setNodeState('c;d', 1);
+ $bp->setNodeState('d;e', 1);
+
+ $this->assertEquals(
+ 'WARNING',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ /**
+ * @return BpConfig
+ */
+ protected function getBp($count = 3, $min = 2, $defaultState = null)
+ {
+ $names = array();
+ $a = 97;
+ for ($i = 1; $i <= $count; $i++) {
+ $names[] = chr($a + $i) . ';' . chr($a + $i + 1);
+ }
+
+ $storage = new LegacyStorage($this->emptyConfigSection());
+ $expression = sprintf('a = %d of: %s', $min, join(' + ', $names));
+ $bp = $storage->loadFromString('dummy', $expression);
+ foreach ($names as $n) {
+ if ($defaultState !== null) {
+ $bp->setNodeState($n, $defaultState);
+ }
+ }
+
+ return $bp;
+ }
+}
diff --git a/test/php/library/Businessprocess/Operators/NotOperatorTest.php b/test/php/library/Businessprocess/Operators/NotOperatorTest.php
new file mode 100644
index 0000000..fb62545
--- /dev/null
+++ b/test/php/library/Businessprocess/Operators/NotOperatorTest.php
@@ -0,0 +1,151 @@
+<?php
+
+namespace Tests\Icinga\Module\Businessprocess\Operator;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\Test\BaseTestCase;
+use Icinga\Module\Businessprocess\Storage\LegacyStorage;
+
+class NotOperatorTest extends BaseTestCase
+{
+ public function testNegationOperatorsCanBeParsed()
+ {
+ $storage = new LegacyStorage($this->emptyConfigSection());
+ $expressions = array(
+ 'a = !b;c',
+ 'a = ! b;c',
+ 'a = b;c ! c;d ! d;e',
+ 'a = ! b;c ! c;d ! d;e !',
+ );
+
+ foreach ($expressions as $expression) {
+ $this->assertInstanceOf(
+ 'Icinga\\Module\\Businessprocess\\BpConfig',
+ $storage->loadFromString('dummy', $expression)
+ );
+ }
+ }
+
+ public function testASimpleNegationGivesTheCorrectResult()
+ {
+ $storage = new LegacyStorage($this->emptyConfigSection());
+ $expression = 'a = !b;c';
+ $bp = $storage->loadFromString('dummy', $expression);
+ $a = $bp->getNode('a');
+ $b = $bp->getNode('b;c')->setState(3);
+ $this->assertEquals(
+ 'OK',
+ $a->getStateName()
+ );
+
+ $a->clearState();
+ $b->setState(0);
+ $this->assertEquals(
+ 'CRITICAL',
+ $a->getStateName()
+ );
+ }
+
+ public function testThreeTimesCriticalIsOk()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 2);
+ $bp->setNodeState('c;d', 2);
+ $bp->setNodeState('d;e', 2);
+
+ $this->assertEquals(
+ 'OK',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testThreeTimesUnknownIsOk()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 3);
+ $bp->setNodeState('c;d', 3);
+ $bp->setNodeState('d;e', 3);
+
+ $this->assertEquals(
+ 'OK',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testThreeTimesWarningIsWarning()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 1);
+ $bp->setNodeState('c;d', 1);
+ $bp->setNodeState('d;e', 1);
+
+ $this->assertEquals(
+ 'WARNING',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testThreeTimesOkIsCritical()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 0);
+ $bp->setNodeState('c;d', 0);
+ $bp->setNodeState('d;e', 0);
+
+ $this->assertEquals(
+ 'CRITICAL',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testNotOkAndWarningAndCriticalIsOk()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 0);
+ $bp->setNodeState('c;d', 1);
+ $bp->setNodeState('d;e', 2);
+
+ $this->assertEquals(
+ 'OK',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testNotWarningAndUnknownAndCriticalIsOk()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 3);
+ $bp->setNodeState('c;d', 2);
+ $bp->setNodeState('d;e', 1);
+
+ $this->assertEquals(
+ 'OK',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testNotTwoTimesWarningAndOkIsWarning()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 0);
+ $bp->setNodeState('c;d', 1);
+ $bp->setNodeState('d;e', 1);
+
+ $this->assertEquals(
+ 'WARNING',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ /**
+ * @return BpConfig
+ */
+ protected function getBp()
+ {
+ $storage = new LegacyStorage($this->emptyConfigSection());
+ $expression = 'a = ! b;c ! c;d ! d;e';
+ $bp = $storage->loadFromString('dummy', $expression);
+
+ return $bp;
+ }
+}
diff --git a/test/php/library/Businessprocess/Operators/OrOperatorTest.php b/test/php/library/Businessprocess/Operators/OrOperatorTest.php
new file mode 100644
index 0000000..02043d0
--- /dev/null
+++ b/test/php/library/Businessprocess/Operators/OrOperatorTest.php
@@ -0,0 +1,116 @@
+<?php
+
+namespace Tests\Icinga\Module\Businessprocess\Operator;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\Test\BaseTestCase;
+use Icinga\Module\Businessprocess\Storage\LegacyStorage;
+
+class OrOperatorTest extends BaseTestCase
+{
+ public function testTheOperatorCanBeParsed()
+ {
+ $storage = new LegacyStorage($this->emptyConfigSection());
+ $expressions = array(
+ 'a = b;c',
+ 'a = b;c | c;d | d;e',
+ );
+
+ foreach ($expressions as $expression) {
+ $this->assertInstanceOf(
+ 'Icinga\\Module\\Businessprocess\\BpConfig',
+ $storage->loadFromString('dummy', $expression)
+ );
+ }
+ }
+
+ public function testThreeTimesCriticalIsCritical()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 2);
+ $bp->setNodeState('c;d', 2);
+ $bp->setNodeState('d;e', 2);
+
+ $this->assertEquals(
+ 'CRITICAL',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testTwoTimesCriticalOrUnknownIsUnknown()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 2);
+ $bp->setNodeState('c;d', 3);
+ $bp->setNodeState('d;e', 2);
+
+ $this->assertEquals(
+ 'UNKNOWN',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testCriticalOrWarningOrOkIsOk()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 2);
+ $bp->setNodeState('c;d', 1);
+ $bp->setNodeState('d;e', 0);
+
+ $this->assertEquals(
+ 'OK',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testUnknownOrWarningOrCriticalIsWarning()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 2);
+ $bp->setNodeState('c;d', 1);
+ $bp->setNodeState('d;e', 3);
+
+ $this->assertEquals(
+ 'WARNING',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testTwoTimesWarningAndOkIsOk()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 0);
+ $bp->setNodeState('c;d', 1);
+ $bp->setNodeState('d;e', 1);
+
+ $this->assertEquals(
+ 'OK',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ public function testThreeTimesWarningIsWarning()
+ {
+ $bp = $this->getBp();
+ $bp->setNodeState('b;c', 1);
+ $bp->setNodeState('c;d', 1);
+ $bp->setNodeState('d;e', 1);
+
+ $this->assertEquals(
+ 'WARNING',
+ $bp->getNode('a')->getStateName()
+ );
+ }
+
+ /**
+ * @return BpConfig
+ */
+ protected function getBp()
+ {
+ $storage = new LegacyStorage($this->emptyConfigSection());
+ $expression = 'a = b;c | c;d | d;e';
+ $bp = $storage->loadFromString('dummy', $expression);
+
+ return $bp;
+ }
+}
diff --git a/test/php/library/Businessprocess/ServiceNodeTest.php b/test/php/library/Businessprocess/ServiceNodeTest.php
new file mode 100644
index 0000000..62c1605
--- /dev/null
+++ b/test/php/library/Businessprocess/ServiceNodeTest.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Tests\Icinga\Module\Businessprocess;
+
+use Icinga\Module\Businessprocess\BpConfig;
+use Icinga\Module\Businessprocess\ServiceNode;
+use Icinga\Module\Businessprocess\Test\BaseTestCase;
+
+class ServiceNodeTest extends BaseTestCase
+{
+ public function testReturnsCorrectHostName()
+ {
+ $this->assertEquals(
+ 'localhost',
+ $this->pingOnLocalhost()->getHostname()
+ );
+ }
+
+ public function testReturnsCorrectServiceDescription()
+ {
+ $this->assertEquals(
+ 'ping <> pong',
+ $this->pingOnLocalhost()->getServiceDescription()
+ );
+ }
+
+ public function testReturnsCorrectAlias()
+ {
+ $this->assertEquals(
+ 'ping <> pong on localhost',
+ $this->pingOnLocalhost()->getAlias()
+ );
+ }
+
+ public function testRendersCorrectLink()
+ {
+ $this->assertEquals(
+ '<a href="/icingaweb2/businessprocess/service/show?host=localhost&amp;service=ping%20%3C%3E%20pong">'
+ . 'ping &lt;&gt; pong on localhost</a>',
+ $this->pingOnLocalhost()->getLink()->render()
+ );
+ }
+
+ /**
+ * @return ServiceNode
+ */
+ protected function pingOnLocalhost()
+ {
+ $bp = new BpConfig();
+ return (new ServiceNode((object) array(
+ 'hostname' => 'localhost',
+ 'service' => 'ping <> pong',
+ 'state' => 0,
+ )))->setBpConfig($bp)->setHostAlias('localhost')->setAlias('ping <> pong');
+ }
+}
diff --git a/test/php/library/Businessprocess/SimulationTest.php b/test/php/library/Businessprocess/SimulationTest.php
new file mode 100644
index 0000000..aefeb91
--- /dev/null
+++ b/test/php/library/Businessprocess/SimulationTest.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Tests\Icinga\Module\Businessprocess;
+
+use Icinga\Module\Businessprocess\Simulation;
+use Icinga\Module\Businessprocess\Test\BaseTestCase;
+
+class SimulationTest extends BaseTestCase
+{
+ public function testSimulationInstantiation()
+ {
+ $class = 'Icinga\\Module\\Businessprocess\\Simulation';
+ $this->assertInstanceOf(
+ $class,
+ Simulation::create()
+ );
+ }
+
+ public function testAppliedSimulation()
+ {
+ $data = (object) array(
+ 'state' => 0,
+ 'acknowledged' => false,
+ 'in_downtime' => false
+ );
+ $config = $this->makeInstance()->loadProcess('simple_with-header');
+ $simulation = Simulation::create(array(
+ 'host1;Hoststatus' => $data
+ ));
+ $parent = $config->getBpNode('singleHost');
+
+ $config->applySimulation($simulation);
+ $this->assertEquals(
+ 'OK',
+ $parent->getStateName()
+ );
+
+ $parent->clearState();
+ $data->state = 1;
+ $simulation->set('host1;Hoststatus', $data);
+ $config->applySimulation($simulation);
+ $this->assertEquals(
+ 'CRITICAL',
+ $parent->getStateName()
+ );
+ }
+}
diff --git a/test/php/library/Businessprocess/Storage/LegacyStorageTest.php b/test/php/library/Businessprocess/Storage/LegacyStorageTest.php
new file mode 100644
index 0000000..75bfcd5
--- /dev/null
+++ b/test/php/library/Businessprocess/Storage/LegacyStorageTest.php
@@ -0,0 +1,175 @@
+<?php
+
+namespace Tests\Icinga\Module\Businessprocess\Storage;
+
+use Icinga\Module\Businessprocess\BpNode;
+use Icinga\Module\Businessprocess\ImportedNode;
+use Icinga\Module\Businessprocess\Test\BaseTestCase;
+use Icinga\Module\Businessprocess\Storage\LegacyStorage;
+
+class LegacyStorageTest extends BaseTestCase
+{
+ private $processClass = 'Icinga\\Module\\Businessprocess\\BpConfig';
+
+ public function testCanBeInstantiatedWithAnEmptyConfigSection()
+ {
+ $baseClass = 'Icinga\\Module\\Businessprocess\\Storage\\LegacyStorage';
+ $this->assertInstanceOf(
+ $baseClass,
+ new LegacyStorage($this->emptyConfigSection())
+ );
+ }
+
+ public function testDefaultConfigDirIsDiscoveredCorrectly()
+ {
+ $this->assertEquals(
+ $this->getTestsBaseDir('config/modules/businessprocess/processes'),
+ $this->makeInstance()->getConfigDir()
+ );
+ }
+
+ public function testAllAvailableProcessesAreListed()
+ {
+ $keys = array_keys($this->makeInstance()->listProcesses());
+ $this->assertEquals(
+ array(
+ 'also-with-semicolons',
+ 'broken_wrong-operator',
+ 'combined',
+ 'simple_with-header',
+ 'simple_without-header',
+ 'with-semicolons'
+ ),
+ $keys
+ );
+ }
+
+ public function testHeaderTitlesAreRespectedInProcessList()
+ {
+ $keys = array_values($this->makeInstance()->listProcesses());
+ $this->assertEquals(
+ array(
+ 'Also With Semicolons (also-with-semicolons)',
+ 'broken_wrong-operator',
+ 'combined',
+ 'Simple with header (simple_with-header)',
+ 'simple_without-header',
+ 'With Semicolons (with-semicolons)'
+ ),
+ $keys
+ );
+ }
+
+ public function testProcessFilenameIsReturned()
+ {
+ $this->assertEquals(
+ $this->getTestsBaseDir('config/modules/businessprocess/processes/simple_with-header.conf'),
+ $this->makeInstance()->getFilename('simple_with-header')
+ );
+ }
+
+ public function testAnExistingProcessExists()
+ {
+ $this->assertTrue(
+ $this->makeInstance()->hasProcess('simple_with-header')
+ );
+ }
+
+ public function testAMissingProcessIsMissing()
+ {
+ $this->assertFalse(
+ $this->makeInstance()->hasProcess('simple_with-headerx')
+ );
+ }
+
+ public function testAValidProcessCanBeLoaded()
+ {
+ $this->assertInstanceOf(
+ $this->processClass,
+ $this->makeInstance()->loadProcess('simple_with-header')
+ );
+ }
+
+ public function testProcessConfigCanBeLoadedFromAString()
+ {
+ $this->assertInstanceOf(
+ $this->processClass,
+ $this->makeInstance()->loadFromString('dummy', 'a = Host1;ping & Host2;ping')
+ );
+ }
+
+ public function testFullProcessSourceCanBeFetched()
+ {
+ $this->assertEquals(
+ file_get_contents(
+ $this->getTestsBaseDir(
+ 'config/modules/businessprocess/processes/simple_with-header.conf'
+ )
+ ),
+ $this->makeInstance()->getSource('simple_with-header')
+ );
+ }
+
+ public function testTitleCanBeReadFromConfig()
+ {
+ $this->assertEquals(
+ 'Simple with header',
+ $this->makeInstance()->loadProcess('simple_with-header')->getMetadata()->get('Title')
+ );
+ }
+
+ public function testInfoUrlBeReadFromConfig()
+ {
+ $this->assertEquals(
+ 'https://top.example.com/',
+ $this->makeInstance()->loadProcess('simple_with-header')->getBpNode('top')->getInfoUrl()
+ );
+ }
+
+ public function testAConfiguredLoopCanBeParsed()
+ {
+ $this->assertInstanceOf(
+ $this->processClass,
+ $this->makeLoop()
+ );
+ }
+
+ public function testImportedNodesCanBeParsed()
+ {
+ $this->assertInstanceOf(
+ $this->processClass,
+ $this->makeInstance()->loadProcess('combined')
+ );
+ }
+
+ public function testConfigsWithNodesThatHaveSemicolonsInTheirNameCanBeParsed()
+ {
+ $bp = $this->makeInstance()->loadProcess('with-semicolons');
+
+ $this->assertInstanceOf($this->processClass, $bp);
+
+ $this->assertTrue($bp->hasNode('to\\;p'));
+ $this->assertSame(
+ 'https://top.example.com/',
+ $bp->getNode('to\\;p')->getInfoUrl()
+ );
+
+ $this->assertTrue($bp->hasNode('host\;1;Hoststatus'));
+ $this->assertSame('host;1', $bp->getNode('host\;1;Hoststatus')->getHostname());
+
+ $this->assertTrue($bp->hasNode('host\;1;pi;ng'));
+ $this->assertSame('host;1', $bp->getNode('host\;1;pi;ng')->getHostname());
+ $this->assertSame('pi;ng', $bp->getNode('host\;1;pi;ng')->getServiceDescription());
+
+ $this->assertTrue($bp->hasNode('singleHost'));
+ $this->assertTrue($bp->getNode('singleHost')->hasChild('to\\;p'));
+ $this->assertInstanceOf(BpNode::class, $bp->getNode('to\\;p'));
+
+ $this->assertInstanceOf(BpNode::class, $bp->getNode('no\\;alias'));
+ $this->assertSame('no;alias', $bp->getNode('no\\;alias')->getAlias());
+
+ $this->assertTrue($bp->hasNode('@also-with-semicolons:b\;ar'));
+ $this->assertTrue($bp->getNode('singleHost')->hasChild('@also-with-semicolons:b\;ar'));
+ $this->assertInstanceOf(ImportedNode::class, $bp->getNode('@also-with-semicolons:b\;ar'));
+ }
+}
diff --git a/test/php/library/Businessprocess/Web/Component/TabsTest.php b/test/php/library/Businessprocess/Web/Component/TabsTest.php
new file mode 100644
index 0000000..f1181d2
--- /dev/null
+++ b/test/php/library/Businessprocess/Web/Component/TabsTest.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Tests\Icinga\Module\Businessprocess\Web\Component;
+
+use Icinga\Module\Businessprocess\Web\Component\Tabs;
+use Icinga\Module\Businessprocess\Test\BaseTestCase;
+
+class TabsTest extends BaseTestCase
+{
+ public function testEmptyTabsCanBeInstantiated()
+ {
+ $this->assertInstanceOf(
+ 'Icinga\Module\Businessprocess\Web\Component\Tabs',
+ new Tabs()
+ );
+ }
+}
diff --git a/test/phpunit-compat.php b/test/phpunit-compat.php
new file mode 100644
index 0000000..2b1be3a
--- /dev/null
+++ b/test/phpunit-compat.php
@@ -0,0 +1,10 @@
+<?php
+
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @codingStandardsIgnoreStart
+ */
+class PHPUnit_Framework_TestCase extends TestCase
+{
+}
diff --git a/test/setup_vendor.sh b/test/setup_vendor.sh
new file mode 100755
index 0000000..7d47ff8
--- /dev/null
+++ b/test/setup_vendor.sh
@@ -0,0 +1,82 @@
+#!/bin/bash
+
+set -ex
+
+MODULE_HOME=${MODULE_HOME:="$(dirname "$(readlink -f $(dirname "$0"))")"}
+PHP_VERSION="$(php -r 'echo phpversion();')"
+
+# see also .travis.yml
+ICINGAWEB_VERSION=${ICINGAWEB_VERSION:=2.5.1}
+ICINGAWEB_GITREF=${ICINGAWEB_GITREF:=}
+
+IPL_VERSION=${IPL_VERSION:=0.1.1}
+
+PHPCS_VERSION=${PHPCS_VERSION:=2.9.1}
+
+if [ "$PHP_VERSION" '<' 5.6.0 ]; then
+ PHPUNIT_VERSION=${PHPUNIT_VERSION:=4.8}
+else
+ PHPUNIT_VERSION=${PHPUNIT_VERSION:=5.7}
+fi
+
+cd ${MODULE_HOME}
+
+test -d vendor || mkdir vendor
+cd vendor/
+
+# icingaweb2
+if [ -n "$ICINGAWEB_GITREF" ]; then
+ icingaweb_path="icingaweb2"
+ test ! -L "$icingaweb_path" || rm "$icingaweb_path"
+
+ if [ ! -d "$icingaweb_path" ]; then
+ git clone https://github.com/Icinga/icingaweb2.git "$icingaweb_path"
+ fi
+
+ (
+ set -e
+ cd "$icingaweb_path"
+ git fetch -p
+ git checkout -f "$ICINGAWEB_GITREF"
+ )
+else
+ icingaweb_path="icingaweb2-${ICINGAWEB_VERSION}"
+ if [ ! -e "${icingaweb_path}".tar.gz ]; then
+ wget -O "${icingaweb_path}".tar.gz https://github.com/Icinga/icingaweb2/archive/v"${ICINGAWEB_VERSION}".tar.gz
+ fi
+ if [ ! -d "${icingaweb_path}" ]; then
+ tar xf "${icingaweb_path}".tar.gz
+ fi
+
+ rm -f icingaweb2
+ ln -svf "${icingaweb_path}" icingaweb2
+fi
+ln -svf "${icingaweb_path}"/library/Icinga
+ln -svf "${icingaweb_path}"/library/vendor/Zend
+
+# ipl
+ipl_path="ipl"
+if [ ! -d "$ipl_path" ]; then
+ git clone https://github.com/Icinga/icingaweb2-module-ipl.git "$ipl_path"
+fi
+(
+ set -e
+ cd "$ipl_path"
+ git fetch -p
+ git checkout -f "stable/$IPL_VERSION"
+)
+
+# phpunit
+phpunit_path="phpunit-${PHPUNIT_VERSION}"
+if [ ! -e "${phpunit_path}".phar ]; then
+ wget -O "${phpunit_path}".phar https://phar.phpunit.de/phpunit-${PHPUNIT_VERSION}.phar
+fi
+ln -svf "${phpunit_path}".phar phpunit.phar
+
+# phpcs
+phpcs_path="phpcs-${PHPCS_VERSION}"
+if [ ! -e "${phpcs_path}".phar ]; then
+ wget -O "${phpcs_path}".phar \
+ https://github.com/squizlabs/PHP_CodeSniffer/releases/download/${PHPCS_VERSION}/phpcs.phar
+fi
+ln -svf "${phpcs_path}".phar phpcs.phar