diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-14 13:15:40 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-14 13:15:40 +0000 |
commit | b7fd908d538ed19fe41f03c0a3f93351d8da64e9 (patch) | |
tree | 46e14f318948cd4f5d7e874f83e7dfcc5d42fc64 | |
parent | Initial commit. (diff) | |
download | icingaweb2-module-businessprocess-upstream.tar.xz icingaweb2-module-businessprocess-upstream.zip |
Adding upstream version 2.5.0.upstream/2.5.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
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 @@ -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 Binary files differnew file mode 100644 index 0000000..2015a31 --- /dev/null +++ b/doc/screenshot/00_preview/0001_preview-tree-view.png diff --git a/doc/screenshot/00_preview/0002_preview_tile_view.png b/doc/screenshot/00_preview/0002_preview_tile_view.png Binary files differnew file mode 100644 index 0000000..08770ae --- /dev/null +++ b/doc/screenshot/00_preview/0002_preview_tile_view.png 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 Binary files differnew file mode 100644 index 0000000..e8db7ae --- /dev/null +++ b/doc/screenshot/00_preview/0004_preview_tile_and_subtree.png diff --git a/doc/screenshot/00_preview/0005_readme-preview.png b/doc/screenshot/00_preview/0005_readme-preview.png Binary files differnew file mode 100644 index 0000000..4515c86 --- /dev/null +++ b/doc/screenshot/00_preview/0005_readme-preview.png diff --git a/doc/screenshot/02_installation/101_menu-configuration-modules.png b/doc/screenshot/02_installation/101_menu-configuration-modules.png Binary files differnew file mode 100644 index 0000000..0c2d9df --- /dev/null +++ b/doc/screenshot/02_installation/101_menu-configuration-modules.png diff --git a/doc/screenshot/02_installation/102_enable-module.png b/doc/screenshot/02_installation/102_enable-module.png Binary files differnew file mode 100644 index 0000000..140d38d --- /dev/null +++ b/doc/screenshot/02_installation/102_enable-module.png diff --git a/doc/screenshot/03_getting-started/0201_empty-dashboard.png b/doc/screenshot/03_getting-started/0201_empty-dashboard.png Binary files differnew file mode 100644 index 0000000..b8fddcb --- /dev/null +++ b/doc/screenshot/03_getting-started/0201_empty-dashboard.png diff --git a/doc/screenshot/03_getting-started/0202_create-new-configuration.png b/doc/screenshot/03_getting-started/0202_create-new-configuration.png Binary files differnew file mode 100644 index 0000000..48e91fa --- /dev/null +++ b/doc/screenshot/03_getting-started/0202_create-new-configuration.png diff --git a/doc/screenshot/03_getting-started/0203_create-new_name.png b/doc/screenshot/03_getting-started/0203_create-new_name.png Binary files differnew file mode 100644 index 0000000..02845eb --- /dev/null +++ b/doc/screenshot/03_getting-started/0203_create-new_name.png diff --git a/doc/screenshot/03_getting-started/0204_create-new_title.png b/doc/screenshot/03_getting-started/0204_create-new_title.png Binary files differnew file mode 100644 index 0000000..99e26bb --- /dev/null +++ b/doc/screenshot/03_getting-started/0204_create-new_title.png diff --git a/doc/screenshot/03_getting-started/0205_create-new_description.png b/doc/screenshot/03_getting-started/0205_create-new_description.png Binary files differnew file mode 100644 index 0000000..454ec82 --- /dev/null +++ b/doc/screenshot/03_getting-started/0205_create-new_description.png diff --git a/doc/screenshot/03_getting-started/0206_create-new_backend.png b/doc/screenshot/03_getting-started/0206_create-new_backend.png Binary files differnew file mode 100644 index 0000000..31fcc83 --- /dev/null +++ b/doc/screenshot/03_getting-started/0206_create-new_backend.png 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 Binary files differnew file mode 100644 index 0000000..f875057 --- /dev/null +++ b/doc/screenshot/03_getting-started/0207_create-new_state-type.png 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 Binary files differnew file mode 100644 index 0000000..c868015 --- /dev/null +++ b/doc/screenshot/03_getting-started/0208_create-new_add-to-menu.png diff --git a/doc/screenshot/03_getting-started/0209_new-empty-configuration.png b/doc/screenshot/03_getting-started/0209_new-empty-configuration.png Binary files differnew file mode 100644 index 0000000..b1fbe86 --- /dev/null +++ b/doc/screenshot/03_getting-started/0209_new-empty-configuration.png diff --git a/doc/screenshot/03_getting-started/0210_new-on-dashboard.png b/doc/screenshot/03_getting-started/0210_new-on-dashboard.png Binary files differnew file mode 100644 index 0000000..6717d3d --- /dev/null +++ b/doc/screenshot/03_getting-started/0210_new-on-dashboard.png diff --git a/doc/screenshot/04_first-root-node/0301_empty-config.png b/doc/screenshot/04_first-root-node/0301_empty-config.png Binary files differnew file mode 100644 index 0000000..b1fbe86 --- /dev/null +++ b/doc/screenshot/04_first-root-node/0301_empty-config.png 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 Binary files differnew file mode 100644 index 0000000..f3eeb87 --- /dev/null +++ b/doc/screenshot/04_first-root-node/0302_add-new-node.png diff --git a/doc/screenshot/04_first-root-node/0303_node-title.png b/doc/screenshot/04_first-root-node/0303_node-title.png Binary files differnew file mode 100644 index 0000000..b6f9d12 --- /dev/null +++ b/doc/screenshot/04_first-root-node/0303_node-title.png diff --git a/doc/screenshot/04_first-root-node/0304_operator.png b/doc/screenshot/04_first-root-node/0304_operator.png Binary files differnew file mode 100644 index 0000000..1ffa1c2 --- /dev/null +++ b/doc/screenshot/04_first-root-node/0304_operator.png diff --git a/doc/screenshot/04_first-root-node/0305_display.png b/doc/screenshot/04_first-root-node/0305_display.png Binary files differnew file mode 100644 index 0000000..39e30b3 --- /dev/null +++ b/doc/screenshot/04_first-root-node/0305_display.png diff --git a/doc/screenshot/04_first-root-node/0306_info-url.png b/doc/screenshot/04_first-root-node/0306_info-url.png Binary files differnew file mode 100644 index 0000000..770102d --- /dev/null +++ b/doc/screenshot/04_first-root-node/0306_info-url.png 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 Binary files differnew file mode 100644 index 0000000..3fd5be6 --- /dev/null +++ b/doc/screenshot/04_first-root-node/0307_first-node-created.png diff --git a/doc/screenshot/05_importing_nodes/0401_subprocesses_only.png b/doc/screenshot/05_importing_nodes/0401_subprocesses_only.png Binary files differnew file mode 100644 index 0000000..d4f58ba --- /dev/null +++ b/doc/screenshot/05_importing_nodes/0401_subprocesses_only.png diff --git a/doc/screenshot/05_importing_nodes/0402_choose_existing_process.png b/doc/screenshot/05_importing_nodes/0402_choose_existing_process.png Binary files differnew file mode 100644 index 0000000..4fc5f63 --- /dev/null +++ b/doc/screenshot/05_importing_nodes/0402_choose_existing_process.png diff --git a/doc/screenshot/05_importing_nodes/0403_choose_configuration.png b/doc/screenshot/05_importing_nodes/0403_choose_configuration.png Binary files differnew file mode 100644 index 0000000..19b4c05 --- /dev/null +++ b/doc/screenshot/05_importing_nodes/0403_choose_configuration.png diff --git a/doc/screenshot/05_importing_nodes/0404_choose_process.png b/doc/screenshot/05_importing_nodes/0404_choose_process.png Binary files differnew file mode 100644 index 0000000..010992c --- /dev/null +++ b/doc/screenshot/05_importing_nodes/0404_choose_process.png diff --git a/doc/screenshot/05_importing_nodes/0405_import_successful.png b/doc/screenshot/05_importing_nodes/0405_import_successful.png Binary files differnew file mode 100644 index 0000000..9f4f346 --- /dev/null +++ b/doc/screenshot/05_importing_nodes/0405_import_successful.png diff --git a/doc/screenshot/05_importing_nodes/0406_breadcrumb_integration.png b/doc/screenshot/05_importing_nodes/0406_breadcrumb_integration.png Binary files differnew file mode 100644 index 0000000..0929546 --- /dev/null +++ b/doc/screenshot/05_importing_nodes/0406_breadcrumb_integration.png diff --git a/doc/screenshot/05_importing_nodes/0407_jump_to_original.png b/doc/screenshot/05_importing_nodes/0407_jump_to_original.png Binary files differnew file mode 100644 index 0000000..8fc0e2d --- /dev/null +++ b/doc/screenshot/05_importing_nodes/0407_jump_to_original.png 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 Binary files differnew file mode 100644 index 0000000..5697786 --- /dev/null +++ b/doc/screenshot/06_customize_node_order/0501_tiles_grab_tile.png 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 Binary files differnew file mode 100644 index 0000000..cd7b673 --- /dev/null +++ b/doc/screenshot/06_customize_node_order/0502_tiles_drop_at_location.png 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 Binary files differnew file mode 100644 index 0000000..7687713 --- /dev/null +++ b/doc/screenshot/06_customize_node_order/0503_tree_grab_header.png 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 Binary files differnew file mode 100644 index 0000000..828aebe --- /dev/null +++ b/doc/screenshot/06_customize_node_order/0504_tree_drop_at_location.png diff --git a/doc/screenshot/07_state_overrides/0701_override_config.png b/doc/screenshot/07_state_overrides/0701_override_config.png Binary files differnew file mode 100644 index 0000000..49ca2ad --- /dev/null +++ b/doc/screenshot/07_state_overrides/0701_override_config.png diff --git a/doc/screenshot/07_state_overrides/0702_overridden_tile.png b/doc/screenshot/07_state_overrides/0702_overridden_tile.png Binary files differnew file mode 100644 index 0000000..db2012e --- /dev/null +++ b/doc/screenshot/07_state_overrides/0702_overridden_tile.png diff --git a/doc/screenshot/07_state_overrides/0703_overridden_tree.png b/doc/screenshot/07_state_overrides/0703_overridden_tree.png Binary files differnew file mode 100644 index 0000000..ccf4fde --- /dev/null +++ b/doc/screenshot/07_state_overrides/0703_overridden_tree.png diff --git a/doc/screenshot/09_operators/0901_and-operator.png b/doc/screenshot/09_operators/0901_and-operator.png Binary files differnew file mode 100644 index 0000000..c6e7775 --- /dev/null +++ b/doc/screenshot/09_operators/0901_and-operator.png diff --git a/doc/screenshot/09_operators/0902_or-operator.png b/doc/screenshot/09_operators/0902_or-operator.png Binary files differnew file mode 100644 index 0000000..fd05ec3 --- /dev/null +++ b/doc/screenshot/09_operators/0902_or-operator.png diff --git a/doc/screenshot/09_operators/0903_or-operator-without-ok.png b/doc/screenshot/09_operators/0903_or-operator-without-ok.png Binary files differnew file mode 100644 index 0000000..e9fcd4e --- /dev/null +++ b/doc/screenshot/09_operators/0903_or-operator-without-ok.png diff --git a/doc/screenshot/09_operators/0904_min-operator.png b/doc/screenshot/09_operators/0904_min-operator.png Binary files differnew file mode 100644 index 0000000..fd05ec3 --- /dev/null +++ b/doc/screenshot/09_operators/0904_min-operator.png diff --git a/doc/screenshot/09_operators/0905_deg-operator.jpg b/doc/screenshot/09_operators/0905_deg-operator.jpg Binary files differnew file mode 100644 index 0000000..9dc05a3 --- /dev/null +++ b/doc/screenshot/09_operators/0905_deg-operator.jpg diff --git a/doc/screenshot/09_operators/0906_xor-operator.png b/doc/screenshot/09_operators/0906_xor-operator.png Binary files differnew file mode 100644 index 0000000..fd05ec3 --- /dev/null +++ b/doc/screenshot/09_operators/0906_xor-operator.png diff --git a/doc/screenshot/09_operators/0907_xor-operator-not-ok.png b/doc/screenshot/09_operators/0907_xor-operator-not-ok.png Binary files differnew file mode 100644 index 0000000..8ec41b3 --- /dev/null +++ b/doc/screenshot/09_operators/0907_xor-operator-not-ok.png diff --git a/doc/screenshot/12_web-components_breadcrumb/1201_simple-breadcrumb.png b/doc/screenshot/12_web-components_breadcrumb/1201_simple-breadcrumb.png Binary files differnew file mode 100644 index 0000000..a4f9a4d --- /dev/null +++ b/doc/screenshot/12_web-components_breadcrumb/1201_simple-breadcrumb.png 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 Binary files differnew file mode 100644 index 0000000..fe3406b --- /dev/null +++ b/doc/screenshot/12_web-components_breadcrumb/1202_return-from-fullscreen.png 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 Binary files differnew file mode 100644 index 0000000..b1daec5 --- /dev/null +++ b/doc/screenshot/12_web-components_breadcrumb/1203_add-to-dashboard.png diff --git a/doc/screenshot/12_web-components_breadcrumb/1204_unlocked_config.png b/doc/screenshot/12_web-components_breadcrumb/1204_unlocked_config.png Binary files differnew file mode 100644 index 0000000..0ba84a9 --- /dev/null +++ b/doc/screenshot/12_web-components_breadcrumb/1204_unlocked_config.png 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 Binary files differnew file mode 100644 index 0000000..80a142f --- /dev/null +++ b/doc/screenshot/13_web-components-tile-renderer/1301_tile-view.png 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 Binary files differnew file mode 100644 index 0000000..a03209d --- /dev/null +++ b/doc/screenshot/13_web-components-tile-renderer/1302_tile-and-subtree.png 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 Binary files differnew file mode 100644 index 0000000..7bacb00 --- /dev/null +++ b/doc/screenshot/14_web-components-tree-renderer/1401_tree-view.png diff --git a/doc/screenshot/16_dashboard/1601_add-to-dashboard-link.png b/doc/screenshot/16_dashboard/1601_add-to-dashboard-link.png Binary files differnew file mode 100644 index 0000000..b1daec5 --- /dev/null +++ b/doc/screenshot/16_dashboard/1601_add-to-dashboard-link.png diff --git a/doc/screenshot/16_dashboard/1602_add_to_dashboard-form.png b/doc/screenshot/16_dashboard/1602_add_to_dashboard-form.png Binary files differnew file mode 100644 index 0000000..ab2d255 --- /dev/null +++ b/doc/screenshot/16_dashboard/1602_add_to_dashboard-form.png diff --git a/doc/screenshot/16_dashboard/1603_businessprocesses_on_dashboard.png b/doc/screenshot/16_dashboard/1603_businessprocesses_on_dashboard.png Binary files differnew file mode 100644 index 0000000..116bf04 --- /dev/null +++ b/doc/screenshot/16_dashboard/1603_businessprocesses_on_dashboard.png diff --git a/doc/screenshot/21_store-config/2101_Pending-Changes.png b/doc/screenshot/21_store-config/2101_Pending-Changes.png Binary files differnew file mode 100644 index 0000000..7e33981 --- /dev/null +++ b/doc/screenshot/21_store-config/2101_Pending-Changes.png diff --git a/doc/screenshot/21_store-config/2102_Store-Config.png b/doc/screenshot/21_store-config/2102_Store-Config.png Binary files differnew file mode 100644 index 0000000..d985786 --- /dev/null +++ b/doc/screenshot/21_store-config/2102_Store-Config.png diff --git a/doc/screenshot/21_store-config/2103_Show-Diff.png b/doc/screenshot/21_store-config/2103_Show-Diff.png Binary files differnew file mode 100644 index 0000000..7025f6c --- /dev/null +++ b/doc/screenshot/21_store-config/2103_Show-Diff.png diff --git a/doc/screenshot/22_upload-config/2201_go-to-upload.png b/doc/screenshot/22_upload-config/2201_go-to-upload.png Binary files differnew file mode 100644 index 0000000..1dda67c --- /dev/null +++ b/doc/screenshot/22_upload-config/2201_go-to-upload.png diff --git a/doc/screenshot/22_upload-config/2202_choose-file.png b/doc/screenshot/22_upload-config/2202_choose-file.png Binary files differnew file mode 100644 index 0000000..a8717dd --- /dev/null +++ b/doc/screenshot/22_upload-config/2202_choose-file.png diff --git a/doc/screenshot/22_upload-config/2203_syntax-error.png b/doc/screenshot/22_upload-config/2203_syntax-error.png Binary files differnew file mode 100644 index 0000000..ca81da0 --- /dev/null +++ b/doc/screenshot/22_upload-config/2203_syntax-error.png diff --git a/doc/screenshot/22_upload-config/2204_duplicate-name.png b/doc/screenshot/22_upload-config/2204_duplicate-name.png Binary files differnew file mode 100644 index 0000000..5f9c809 --- /dev/null +++ b/doc/screenshot/22_upload-config/2204_duplicate-name.png diff --git a/doc/screenshot/81_history/8101_bpaddon-overview.png b/doc/screenshot/81_history/8101_bpaddon-overview.png Binary files differnew file mode 100644 index 0000000..6ae8cb0 --- /dev/null +++ b/doc/screenshot/81_history/8101_bpaddon-overview.png diff --git a/doc/screenshot/81_history/8102_bpaddon-detail.png b/doc/screenshot/81_history/8102_bpaddon-detail.png Binary files differnew file mode 100644 index 0000000..7b38a1e --- /dev/null +++ b/doc/screenshot/81_history/8102_bpaddon-detail.png 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 ' '; + } + + 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(' ')->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 Binary files differnew file mode 100644 index 0000000..cda95a8 --- /dev/null +++ b/public/img/ack.gif diff --git a/public/img/downtime.gif b/public/img/downtime.gif Binary files differnew file mode 100644 index 0000000..1687798 --- /dev/null +++ b/public/img/downtime.gif diff --git a/public/img/help.gif b/public/img/help.gif Binary files differnew file mode 100644 index 0000000..9226497 --- /dev/null +++ b/public/img/help.gif diff --git a/public/img/icon_collapse.png b/public/img/icon_collapse.png Binary files differnew file mode 100644 index 0000000..0c7f37b --- /dev/null +++ b/public/img/icon_collapse.png diff --git a/public/img/icon_expand.png b/public/img/icon_expand.png Binary files differnew file mode 100644 index 0000000..19862cf --- /dev/null +++ b/public/img/icon_expand.png 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 @@ -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&service=ping%20%3C%3E%20pong">' + . 'ping <> 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 |