summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.mailmap6
-rw-r--r--.travis.yml28
-rw-r--r--AUTHORS4
-rw-r--r--CHANGELOG.md61
-rw-r--r--COPYING339
-rw-r--r--README.md59
-rw-r--r--RELEASE.md63
-rw-r--r--application/controllers/CommentsController.php17
-rw-r--r--application/controllers/ConfigController.php45
-rw-r--r--application/controllers/EventController.php216
-rw-r--r--application/controllers/EventsController.php186
-rw-r--r--application/controllers/IndexController.php14
-rw-r--r--application/forms/Config/BackendConfigForm.php112
-rw-r--r--application/forms/Config/GlobalConfigForm.php32
-rw-r--r--application/forms/Config/MonitoringConfigForm.php72
-rw-r--r--application/forms/Event/EventCommentForm.php144
-rw-r--r--application/forms/Events/AckFilterForm.php80
-rw-r--r--application/forms/Events/SeverityFilterForm.php224
-rw-r--r--application/locale/de_DE/LC_MESSAGES/eventdb.mobin0 -> 6301 bytes
-rw-r--r--application/locale/de_DE/LC_MESSAGES/eventdb.po413
-rw-r--r--application/views/helpers/Column.php51
-rw-r--r--application/views/helpers/ColumnHeader.php17
-rw-r--r--application/views/helpers/Event.php12
-rw-r--r--application/views/helpers/EventMessage.php70
-rw-r--r--application/views/scripts/config/index.phtml21
-rw-r--r--application/views/scripts/config/monitoring.phtml7
-rw-r--r--application/views/scripts/event/index-plain.phtml62
-rw-r--r--application/views/scripts/event/index.phtml174
-rw-r--r--application/views/scripts/events/details-plain.phtml18
-rw-r--r--application/views/scripts/events/details.phtml50
-rw-r--r--application/views/scripts/events/index-plain.phtml19
-rw-r--r--application/views/scripts/events/index.phtml93
-rw-r--r--application/views/scripts/format/text.phtml13
-rw-r--r--configuration.php46
-rw-r--r--doc/02-Configuration.md53
-rw-r--r--doc/03-CustomVars.md50
-rw-r--r--doc/09-Security.md26
-rw-r--r--doc/10-Screenshots.md24
-rw-r--r--doc/screenshots/configuration-backend.pngbin0 -> 14171 bytes
-rw-r--r--doc/screenshots/configuration-monitoring.pngbin0 -> 20968 bytes
-rw-r--r--doc/screenshots/monitoring-actions.pngbin0 -> 26487 bytes
-rw-r--r--doc/screenshots/monitoring-detailview.pngbin0 -> 70118 bytes
-rw-r--r--doc/screenshots/overview-filtered.pngbin0 -> 122651 bytes
-rw-r--r--doc/screenshots/overview-with-details.pngbin0 -> 166770 bytes
-rw-r--r--doc/screenshots/overview.pngbin0 -> 149458 bytes
-rw-r--r--examples/legacy-filter/column-integration.json50
-rw-r--r--examples/legacy-filter/examples.md9
-rw-r--r--examples/legacy-filter/full-filter.json77
-rw-r--r--library/Eventdb/Data/LegacyFilterParser.php153
-rw-r--r--library/Eventdb/Event.php120
-rw-r--r--library/Eventdb/Eventdb.php184
-rw-r--r--library/Eventdb/EventdbController.php181
-rw-r--r--library/Eventdb/Hook/DetailviewExtensionHook.php124
-rw-r--r--library/Eventdb/ProvidedHook/Monitoring/DetailviewExtension.php81
-rw-r--r--library/Eventdb/ProvidedHook/Monitoring/EventdbActionHook.php182
-rw-r--r--library/Eventdb/ProvidedHook/Monitoring/HostActions.php15
-rw-r--r--library/Eventdb/ProvidedHook/Monitoring/ServiceActions.php15
-rw-r--r--library/Eventdb/Test/BaseTestCase.php27
-rw-r--r--library/Eventdb/Test/Bootstrap.php35
-rw-r--r--library/Eventdb/Test/PseudoHost.php15
-rw-r--r--library/Eventdb/Test/PseudoMonitoringBackend.php14
-rw-r--r--library/Eventdb/Test/PseudoService.php16
-rw-r--r--library/Eventdb/Web/EventdbOutputFormat.php66
-rw-r--r--module.info8
-rw-r--r--phpunit.xml18
-rw-r--r--public/css/module.less285
-rw-r--r--public/js/module.js97
-rw-r--r--run.php9
-rw-r--r--test/bootstrap.php16
-rw-r--r--test/config/authentication.ini0
-rw-r--r--test/config/config.ini0
-rw-r--r--test/config/resources.ini0
-rw-r--r--test/php/library/Eventdb/Data/LegacyFilterParserTest.php77
-rw-r--r--test/php/library/Eventdb/ProvidedHook/Monitoring/EventdbActionHookTest.php186
-rw-r--r--test/phpunit-compat.php10
-rwxr-xr-xtest/setup_vendor.sh69
76 files changed, 5060 insertions, 0 deletions
diff --git a/.mailmap b/.mailmap
new file mode 100644
index 0000000..eedb019
--- /dev/null
+++ b/.mailmap
@@ -0,0 +1,6 @@
+<markus.frosch@icinga.com> <markus.frosch@netways.de>
+<markus.frosch@icinga.com> <markus@lazyfrosch.de>
+<michael.friedrich@icinga.com> <michael.friedrich@netways.de>
+Eric Lippmann <eric.lippmann@icinga.com> <eric.lippmann@netways.de>
+Eric Lippmann <eric.lippmann@icinga.com> <lippserd@googlemail.com>
+<johannes.meyer@icinga.com> <johannes.meyer@netways.de>
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..e0b0b51
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,28 @@
+language: php
+php:
+ - '5.4'
+ - '5.5'
+ - '5.6'
+ - '7.0'
+ - '7.1'
+ - nightly
+
+cache:
+ directories:
+ - vendor
+
+matrix:
+ fast_finish: true
+ allow_failures:
+ - php: nightly
+
+branches:
+ only:
+ - master
+ - /^v\d/
+
+before_script:
+- ./test/setup_vendor.sh
+
+script:
+ - php vendor/phpunit.phar
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..263d5c2
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,4 @@
+Alexander A. Klimov <alexander.klimov@icinga.com>
+Eric Lippmann <eric.lippmann@icinga.com>
+Markus Frosch <markus.frosch@icinga.com>
+Michael Friedrich <michael.friedrich@icinga.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..0302b1f
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,61 @@
+# Change Log
+
+## [v1.3.0](https://github.com/Icinga/icingaweb2-module-eventdb/tree/v1.3.0) (2018-01-25)
+[Full Changelog](https://github.com/Icinga/icingaweb2-module-eventdb/compare/v1.2.0...v1.3.0)
+
+**Implemented enhancements:**
+
+- Links in Event text [\#24](https://github.com/Icinga/icingaweb2-module-eventdb/issues/24)
+- Severity Widget for fast clicking users [\#23](https://github.com/Icinga/icingaweb2-module-eventdb/issues/23)
+- Add default\_filter config option for the menu url [\#27](https://github.com/Icinga/icingaweb2-module-eventdb/pull/27) ([lazyfrosch](https://github.com/lazyfrosch))
+- Add DetailviewExtensionHook for detail and multi-select [\#26](https://github.com/Icinga/icingaweb2-module-eventdb/pull/26) ([lazyfrosch](https://github.com/lazyfrosch))
+
+**Fixed bugs:**
+
+- Detailview extension shown despite customvar not present [\#25](https://github.com/Icinga/icingaweb2-module-eventdb/issues/25)
+- Pagination issue [\#22](https://github.com/Icinga/icingaweb2-module-eventdb/issues/22)
+
+## [v1.2.0](https://github.com/Icinga/icingaweb2-module-eventdb/tree/v1.2.0) (2017-08-17)
+[Full Changelog](https://github.com/Icinga/icingaweb2-module-eventdb/compare/v1.1.0...v1.2.0)
+
+**Implemented enhancements:**
+
+- Add more output / format options [\#21](https://github.com/Icinga/icingaweb2-module-eventdb/pull/21) ([lazyfrosch](https://github.com/lazyfrosch))
+
+## [v1.1.0](https://github.com/Icinga/icingaweb2-module-eventdb/tree/v1.1.0) (2017-08-16)
+[Full Changelog](https://github.com/Icinga/icingaweb2-module-eventdb/compare/v1.0.0...v1.1.0)
+
+**Implemented enhancements:**
+
+- Add screenshot to README.md [\#1](https://github.com/Icinga/icingaweb2-module-eventdb/issues/1)
+- Support EDBC extensions [\#15](https://github.com/Icinga/icingaweb2-module-eventdb/issues/15)
+- Event list should be auto-reloading [\#13](https://github.com/Icinga/icingaweb2-module-eventdb/issues/13)
+- Add integration to host and service [\#12](https://github.com/Icinga/icingaweb2-module-eventdb/issues/12)
+- Add German translation [\#9](https://github.com/Icinga/icingaweb2-module-eventdb/issues/9)
+- Popup Tool Tips for Action Buttons in Interface missing [\#7](https://github.com/Icinga/icingaweb2-module-eventdb/issues/7)
+- Fuzzy link to hostnames [\#5](https://github.com/Icinga/icingaweb2-module-eventdb/issues/5)
+- Add EDBC extensions [\#20](https://github.com/Icinga/icingaweb2-module-eventdb/pull/20) ([lazyfrosch](https://github.com/lazyfrosch))
+- Add detailview extension [\#19](https://github.com/Icinga/icingaweb2-module-eventdb/pull/19) ([lazyfrosch](https://github.com/lazyfrosch))
+- Integration into monitoring [\#16](https://github.com/Icinga/icingaweb2-module-eventdb/pull/16) ([lazyfrosch](https://github.com/lazyfrosch))
+- Improve list and detail UI [\#14](https://github.com/Icinga/icingaweb2-module-eventdb/pull/14) ([lazyfrosch](https://github.com/lazyfrosch))
+- Improve usability of priority/severity widget [\#11](https://github.com/Icinga/icingaweb2-module-eventdb/pull/11) ([lazyfrosch](https://github.com/lazyfrosch))
+- DB repository: add convert rules for event.host\_address [\#6](https://github.com/Icinga/icingaweb2-module-eventdb/pull/6) ([Al2Klimov](https://github.com/Al2Klimov))
+
+**Fixed bugs:**
+
+- Acknowledgement only available in multi selection [\#8](https://github.com/Icinga/icingaweb2-module-eventdb/issues/8)
+
+**Closed issues:**
+
+- Configuration for extra types [\#18](https://github.com/Icinga/icingaweb2-module-eventdb/issues/18)
+- Add Host Column to Overview [\#2](https://github.com/Icinga/icingaweb2-module-eventdb/issues/2)
+- The content of 'Host Address' is not readable [\#4](https://github.com/Icinga/icingaweb2-module-eventdb/issues/4)
+
+**Merged pull requests:**
+
+- Add basic phpunit environment [\#17](https://github.com/Icinga/icingaweb2-module-eventdb/pull/17) ([lazyfrosch](https://github.com/lazyfrosch))
+- Add GitHub issue template [\#10](https://github.com/Icinga/icingaweb2-module-eventdb/pull/10) ([dnsmichi](https://github.com/dnsmichi))
+
+
+
+\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* \ No newline at end of file
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..ecbc059
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. \ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..4edb9e1
--- /dev/null
+++ b/README.md
@@ -0,0 +1,59 @@
+# EventDB Module for Icinga Web 2
+
+[![Build Status](https://travis-ci.org/Icinga/icingaweb2-module-eventdb.svg?branch=master)](https://travis-ci.org/Icinga/icingaweb2-module-eventdb)
+[![Github Tag](https://img.shields.io/github/tag/Icinga/icingaweb2-module-eventdb.svg)](https://github.com/Icinga/icingaweb2-module-eventdb)
+
+![Icinga Logo](https://www.icinga.com/wp-content/uploads/2014/06/icinga_logo.png)
+
+* [About](#about)
+* [Requirements](#requirements)
+* [Getting Started](#getting-started)
+* [Documentation](#documentation)
+* [License](#license)
+
+## About
+
+With the EventDB Module you can browse, comment and acknowledge events collected
+by [EventDB](https://git.netways.org/eventdb/eventdb) easily in
+[Icinga Web 2](https://www.icinga.org/products/icinga-web-2/).
+
+![screenshot](doc/screenshots/overview.png)
+
+Also see [Screenshots](doc/10-Screenshots.md) in documentation.
+
+## Requirements
+
+* Icinga Web 2
+* A database with events collected by EventDB
+
+## Getting started
+
+Install and enable the module, then go to [Configuration](doc/02-Configuration.md)
+to set up the essentials.
+
+## Documentation
+
+* [Configuration](doc/02-Configuration.md)
+* [Custom variables](doc/03-CustomVars.md)
+* [Security](doc/09-Security.md)
+* [Screenshots](doc/10-Screenshots.md)
+
+## License
+
+ Copyright (C) 2016-2017 Icinga Development Team
+ 2016-2017 Eric Lippmann <eric.lippmann@icinga.com>
+ 2017 Markus Frosch <markus.frosch@icinga.com>
+
+ 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.
diff --git a/RELEASE.md b/RELEASE.md
new file mode 100644
index 0000000..57cd2ea
--- /dev/null
+++ b/RELEASE.md
@@ -0,0 +1,63 @@
+# Release Workflow
+
+Specify the release version.
+
+```
+VERSION=1.3.0
+```
+
+## Issues
+
+Check issues at https://github.com/Icinga/icingaweb2-module-eventdb
+
+## Authors
+
+Update the [.mailmap](.mailmap) and [AUTHORS](AUTHORS) files:
+
+```
+git checkout master
+git log --use-mailmap | grep ^Author: | cut -f2- -d' ' | sort | uniq > AUTHORS
+```
+
+## Update metadata
+
+Edit and update [module.info](module.info).
+
+## Changelog
+
+Update the [CHANGELOG.md](CHANGELOG.md) file.
+
+Uses [github_changelog_generator](https://github.com/skywinder/github-changelog-generator)
+
+```
+export CHANGELOG_GITHUB_TOKEN=xxx
+github_changelog_generator --future-release v$VERSION
+```
+
+Check if the file has been updated correctly.
+
+## Git Tag
+
+Commit these changes to the "master" branch:
+
+```
+git commit -v -a -m "Release version $VERSION"
+git push origin master
+```
+
+And tag it with a signed tag:
+
+```
+git tag -s -m "Version $VERSION" v$VERSION
+```
+
+Push the tag.
+
+```
+git push --tags
+```
+
+## GitHub Release
+
+Create a new release for the newly created Git tag.
+https://github.com/Icinga/icingaweb2-module-eventdb/releases
diff --git a/application/controllers/CommentsController.php b/application/controllers/CommentsController.php
new file mode 100644
index 0000000..a371c7b
--- /dev/null
+++ b/application/controllers/CommentsController.php
@@ -0,0 +1,17 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Eventdb\Controllers;
+
+use Icinga\Module\Eventdb\EventdbController;
+
+class CommentsController extends EventdbController
+{
+ /**
+ * @deprecated Moved to eventdb/events/details
+ */
+ public function newAction()
+ {
+ $this->redirectNow($this->getRequest()->getUrl()->setPath('eventdb/events/details'));
+ }
+}
diff --git a/application/controllers/ConfigController.php b/application/controllers/ConfigController.php
new file mode 100644
index 0000000..d812918
--- /dev/null
+++ b/application/controllers/ConfigController.php
@@ -0,0 +1,45 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Eventdb\Controllers;
+
+use Icinga\Module\Eventdb\Forms\Config\BackendConfigForm;
+use Icinga\Module\Eventdb\Forms\Config\GlobalConfigForm;
+use Icinga\Module\Eventdb\Forms\Config\MonitoringConfigForm;
+use Icinga\Web\Controller;
+
+class ConfigController extends Controller
+{
+ public function init()
+ {
+ $this->assertPermission('config/modules');
+ parent::init();
+ }
+
+ public function indexAction()
+ {
+ $backendConfig = new BackendConfigForm();
+ $backendConfig
+ ->setIniConfig($this->Config())
+ ->handleRequest();
+ $this->view->backendConfig = $backendConfig;
+
+ $globalConfig = new GlobalConfigForm();
+ $globalConfig
+ ->setIniConfig($this->Config())
+ ->handleRequest();
+ $this->view->globalConfig = $globalConfig;
+
+ $this->view->tabs = $this->Module()->getConfigTabs()->activate('config');
+ }
+
+ public function monitoringAction()
+ {
+ $monitoringConfig = new MonitoringConfigForm();
+ $monitoringConfig
+ ->setIniConfig($this->Config())
+ ->handleRequest();
+ $this->view->form = $monitoringConfig;
+ $this->view->tabs = $this->Module()->getConfigTabs()->activate('monitoring');
+ }
+}
diff --git a/application/controllers/EventController.php b/application/controllers/EventController.php
new file mode 100644
index 0000000..f6a323c
--- /dev/null
+++ b/application/controllers/EventController.php
@@ -0,0 +1,216 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Eventdb\Controllers;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Exception\NotFoundError;
+use Icinga\Module\Eventdb\Event;
+use Icinga\Module\Eventdb\EventdbController;
+use Icinga\Module\Eventdb\Forms\Event\EventCommentForm;
+use Icinga\Module\Eventdb\Hook\DetailviewExtensionHook;
+use Icinga\Module\Eventdb\Web\EventdbOutputFormat;
+use Icinga\Web\Hook;
+use Icinga\Web\Url;
+
+class EventController extends EventdbController
+{
+ public function indexAction()
+ {
+ $eventId = $this->params->getRequired('id');
+
+ $url = Url::fromRequest();
+
+ $this->getTabs()->add('event', array(
+ 'active' => ! $this->isFormatRequest(),
+ 'title' => $this->translate('Event'),
+ 'url' => $url->without(array('format'))
+ ))->extend(new EventdbOutputFormat(array(), array(EventdbOutputFormat::TYPE_TEXT)));
+
+ $columnConfig = $this->Config('columns');
+ if (! $columnConfig->isEmpty()) {
+ $additionalColumns = $columnConfig->keys();
+ } else {
+ $additionalColumns = array();
+ }
+
+ $event = $this->getDb()
+ ->select()
+ ->from('event');
+
+ $columns = array_merge($event->getColumns(), $additionalColumns);
+
+ $event->from('event', $columns);
+ $event->where('id', $eventId);
+
+ $event->applyFilter(Filter::matchAny(array_map(
+ '\Icinga\Data\Filter\Filter::fromQueryString',
+ $this->getRestrictions('eventdb/events/filter', 'eventdb/events')
+ )));
+
+ $eventData = $event->fetchRow();
+ if (! $eventData) {
+ throw new NotFoundError('Could not find event with id %d', $eventId);
+ }
+
+ $eventObj = Event::fromData($eventData);
+
+ $groupedEvents = null;
+ if ($this->getDb()->hasCorrelatorExtensions()) {
+ $group_leader = (int) $eventObj->group_leader;
+ if ($group_leader > 0) {
+ // redirect to group leader
+ $this->redirectNow(Url::fromPath('eventdb/event', array('id' => $group_leader)));
+ }
+
+ if ($group_leader === -1) {
+ // load grouped events, if any
+ $groupedEvents = $this->getDb()
+ ->select()
+ ->from('event')
+ ->where('group_leader', $eventObj->id)
+ ->order('ack', 'ASC')
+ ->order('created', 'DESC');
+ }
+ }
+
+ $comments = null;
+ $commentForm = null;
+ if ($this->hasPermission('eventdb/comments')) {
+ $comments = $this->getDb()
+ ->select()
+ ->from('comment', array(
+ 'id',
+ 'type',
+ 'message',
+ 'created',
+ 'modified',
+ 'user'
+ ))
+ ->where('event_id', $eventId)
+ ->order('created', 'DESC');
+
+ if ($this->hasPermission('eventdb/interact')) {
+ $commentForm = new EventCommentForm();
+ $commentForm
+ ->setDb($this->getDb())
+ ->setFilter(Filter::expression('id', '=', $eventId));
+ }
+ }
+
+ $format = $this->params->get('format');
+ if ($format === 'sql') {
+ $this->sendSqlSummary(array($event, $comments, $groupedEvents));
+ } elseif ($this->isApiRequest()) {
+ $data = new \stdClass;
+ $data->event = $eventData;
+ if ($comments !== null) {
+ $data->comments = $comments;
+ }
+ if ($groupedEvents !== null) {
+ $data->groupedEvents = $groupedEvents;
+ }
+ $this->sendJson($data);
+ } elseif ($this->isTextRequest()) {
+ $this->view->event = $eventObj;
+ $this->view->columnConfig = $columnConfig;
+ $this->view->additionalColumns = $additionalColumns;
+ $this->view->groupedEvents = $groupedEvents;
+ $this->view->comments = $comments;
+
+ $this->sendText(null, 'event/index-plain');
+ } else {
+ if ($commentForm !== null) {
+ $commentForm->handleRequest();
+ }
+
+ $this->view->event = $eventObj;
+ $this->view->columnConfig = $columnConfig;
+ $this->view->additionalColumns = $additionalColumns;
+ $this->view->groupedEvents = $groupedEvents;
+ $this->view->comments = $comments;
+ $this->view->commentForm = $commentForm;
+
+ $this->view->extensionsHtml = array();
+ foreach (Hook::all('Eventdb\DetailviewExtension') as $hook) {
+ /** @var DetailviewExtensionHook $hook */
+ $module = $this->view->escape($hook->getModule()->getName());
+ $this->view->extensionsHtml[] =
+ '<div class="icinga-module module-' . $module . '" data-icinga-module="' . $module . '">'
+ . $hook->setView($this->view)->getHtmlForEvent($eventObj)
+ . '</div>';
+ }
+ }
+ }
+
+ /**
+ * @deprecated redirects to index view now
+ */
+ public function commentsAction()
+ {
+ $this->redirectNow(
+ Url::fromPath(
+ 'eventdb/event',
+ array('id' => $this->params->getRequired('id'))
+ )
+ );
+ }
+
+ /**
+ * Action allowing you to be forwarded to host in Icinga monitoring
+ *
+ * **But** case insensitive!
+ */
+ public function hostAction()
+ {
+ $host = $this->params->getRequired('host');
+
+ $backend = $this->monitoringBackend();
+
+ $query = $backend->select()
+ ->from('hoststatus', array('host_name'))
+ ->where('host', $host);
+
+ $realHostname = $query->fetchOne();
+
+ if ($realHostname !== null && $realHostname !== false) {
+ $this->redirectNow(Url::fromPath('monitoring/host/services', array('host' => $realHostname)));
+ } else {
+ throw new NotFoundError('Could not find a hostname matching: %s', $host);
+ }
+ }
+
+ /**
+ * Action allowing you to be forwarded to host in Icinga monitoring
+ *
+ * **But** case insensitive!
+ */
+ public function serviceAction()
+ {
+ $host = $this->params->getRequired('host');
+ $service = $this->params->getRequired('service');
+
+ $backend = $this->monitoringBackend();
+
+ $query = $backend->select()
+ ->from('servicestatus', array('host_name', 'service'))
+ ->where('host', $host)
+ ->where('service', $service);
+
+ $realService = $query->fetchRow();
+
+ if ($realService !== null && $realService !== false) {
+ $this->redirectNow(
+ Url::fromPath(
+ 'monitoring/service/show',
+ array(
+ 'host' => $realService->host_name,
+ 'service' => $realService->service
+ )
+ )
+ );
+ } else {
+ throw new NotFoundError('Could not find a service "%s" for host "%s"', $service, $host);
+ }
+ }
+}
diff --git a/application/controllers/EventsController.php b/application/controllers/EventsController.php
new file mode 100644
index 0000000..09e5a8d
--- /dev/null
+++ b/application/controllers/EventsController.php
@@ -0,0 +1,186 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Eventdb\Controllers;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Eventdb\EventdbController;
+use Icinga\Module\Eventdb\Forms\Event\EventCommentForm;
+use Icinga\Module\Eventdb\Forms\Events\AckFilterForm;
+use Icinga\Module\Eventdb\Forms\Events\SeverityFilterForm;
+use Icinga\Module\Eventdb\Hook\DetailviewExtensionHook;
+use Icinga\Module\Eventdb\Web\EventdbOutputFormat;
+use Icinga\Util\StringHelper;
+use Icinga\Web\Hook;
+use Icinga\Web\Url;
+
+class EventsController extends EventdbController
+{
+ public function init()
+ {
+ parent::init();
+ $this->view->title = 'EventDB: ' . $this->translate('Events');
+ }
+
+ public function indexAction()
+ {
+ $this->assertPermission('eventdb/events');
+
+ $this->getTabs()->add('events', array(
+ 'active' => ! $this->isFormatRequest(),
+ 'title' => $this->translate('Events'),
+ 'url' => Url::fromRequest()->without(array('format'))
+ ))->extend(new EventdbOutputFormat(array(), array(EventdbOutputFormat::TYPE_TEXT)));
+
+ $columnConfig = $this->Config('columns');
+ if ($this->params->has('columns')) {
+ $additionalColumns = StringHelper::trimSplit($this->params->get('columns'));
+ } elseif (! $columnConfig->isEmpty()) {
+ $additionalColumns = $columnConfig->keys();
+ } else {
+ $additionalColumns = array();
+ }
+
+ $events = $this->getDb()->select()
+ ->from('event');
+
+ $columns = array_merge($events->getColumns(), $additionalColumns);
+ $events->columns($columns);
+
+ $events->applyFilter(Filter::matchAny(array_map(
+ '\Icinga\Data\Filter\Filter::fromQueryString',
+ $this->getRestrictions('eventdb/events/filter', 'eventdb/events')
+ )));
+
+ $this->getDb()->filterGroups($events);
+
+ $this->setupPaginationControl($events);
+
+ $this->setupFilterControl(
+ $events,
+ array(
+ 'host_name' => $this->translate('Host'),
+ 'host_address' => $this->translate('Host Address'),
+ 'type' => $this->translate('Type'),
+ 'program' => $this->translate('Program'),
+ 'facility' => $this->translate('Facility'),
+ 'priority' => $this->translate('Priority'),
+ 'message' => $this->translate('Message'),
+ 'ack' => $this->translate('Acknowledged'),
+ 'created' => $this->translate('Created')
+ ),
+ array('host_name'),
+ array('columns', 'format')
+ );
+
+ $this->setupLimitControl();
+
+ $this->setupSortControl(
+ array(
+ 'host_name' => $this->translate('Host'),
+ 'host_address' => $this->translate('Host Address'),
+ 'type' => $this->translate('Type'),
+ 'program' => $this->translate('Program'),
+ 'facility' => $this->translate('Facility'),
+ 'priority' => $this->translate('Priority'),
+ 'message' => $this->translate('Message'),
+ 'ack' => $this->translate('Acknowledged'),
+ 'created' => $this->translate('Created')
+ ),
+ $events,
+ array('created' => 'desc')
+ );
+
+ if ($this->view->compact) {
+ $events->peekAhead();
+ }
+
+ if ($this->params->get('format') === 'sql') {
+ $this->sendSqlSummary($events);
+ } elseif ($this->isApiRequest()) {
+ $data = new \stdClass;
+ $data->events = $events->fetchAll();
+ $this->sendJson($data);
+ exit;
+ } elseif ($this->isTextRequest()) {
+ $this->view->columnConfig = $this->Config('columns');
+ $this->view->additionalColumns = $additionalColumns;
+ $this->view->events = $events;
+
+ $this->sendText(null, 'events/index-plain');
+ } else {
+ $this->setAutorefreshInterval(15);
+
+ $severityFilterForm = new SeverityFilterForm();
+ $severityFilterForm->handleRequest();
+
+ $ackFilterForm = new AckFilterForm();
+ $ackFilterForm->handleRequest();
+
+ $this->view->ackFilterForm = $ackFilterForm;
+ $this->view->columnConfig = $this->Config('columns');
+ $this->view->additionalColumns = $additionalColumns;
+ $this->view->events = $events;
+ $this->view->severityFilterForm = $severityFilterForm;
+ }
+ }
+
+ public function detailsAction()
+ {
+ $this->assertPermission('eventdb/events');
+
+ $url = Url::fromRequest()->without(array('format'));
+
+ $this->getTabs()->add('events', array(
+ 'active' => ! $this->isFormatRequest(),
+ 'title' => $this->translate('Events'),
+ 'url' => $url
+ ))->extend(new EventdbOutputFormat(array(), array(EventdbOutputFormat::TYPE_TEXT)));
+
+ $events = $this->getDb()
+ ->select()
+ ->from('event');
+
+ $this->getDb()->filterGroups($events);
+
+ $filter = Filter::fromQueryString($url->getQueryString());
+ $events->applyFilter($filter);
+
+ $events->applyFilter(Filter::matchAny(array_map(
+ '\Icinga\Data\Filter\Filter::fromQueryString',
+ $this->getRestrictions('eventdb/events/filter', 'eventdb/events')
+ )));
+
+ if ($this->isApiRequest()) {
+ $this->sendJson($events->fetchAll());
+ } elseif ($this->isTextRequest()) {
+ $this->view->events = $events->fetchAll();
+ $this->view->columnConfig = $this->Config('columns');
+
+ $this->sendText(null, 'events/details-plain');
+ } else {
+ $commentForm = null;
+ if ($this->hasPermission('eventdb/interact')) {
+ $commentForm = new EventCommentForm();
+ $commentForm
+ ->setDb($this->getDb())
+ ->setFilter($filter)
+ ->handleRequest();
+ $this->view->commentForm = $commentForm;
+ }
+
+ $this->view->events = $events->fetchAll();
+ $this->view->columnConfig = $this->Config('columns');
+
+ $this->view->extensionsHtml = array();
+ foreach (Hook::all('Eventdb\DetailviewExtension') as $hook) {
+ /** @var DetailviewExtensionHook $hook */
+ $module = $this->view->escape($hook->getModule()->getName());
+ $this->view->extensionsHtml[] =
+ '<div class="icinga-module module-' . $module . '" data-icinga-module="' . $module . '">'
+ . $hook->setView($this->view)->getHtmlForEvents($this->view->events)
+ . '</div>';
+ }
+ }
+ }
+}
diff --git a/application/controllers/IndexController.php b/application/controllers/IndexController.php
new file mode 100644
index 0000000..5322936
--- /dev/null
+++ b/application/controllers/IndexController.php
@@ -0,0 +1,14 @@
+<?php
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Eventdb\Controllers;
+
+use Icinga\Web\Controller;
+
+class IndexController extends Controller
+{
+ public function indexAction()
+ {
+ $this->redirectNow('eventdb/events');
+ }
+}
diff --git a/application/forms/Config/BackendConfigForm.php b/application/forms/Config/BackendConfigForm.php
new file mode 100644
index 0000000..a9c85c1
--- /dev/null
+++ b/application/forms/Config/BackendConfigForm.php
@@ -0,0 +1,112 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Eventdb\Forms\Config;
+
+use Exception;
+use Icinga\Data\ResourceFactory;
+use Icinga\Forms\ConfigForm;
+
+/**
+ * Form for managing the connection to the EventDB backend
+ */
+class BackendConfigForm extends ConfigForm
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setSubmitLabel($this->translate('Save'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $resources = array();
+ foreach (ResourceFactory::getResourceConfigs() as $name => $config) {
+ if ($config->type === 'db') {
+ $resources[] = $name;
+ }
+ }
+
+ $this->addElement(
+ 'select',
+ 'backend_resource',
+ array(
+ 'description' => $this->translate('The resource to use'),
+ 'label' => $this->translate('Resource'),
+ 'multiOptions' => array_combine($resources, $resources),
+ 'required' => true
+ )
+ );
+
+ if (isset($formData['skip_validation']) && $formData['skip_validation']) {
+ $this->addSkipValidationCheckbox();
+ }
+ }
+
+ /**
+ * Return whether the given values are valid
+ *
+ * @param array $formData The data to validate
+ *
+ * @return bool
+ */
+ public function isValid($formData)
+ {
+ if (! parent::isValid($formData)) {
+ return false;
+ }
+
+ if (($el = $this->getElement('skip_validation')) === null || ! $el->isChecked()) {
+ $resourceConfig = ResourceFactory::getResourceConfig($this->getValue('backend_resource'));
+
+ if (! $this->isValidEventDbSchema($resourceConfig)) {
+ if ($el === null) {
+ $this->addSkipValidationCheckbox();
+ }
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public function isValidEventDbSchema($resourceConfig)
+ {
+ try {
+ $db = ResourceFactory::createResource($resourceConfig);
+ $db->select()->from('event', array('id'))->fetchOne();
+ } catch (Exception $_) {
+ $this->error($this->translate(
+ 'Cannot find the EventDB schema. Please verify that the given database '
+ . 'contains the schema and that the configured user has access to it.'
+ ));
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Add a checkbox to the form by which the user can skip the schema validation
+ */
+ protected function addSkipValidationCheckbox()
+ {
+ $this->addElement(
+ 'checkbox',
+ 'skip_validation',
+ array(
+ 'description' => $this->translate(
+ 'Check this to not to validate the EventDB schema of the chosen resource'
+ ),
+ 'ignore' => true,
+ 'label' => $this->translate('Skip Validation'),
+ 'order' => 0
+ )
+ );
+ }
+}
diff --git a/application/forms/Config/GlobalConfigForm.php b/application/forms/Config/GlobalConfigForm.php
new file mode 100644
index 0000000..5b4bf2a
--- /dev/null
+++ b/application/forms/Config/GlobalConfigForm.php
@@ -0,0 +1,32 @@
+<?php
+/* Icinga Web 2 - EventDB | (c) 2018 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Eventdb\Forms\Config;
+
+use Icinga\Forms\ConfigForm;
+
+class GlobalConfigForm extends ConfigForm
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setSubmitLabel($this->translate('Save'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'global_default_filter',
+ array(
+ 'description' => $this->translate('Filter to be used by the menu link for EventDB by default'),
+ 'label' => $this->translate('Default Filter')
+ )
+ );
+ }
+}
diff --git a/application/forms/Config/MonitoringConfigForm.php b/application/forms/Config/MonitoringConfigForm.php
new file mode 100644
index 0000000..e1c7638
--- /dev/null
+++ b/application/forms/Config/MonitoringConfigForm.php
@@ -0,0 +1,72 @@
+<?php
+/* Icinga Web 2 - EventDB | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Eventdb\Forms\Config;
+
+use Icinga\Forms\ConfigForm;
+
+/**
+ * Form for managing settings for the integration into monitoring module
+ */
+class MonitoringConfigForm extends ConfigForm
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setSubmitLabel($this->translate('Save'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'monitoring_custom_var',
+ array(
+ 'description' => $this->translate('Name of the custom variable to enable EventDB integration for (usually "edb")'),
+ 'label' => $this->translate('Custom Variable')
+ )
+ );
+
+ $this->addElement(
+ 'checkbox',
+ 'monitoring_always_on_host',
+ array(
+ 'description' => $this->translate('Always enable the integration on hosts, even when the custom variable is not set'),
+ 'label' => $this->translate('Always enable for hosts')
+ )
+ );
+
+ $this->addElement(
+ 'checkbox',
+ 'monitoring_always_on_service',
+ array(
+ 'description' => $this->translate('Always enable the integration on services, even when the custom variable is not set'),
+ 'label' => $this->translate('Always enable for services')
+ )
+ );
+
+ $this->addElement(
+ 'checkbox',
+ 'monitoring_detailview_disable',
+ array(
+ 'description' => $this->translate('Disable the detail view inside the monitoring module'),
+ 'label' => $this->translate('Disable the detail view')
+ )
+ );
+
+ $this->addElement(
+ 'text',
+ 'monitoring_detailview_filter',
+ array(
+ 'description' => $this->translate('Filter events in the detail view area inside the monitoring module'),
+ 'label' => $this->translate('Filter the detail view'),
+ 'value' => 'ack=0', // Also see DetailviewExtension
+ )
+ );
+ }
+}
diff --git a/application/forms/Event/EventCommentForm.php b/application/forms/Event/EventCommentForm.php
new file mode 100644
index 0000000..130910c
--- /dev/null
+++ b/application/forms/Event/EventCommentForm.php
@@ -0,0 +1,144 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Eventdb\Forms\Event;
+
+use Exception;
+use Icinga\Data\Filter\Filter;
+use Icinga\Module\Eventdb\Eventdb;
+use Icinga\Web\Form;
+
+/**
+ * Form for managing the connection to the EventDB backend
+ */
+class EventCommentForm extends Form
+{
+ /**
+ * @var Eventdb
+ */
+ protected $db;
+
+ protected $filter;
+
+ protected static $types = array(
+ 'comment',
+ 'ack',
+ 'revoke'
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ public static $defaultElementDecorators = array(
+ array('ViewHelper', array('separator' => '')),
+ array('Errors', array('separator' => '')),
+ );
+
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setSubmitLabel($this->translate('Submit'));
+ }
+
+ public function setDb(Eventdb $db)
+ {
+ $this->db = $db;
+ return $this;
+ }
+
+ public function setFilter(Filter $filter)
+ {
+ $this->filter = $filter;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'select',
+ 'type',
+ array(
+ 'label' => $this->translate('Type'),
+ 'multiOptions' => array(
+ 0 => $this->translate('Comment'),
+ 1 => $this->translate('Acknowledge'),
+ 2 => $this->translate('Revoke')
+ ),
+ 'required' => true,
+ 'value' => 1,
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'comment',
+ array(
+ 'label' => $this->translate('Comment'),
+ 'required' => true
+ )
+ );
+ }
+
+ public function onSuccess()
+ {
+ $type = $this->getValue('type');
+ $comment = $this->getValue('comment');
+ $username = $this->Auth()->getUser()->getUsername();
+
+ $events = $this->db->select()->from('event', array('id'))->applyFilter($this->filter);
+
+ $dbAdapter = $this->db->getDataSource()->getDbAdapter();
+
+ $dbAdapter->beginTransaction();
+ try {
+ foreach ($events as $event) {
+ $this->db->insert('comment', array(
+ 'event_id' => $event->id,
+ 'type' => $type,
+ 'message' => $comment,
+ 'created' => date(Eventdb::DATETIME_FORMAT),
+ 'modified' => date(Eventdb::DATETIME_FORMAT),
+ 'user' => $username
+ ));
+
+ if ($type !== '0') {
+ $ackFilter = Filter::expression('id', '=', $event->id);
+ if ($this->db->hasCorrelatorExtensions()) {
+ $ackFilter = Filter::matchAny($ackFilter, Filter::where('group_leader', $event->id));
+ }
+ $this->db->update('event', array(
+ 'ack' => $type === '1' ? 1 : 0
+ ), $ackFilter);
+ }
+ }
+ $dbAdapter->commit();
+ return true;
+ } catch (Exception $e) {
+ $dbAdapter->rollback();
+ $this->error($e->getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function loadDefaultDecorators()
+ {
+ parent::loadDefaultDecorators();
+
+ $this->removeDecorator('FormHints');
+ }
+
+ public function addSubmitButton()
+ {
+ parent::addSubmitButton();
+
+ $btn = $this->getElement('btn_submit');
+ $btn->removeDecorator('HtmlTag');
+ }
+}
diff --git a/application/forms/Events/AckFilterForm.php b/application/forms/Events/AckFilterForm.php
new file mode 100644
index 0000000..829c6ce
--- /dev/null
+++ b/application/forms/Events/AckFilterForm.php
@@ -0,0 +1,80 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Eventdb\Forms\Events;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Web\Form;
+
+class AckFilterForm extends Form
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setAttrib('class', 'inline ack-filter-form');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addSubmitButton()
+ {
+ if ((bool) $this->getRequest()->getUrl()->getParams()->get('ack', true)) {
+ $icon = 'ok';
+ $title = $this->translate('Hide acknowledged events');
+ } else {
+ $icon = 'cancel';
+ $title = $this->translate('Show also acknowledged events');
+ }
+
+ $this->addElements(array(
+ array(
+ 'button',
+ 'btn_submit',
+ array(
+ 'class' => 'link-button spinner',
+ 'decorators' => array(
+ 'ViewHelper',
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ ),
+ 'escape' => false,
+ 'ignore' => true,
+ 'label' => $this->getView()->icon($icon) . $this->translate('Ack'),
+ 'type' => 'submit',
+ 'title' => $title,
+ 'value' => $this->translate('Ack')
+ )
+ )
+ ));
+
+ return $this;
+ }
+
+ public function onSuccess()
+ {
+ $redirect = clone $this->getRequest()->getUrl();
+ $params = $redirect->getParams();
+ $modifyFilter = $params->shift('modifyFilter');
+ $columns = $params->shift('columns');
+ if (! (bool) $this->getRequest()->getUrl()->getParams()->get('ack', true)) {
+ $params->remove('ack');
+ } else {
+ $redirect->setQueryString(
+ Filter::fromQueryString($redirect->getQueryString())
+ ->andFilter(Filter::expression('ack', '=', 0))
+ ->toQueryString()
+ );
+ }
+ $params = $redirect->getParams();
+ if ($modifyFilter) {
+ $params->add('modifyFilter');
+ }
+ if ($columns) {
+ $params->add('columns', $columns);
+ }
+ $this->setRedirectUrl($redirect);
+ return true;
+ }
+}
diff --git a/application/forms/Events/SeverityFilterForm.php b/application/forms/Events/SeverityFilterForm.php
new file mode 100644
index 0000000..bd1ec5e
--- /dev/null
+++ b/application/forms/Events/SeverityFilterForm.php
@@ -0,0 +1,224 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Eventdb\Forms\Events;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterAnd;
+use Icinga\Data\Filter\FilterChain;
+use Icinga\Data\Filter\FilterExpression;
+use Icinga\Data\Filter\FilterMatch;
+use Icinga\Module\Eventdb\Event;
+use Icinga\Web\Form;
+
+class SeverityFilterForm extends Form
+{
+ protected $includedPriorities;
+ protected $excludedPriorities;
+
+ /**
+ * @var FilterChain
+ */
+ protected $filter;
+
+ /**
+ * @var array
+ */
+ protected $filterEditorParams = array('modifyFilter', 'addFilter');
+
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setAttrib('class', 'inline severity-filter-form');
+ }
+
+ protected function findPriorities(Filter $filter, $sign, &$target)
+ {
+ if ($filter->isEmpty()) {
+ return;
+ }
+
+ if ($filter->isChain()) {
+ /** @var FilterChain $filter */
+ foreach ($filter->filters() as $part) {
+ /** @var Filter $part */
+ if (! $part->isEmpty() && $part->isExpression()) {
+ /** @var FilterMatch $part */
+ if (strtolower($part->getColumn()) === 'priority' && $part->getSign() === $sign) {
+ $expression = $part->getExpression();
+ if (is_array($expression)) {
+ foreach ($expression as $priority) {
+ $target[(int) $priority] = $part;
+ }
+ } else {
+ $target[(int) $expression] = $part;
+ }
+ }
+ } else {
+ /** @var FilterChain $part */
+ foreach ($part->filters() as $or) {
+ /** @var FilterExpression $or */
+ if (strtolower($or->getColumn()) === 'priority' && $or->getSign() === $sign) {
+ $expression = $or->getExpression();
+ if (is_array($expression)) {
+ foreach ($expression as $priority) {
+ $target[(int) $priority] = $or;
+ }
+ } else {
+ $target[(int) $expression] = $or;
+ }
+ }
+ }
+ }
+ }
+ } else {
+ /** @var FilterMatch $filter */
+ if (strtolower($filter->getColumn()) === 'priority' && $filter->getSign() === $sign) {
+ $expression = $filter->getExpression();
+ if (is_array($expression)) {
+ foreach ($expression as $priority) {
+ $target[(int) $priority] = $filter;
+ }
+ } else {
+ $target[(int) $expression] = $filter;
+ }
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $includedPriorities = array();
+ $excludedPriorities = array();
+ $params = $this->getRequest()->getUrl()->getParams()
+ ->without($this->filterEditorParams)
+ ->without('columns')
+ ->without('page');
+
+ $filter = Filter::fromQueryString((string) $params);
+
+ $this->findPriorities($filter, '=', $includedPriorities);
+ $this->findPriorities($filter, '!=', $excludedPriorities);
+
+ foreach (Event::$priorities as $id => $priority) {
+ $class = $priority;
+ if (
+ (empty($includedPriorities) or isset($includedPriorities[$id]))
+ && ! isset($excludedPriorities[$id])
+ ) {
+ $class .= ' active';
+ $title = $this->translate('Filter out %s');
+ } else {
+ $title = $this->translate('Filter in %s');
+ }
+
+ $label = ucfirst(substr($priority, 0, 1));
+ if ($id === 3) {
+ $label .= substr($priority, 1, 1);
+ }
+ $this->addElement(
+ 'submit',
+ $priority,
+ array(
+ 'class' => $class,
+ 'label' => $label,
+ 'title' => sprintf($title, ucfirst($priority)),
+ )
+ );
+ }
+
+ $this->includedPriorities = $includedPriorities;
+ $this->excludedPriorities = $excludedPriorities;
+
+ $this->filter = $filter;
+ }
+
+ public function onSuccess()
+ {
+ $postData = $this->getRequest()->getPost();
+ unset($postData['formUID']);
+ unset($postData['CSRFToken']);
+ reset($postData);
+ $priority = Event::getPriorityId(key($postData));
+ $redirect = clone $this->getRequest()->getUrl();
+
+ // convert inclusion to exclusion
+ if (! empty($this->includedPriorities)) {
+ if (empty($this->excludedPriorities)) {
+ $this->excludedPriorities = array();
+ }
+ // set exclusion with for all not included values
+ foreach (array_keys(Event::$priorities) as $id) {
+ if (! isset($this->includedPriorities[$id])
+ && ! isset($this->excludedPriorities[$id])
+ ) {
+ $this->excludedPriorities[$id] = true;
+ }
+ }
+ // purge from inclusions from filter
+ if ($this->filter instanceof FilterChain) {
+ foreach ($this->includedPriorities as $filter) {
+ if ($filter instanceof Filter) {
+ /** @var Filter $filter */
+ $this->filter = $this->filter->removeId($filter->getId());
+ }
+ }
+ }
+ }
+
+ if ($this->filter instanceof FilterChain) {
+ // purge existing exclusions from a complex filter
+ foreach ($this->excludedPriorities as $filter) {
+ if ($filter instanceof Filter) {
+ /** @var Filter $filter */
+ $this->filter = $this->filter->removeId($filter->getId());
+ }
+ }
+ } elseif (! empty($this->excludedPriorities)) {
+ // empty the filter - because it only was a simple exclusion
+ $this->filter = new FilterAnd;
+ }
+
+ // toggle exclusion
+ if (isset($this->excludedPriorities[$priority])) {
+ // in exclusion: just remove
+ unset($this->excludedPriorities[$priority]);
+ } else {
+ // not set: add to exclusion
+ $this->excludedPriorities[$priority] = true;
+ }
+
+ $priorityFilter = Filter::matchAll();
+ foreach (array_keys($this->excludedPriorities) as $id) {
+ $priorityFilter->andFilter(Filter::expression('priority', '!=', $id));
+ }
+
+ if ($this->filter->isEmpty()) {
+ // set the Filter
+ $this->filter = $priorityFilter;
+ } else {
+ // append our filter to the rest of the existing Filter
+ $this->filter = $this->filter->andFilter($priorityFilter);
+ }
+
+ $redirect->setQueryString($this->filter->toQueryString());
+
+ $requestParams = $this->getRequest()->getUrl()->getParams();
+ $redirectParams = $redirect->getParams();
+ foreach ($this->filterEditorParams as $filterEditorParam) {
+ if ($requestParams->has($filterEditorParam)) {
+ $redirectParams->add($filterEditorParam);
+ }
+ }
+ if ($requestParams->has('columns')) {
+ $redirectParams->add('columns', $this->getRequest()->getUrl()->getParam('columns'));
+ }
+ $this->setRedirectUrl($redirect);
+ return true;
+ }
+}
diff --git a/application/locale/de_DE/LC_MESSAGES/eventdb.mo b/application/locale/de_DE/LC_MESSAGES/eventdb.mo
new file mode 100644
index 0000000..0bcec3c
--- /dev/null
+++ b/application/locale/de_DE/LC_MESSAGES/eventdb.mo
Binary files differ
diff --git a/application/locale/de_DE/LC_MESSAGES/eventdb.po b/application/locale/de_DE/LC_MESSAGES/eventdb.po
new file mode 100644
index 0000000..29b432a
--- /dev/null
+++ b/application/locale/de_DE/LC_MESSAGES/eventdb.po
@@ -0,0 +1,413 @@
+# Icinga Web 2 - Head for multiple monitoring backends.
+# Copyright (C) 2018 Icinga Development Team
+# This file is distributed under the same license as Eventdb Module.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Eventdb Module (1.1.0)\n"
+"Report-Msgid-Bugs-To: dev@icinga.com\n"
+"POT-Creation-Date: 2018-01-25 14:31+0000\n"
+"PO-Revision-Date: 2018-01-25 15:32+0100\n"
+"Last-Translator: Markus Frosch <markus.frosch@icinga.com>\n"
+"Language: de_DE\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Poedit-Basepath: .\n"
+"Language-Team: \n"
+"X-Generator: Poedit 2.0.5\n"
+"X-Poedit-SearchPath-0: .\n"
+
+#: ../../../../modules/eventdb/application/views/scripts/events/index.phtml:15
+#, php-format
+msgctxt "Multi-selection count"
+msgid "%s row(s) selected"
+msgstr "%s Zeilen ausgewählt"
+
+#: ../../../../modules/eventdb/application/forms/Events/AckFilterForm.php:44
+#: ../../../../modules/eventdb/application/forms/Events/AckFilterForm.php:47
+msgid "Ack"
+msgstr "Bestätigt"
+
+#: ../../../../modules/eventdb/application/forms/Event/EventCommentForm.php:69
+msgid "Acknowledge"
+msgstr "Bestätigen"
+
+#: ../../../../modules/eventdb/application/views/helpers/Column.php:23
+#: ../../../../modules/eventdb/application/views/scripts/events/index-plain.phtml:10
+#: ../../../../modules/eventdb/application/views/scripts/events/details-plain.phtml:9
+#: ../../../../modules/eventdb/application/views/scripts/events/details.phtml:24
+#: ../../../../modules/eventdb/application/views/scripts/events/index.phtml:72
+#: ../../../../modules/eventdb/application/views/scripts/event/index-plain.phtml:18
+#: ../../../../modules/eventdb/application/views/scripts/event/index-plain.phtml:51
+#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:27
+#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:161
+#: ../../../../modules/eventdb/application/controllers/EventsController.php:69
+#: ../../../../modules/eventdb/application/controllers/EventsController.php:87
+msgid "Acknowledged"
+msgstr "Bestätigt"
+
+#: ../../../../modules/eventdb/application/views/scripts/event/index-plain.phtml:9
+#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:9
+msgid "Acknowledgement"
+msgstr "Bestätigung"
+
+#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:60
+msgid "Actions"
+msgstr "Aktionen"
+
+#: ../../../../modules/eventdb/application/views/scripts/events/details.phtml:41
+#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:118
+msgid "Add comment / acknowledge"
+msgstr "Kommentar / Bestätigung hinzufügen"
+
+#: ../../../../modules/eventdb/library/Eventdb/ProvidedHook/Monitoring/EventdbActionHook.php:151
+msgid "All events for host"
+msgstr "Alle Ereignisse für Host"
+
+#: ../../../../modules/eventdb/configuration.php:40
+msgid "Allow to acknowledge and comment events"
+msgstr "Erlauben Ereignisse zu Bestätigen und zu Kommentieren"
+
+#: ../../../../modules/eventdb/configuration.php:35
+msgid "Allow to view comments"
+msgstr "Erlauben Kommentare zu sehen"
+
+#: ../../../../modules/eventdb/configuration.php:30
+msgid "Allow to view events"
+msgstr "Erlauben Ereignisse zu sehen"
+
+#: ../../../../modules/eventdb/application/forms/Config/MonitoringConfigForm.php:40
+msgid "Always enable for hosts"
+msgstr "Immer für Hosts aktivieren"
+
+#: ../../../../modules/eventdb/application/forms/Config/MonitoringConfigForm.php:49
+msgid "Always enable for services"
+msgstr "Immer für Services aktivieren"
+
+#: ../../../../modules/eventdb/application/forms/Config/MonitoringConfigForm.php:39
+msgid ""
+"Always enable the integration on hosts, even when the custom variable is not "
+"set"
+msgstr ""
+"Immer bei Interaktionen mit Hosts anzeigen, auch wenn die angepasste "
+"Variable nicht gesetzt ist"
+
+#: ../../../../modules/eventdb/application/forms/Config/MonitoringConfigForm.php:48
+msgid ""
+"Always enable the integration on services, even when the custom variable is "
+"not set"
+msgstr ""
+"Immer bei Interaktionen mit Services anzeigen, auch wenn die angepasste "
+"Variable nicht gesetzt ist"
+
+#: ../../../../modules/eventdb/application/views/scripts/events/details.phtml:44
+msgid "At least one event is set to auto-clear."
+msgstr "Mindestens ein Ereignis ist für automatische Bestätigung gesetzt."
+
+#: ../../../../modules/eventdb/application/views/scripts/events/details.phtml:25
+#: ../../../../modules/eventdb/application/views/scripts/events/index.phtml:73
+#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:28
+#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:162
+msgid "Auto-Clear"
+msgstr "Automatische Bestätigung"
+
+#: ../../../../modules/eventdb/application/forms/Config/BackendConfigForm.php:86
+msgid ""
+"Cannot find the EventDB schema. Please verify that the given database "
+"contains the schema and that the configured user has access to it."
+msgstr ""
+"Kann das EventDB-Schema nicht finden. Bitte stellen Sie sicher, dass die "
+"angegebene Datenbankverbindung das Schema enthält und dass der konfigurierte "
+"Benutzer Zugriff darauf hat."
+
+#: ../../../../modules/eventdb/application/forms/Config/BackendConfigForm.php:104
+msgid "Check this to not to validate the EventDB schema of the chosen resource"
+msgstr ""
+"Häkchen setzen um das EventDB Schema der gewählten Ressource nicht zu "
+"überprüfen."
+
+#: ../../../../modules/eventdb/application/views/scripts/event/index-plain.phtml:8
+#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:8
+#: ../../../../modules/eventdb/application/forms/Event/EventCommentForm.php:68
+#: ../../../../modules/eventdb/application/forms/Event/EventCommentForm.php:80
+msgid "Comment"
+msgstr "Kommentar"
+
+#: ../../../../modules/eventdb/application/views/scripts/event/index-plain.phtml:28
+#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:128
+msgid "Comments"
+msgstr "Kommentare"
+
+#: ../../../../modules/eventdb/configuration.php:19
+msgid "Config"
+msgstr "Konfiguration"
+
+#: ../../../../modules/eventdb/configuration.php:18
+#, fuzzy
+msgid "Configure EventDB"
+msgstr "EventDB Datenbankverbindung einrichten"
+
+#: ../../../../modules/eventdb/configuration.php:23
+#: ../../../../modules/eventdb/application/views/scripts/config/monitoring.phtml:5
+msgid "Configure integration into the monitoring module"
+msgstr "Interaktionen im Monitoring Modul"
+
+#: ../../../../modules/eventdb/application/views/scripts/config/index.phtml:19
+msgid "Configure module"
+msgstr "Modul konfigurieren"
+
+#: ../../../../modules/eventdb/application/views/scripts/config/index.phtml:7
+msgid "Create a New Resource"
+msgstr "Neue Ressource erstellen"
+
+#: ../../../../modules/eventdb/application/views/scripts/config/index.phtml:14
+msgid "Create a new resource"
+msgstr "Neue Ressource erstellen"
+
+#: ../../../../modules/eventdb/application/controllers/EventsController.php:70
+#: ../../../../modules/eventdb/application/controllers/EventsController.php:88
+msgid "Created"
+msgstr "Erstellt"
+
+#: ../../../../modules/eventdb/application/forms/Config/MonitoringConfigForm.php:31
+msgid "Custom Variable"
+msgstr "Angepasste Variable"
+
+#: ../../../../modules/eventdb/application/views/scripts/config/index.phtml:5
+#, fuzzy
+msgid "Database backend"
+msgstr "EventDB Datenbankverbindung einrichten"
+
+#: ../../../../modules/eventdb/application/forms/Config/GlobalConfigForm.php:28
+msgid "Default Filter"
+msgstr "Standard Filter"
+
+#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:36
+msgid "Details"
+msgstr "Details"
+
+#: ../../../../modules/eventdb/application/forms/Config/MonitoringConfigForm.php:58
+msgid "Disable the detail view"
+msgstr "Detailansicht deaktivieren"
+
+#: ../../../../modules/eventdb/application/forms/Config/MonitoringConfigForm.php:57
+msgid "Disable the detail view inside the monitoring module"
+msgstr "Die Detailansicht innerhalb des Monitoring Moduls deaktivieren"
+
+#: ../../../../modules/eventdb/application/controllers/EventController.php:26
+msgid "Event"
+msgstr "Ereignis"
+
+#: ../../../../modules/eventdb/application/controllers/EventsController.php:22
+#: ../../../../modules/eventdb/application/controllers/EventsController.php:31
+#: ../../../../modules/eventdb/application/controllers/EventsController.php:136
+msgid "Events"
+msgstr "Ereignisse"
+
+# Spezifische Syslog Bezeichnung
+#: ../../../../modules/eventdb/application/controllers/EventsController.php:66
+#: ../../../../modules/eventdb/application/controllers/EventsController.php:84
+msgid "Facility"
+msgstr "Facility"
+
+#: ../../../../modules/eventdb/application/forms/Config/MonitoringConfigForm.php:66
+msgid "Filter events in the detail view area inside the monitoring module"
+msgstr "Ereignisse der Detailansicht im Monitoring Modul filtern"
+
+#: ../../../../modules/eventdb/application/forms/Events/SeverityFilterForm.php:117
+#, php-format
+msgid "Filter in %s"
+msgstr "Filtern nach %s"
+
+#: ../../../../modules/eventdb/application/forms/Events/SeverityFilterForm.php:115
+#, php-format
+msgid "Filter out %s"
+msgstr "Filtern ohne %s"
+
+#: ../../../../modules/eventdb/application/forms/Config/MonitoringConfigForm.php:67
+msgid "Filter the detail view"
+msgstr "Detailansicht filtern"
+
+#: ../../../../modules/eventdb/application/forms/Config/GlobalConfigForm.php:27
+msgid "Filter to be used by the menu link for EventDB by default"
+msgstr "Filter für die URL des Menü Eintrages der EventDB"
+
+#: ../../../../modules/eventdb/library/Eventdb/ProvidedHook/Monitoring/EventdbActionHook.php:134
+msgid "Filtered events"
+msgstr "Gefilterte Ereignisse"
+
+#: ../../../../modules/eventdb/application/views/scripts/event/index-plain.phtml:42
+#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:147
+msgid "Grouped Events"
+msgstr "Gruppierte Ereignisse"
+
+#: ../../../../modules/eventdb/application/forms/Events/AckFilterForm.php:26
+msgid "Hide acknowledged events"
+msgstr "Bestätigte Ereignisse verstecken"
+
+#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:65
+#: ../../../../modules/eventdb/application/controllers/EventsController.php:62
+#: ../../../../modules/eventdb/application/controllers/EventsController.php:80
+msgid "Host"
+msgstr "Host"
+
+#: ../../../../modules/eventdb/application/controllers/EventsController.php:63
+#: ../../../../modules/eventdb/application/controllers/EventsController.php:81
+msgid "Host Address"
+msgstr "Host Adresse"
+
+#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:105
+msgid "Host service status"
+msgstr "Host Service Status"
+
+#: ../../../../modules/eventdb/library/Eventdb/ProvidedHook/Monitoring/DetailviewExtension.php:66
+msgid "Loading"
+msgstr "Lade"
+
+#: ../../../../modules/eventdb/application/views/scripts/event/index-plain.phtml:35
+#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:50
+#: ../../../../modules/eventdb/application/controllers/EventsController.php:68
+#: ../../../../modules/eventdb/application/controllers/EventsController.php:86
+msgid "Message"
+msgstr "Meldung"
+
+#: ../../../../modules/eventdb/configuration.php:24
+#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:102
+msgid "Monitoring"
+msgstr "Monitoring"
+
+#: ../../../../modules/eventdb/application/forms/Config/MonitoringConfigForm.php:30
+msgid ""
+"Name of the custom variable to enable EventDB integration for (usually \"edb"
+"\")"
+msgstr ""
+"Name der angepassten Variable um die EventDB Integration zu aktivieren "
+"(normalerweise \"edb\")"
+
+#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:130
+msgid "No comments recorded for this event yet."
+msgstr "Keine Kommentare für dieses Ereignis."
+
+#: ../../../../modules/eventdb/application/views/scripts/events/index-plain.phtml:3
+#: ../../../../modules/eventdb/application/views/scripts/events/index.phtml:33
+msgid "No events recorded yet."
+msgstr "Keine Ereignisse gespeichert."
+
+#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:63
+msgid "Other events for"
+msgstr "Andere Ereignisse für"
+
+#: ../../../../modules/eventdb/application/views/scripts/events/details.phtml:45
+#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:122
+msgid "Please only acknowledge manually, if you know what that means."
+msgstr "Bitte nur manuell Bestätigen wenn Sie wissen was das bedeutet."
+
+#: ../../../../modules/eventdb/application/views/scripts/events/index.phtml:6
+msgctxt "Multi-selection help"
+msgid ""
+"Press and hold the Ctrl key while clicking on rows to select multiple rows "
+"or press and hold the Shift key to select a range of rows"
+msgstr ""
+"Drücken und halten Sie STRG während Sie auf Spalten klicken, um mehrere "
+"auszuwählen. Mit der Umschalttaste können Sie Bereiche von Spalten auswählen"
+
+#: ../../../../modules/eventdb/application/views/scripts/events/index-plain.phtml:9
+#: ../../../../modules/eventdb/application/views/scripts/events/details-plain.phtml:8
+#: ../../../../modules/eventdb/application/views/scripts/event/index-plain.phtml:17
+#: ../../../../modules/eventdb/application/views/scripts/event/index-plain.phtml:50
+#: ../../../../modules/eventdb/application/controllers/EventsController.php:67
+#: ../../../../modules/eventdb/application/controllers/EventsController.php:85
+msgid "Priority"
+msgstr "Priorität"
+
+#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:75
+#: ../../../../modules/eventdb/application/controllers/EventsController.php:65
+#: ../../../../modules/eventdb/application/controllers/EventsController.php:83
+msgid "Program"
+msgstr "Programm"
+
+#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:86
+msgid "Program and Host"
+msgstr "Programm und Host"
+
+#: ../../../../modules/eventdb/application/forms/Config/BackendConfigForm.php:40
+msgid "Resource"
+msgstr "Ressource"
+
+#: ../../../../modules/eventdb/configuration.php:45
+msgid "Restrict views to the events that match the filter"
+msgstr "Ereignisse mit einem Filter einschränken"
+
+#: ../../../../modules/eventdb/application/views/scripts/event/index-plain.phtml:10
+#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:10
+msgid "Revocation"
+msgstr "Widerruf"
+
+#: ../../../../modules/eventdb/application/forms/Event/EventCommentForm.php:70
+msgid "Revoke"
+msgstr "Wiederrufen"
+
+#: ../../../../modules/eventdb/application/forms/Config/BackendConfigForm.php:20
+#: ../../../../modules/eventdb/application/forms/Config/MonitoringConfigForm.php:18
+#: ../../../../modules/eventdb/application/forms/Config/GlobalConfigForm.php:15
+msgid "Save"
+msgstr "Speichern"
+
+#: ../../../../modules/eventdb/application/views/scripts/events/index.phtml:84
+msgid "Show More"
+msgstr "Mehr zeigen"
+
+#: ../../../../modules/eventdb/application/forms/Events/AckFilterForm.php:29
+msgid "Show also acknowledged events"
+msgstr "Auch bestätigte Ereignisse anzeigen"
+
+#: ../../../../modules/eventdb/application/forms/Config/BackendConfigForm.php:107
+msgid "Skip Validation"
+msgstr "Überprüfung überspringen"
+
+#: ../../../../modules/eventdb/application/forms/Event/EventCommentForm.php:42
+msgid "Submit"
+msgstr "Absenden"
+
+#: ../../../../modules/eventdb/library/Eventdb/Web/EventdbOutputFormat.php:49
+msgid "Text"
+msgstr "Text"
+
+#: ../../../../modules/eventdb/application/forms/Config/BackendConfigForm.php:39
+msgid "The resource to use"
+msgstr "Die zu benutzenden Ressource"
+
+#: ../../../../modules/eventdb/application/views/scripts/event/index.phtml:121
+msgid "This event is set to auto-clear."
+msgstr "Das Ereignis ist auf automatische Bestätigung gesetzt."
+
+#: ../../../../modules/eventdb/application/views/scripts/events/index-plain.phtml:7
+#: ../../../../modules/eventdb/application/views/scripts/events/details-plain.phtml:6
+#: ../../../../modules/eventdb/application/views/scripts/event/index-plain.phtml:15
+#: ../../../../modules/eventdb/application/views/scripts/event/index-plain.phtml:48
+msgid "Timestamp"
+msgstr "Zeitstempel"
+
+#: ../../../../modules/eventdb/application/views/scripts/events/index-plain.phtml:12
+#: ../../../../modules/eventdb/application/views/scripts/events/details-plain.phtml:11
+#: ../../../../modules/eventdb/application/views/scripts/event/index-plain.phtml:20
+#: ../../../../modules/eventdb/application/views/scripts/event/index-plain.phtml:53
+#: ../../../../modules/eventdb/application/forms/Event/EventCommentForm.php:66
+#: ../../../../modules/eventdb/application/controllers/EventsController.php:64
+#: ../../../../modules/eventdb/application/controllers/EventsController.php:82
+msgid "Type"
+msgstr "Typ"
+
+#: ../../../../modules/eventdb/application/views/scripts/event/index-plain.phtml:33
+msgid "User"
+msgstr "Benutzername"
+
+#: ../../../../modules/eventdb/library/Eventdb/Eventdb.php:141
+msgid "You need to configure a resource to access the EventDB database first"
+msgstr "Sie müssen zuerst die Datenbank der EventDB konfigurieren"
+
+#~ msgid "Backend"
+#~ msgstr "Datenbank"
diff --git a/application/views/helpers/Column.php b/application/views/helpers/Column.php
new file mode 100644
index 0000000..b343de4
--- /dev/null
+++ b/application/views/helpers/Column.php
@@ -0,0 +1,51 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+use Icinga\Module\Eventdb\Event;
+
+class Zend_View_Helper_Column extends Zend_View_Helper_Abstract
+{
+ public function column($column, Event $event, $classes = array())
+ {
+ switch ($column) {
+ case 'host_name':
+ $default = 'host_url';
+ break;
+ case 'message':
+ $default = 'message';
+ break;
+ default:
+ $default = null;
+ break;
+ }
+
+ if ($column === 'ack') {
+ $html = $event->$column ? $this->view->icon('ok', $this->view->translate('Acknowledged')) : '-';
+ } else {
+ $renderer = $this->view->columnConfig->get($column, 'renderer', $default);
+
+ switch ($renderer) {
+ case 'host_url':
+ $html = $this->view->qlink($event->$column, 'eventdb/event/host',
+ array('host' => $event->$column));
+ break;
+ case 'service_url':
+ $html = $this->view->qlink($event->$column, 'eventdb/event/service',
+ array('service' => $event->$column, 'host' => $event->host_name));
+ break;
+ case 'url':
+ $html = $this->view->qlink($event->$column, $event->$column);
+ break;
+ case 'message':
+ $html = $this->view->eventMessage($event->$column);
+ break;
+ default:
+ $html = $this->view->escape($event->$column);
+ break;
+ }
+ }
+
+ return '<td class="' . 'event-' . $this->view->escape($column) . ' '
+ . implode(' ', $classes) . '" data-base-target="_next">' . $html . '</td>';
+ }
+}
diff --git a/application/views/helpers/ColumnHeader.php b/application/views/helpers/ColumnHeader.php
new file mode 100644
index 0000000..3c7f7cd
--- /dev/null
+++ b/application/views/helpers/ColumnHeader.php
@@ -0,0 +1,17 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+class Zend_View_Helper_ColumnHeader extends Zend_View_Helper_Abstract
+{
+ public function columnHeader($columnHeader, $classes = array(), $plain = false)
+ {
+ $header = $this->view->columnConfig->get($columnHeader, 'label', ucwords(str_replace('_', ' ', $columnHeader)));
+ if ($plain) {
+ return $header;
+ }
+ $htm = '<th classes="' . implode(' ', $classes) . '">';
+ $htm .= $this->view->escape($header);
+ $htm .= '</th>';
+ return $htm;
+ }
+}
diff --git a/application/views/helpers/Event.php b/application/views/helpers/Event.php
new file mode 100644
index 0000000..f61a793
--- /dev/null
+++ b/application/views/helpers/Event.php
@@ -0,0 +1,12 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+use Icinga\Module\Eventdb\Event;
+
+class Zend_View_Helper_Event extends Zend_View_Helper_Abstract
+{
+ public function event($data)
+ {
+ return Event::fromData($data);
+ }
+}
diff --git a/application/views/helpers/EventMessage.php b/application/views/helpers/EventMessage.php
new file mode 100644
index 0000000..de52518
--- /dev/null
+++ b/application/views/helpers/EventMessage.php
@@ -0,0 +1,70 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+use Icinga\Application\Config;
+
+class Zend_View_Helper_EventMessage extends Zend_View_Helper_Abstract
+{
+ /**
+ * The RegExp for locating URLs.
+ *
+ * Modifications:
+ * - Don't allow ; in
+ *
+ * @source https://mathiasbynens.be/demo/url-regex
+ */
+ const URL_REGEX = '@(https?)://(-\.)?([^\s/?\.#-]+\.?)+(/[^\s;]*)?@i';
+
+ /**
+ * Purifier instance
+ *
+ * @var HTMLPurifier
+ */
+ protected static $purifier;
+
+ public function eventMessage($message)
+ {
+ $htm = $this->getPurifier()->purify($message);
+
+ // search for URLs and make them a link
+ $htm = preg_replace_callback(
+ static::URL_REGEX,
+ function ($match) {
+ return sprintf(
+ '<a href="%s" target="_blank">%s</a>',
+ htmlspecialchars($match[0]),
+ htmlspecialchars($match[0])
+ );
+ },
+ $htm
+ );
+
+ return $htm;
+ }
+
+ /**
+ * Get the purifier instance
+ *
+ * @return HTMLPurifier
+ */
+ protected function getPurifier()
+ {
+ if (self::$purifier === null) {
+ require_once 'HTMLPurifier/Bootstrap.php';
+ require_once 'HTMLPurifier.php';
+ require_once 'HTMLPurifier.autoload.php';
+
+ $config = HTMLPurifier_Config::createDefault();
+ $config->set('Core.EscapeNonASCIICharacters', true);
+ $config->set('HTML.Allowed', Config::module('eventdb')->get(
+ 'frontend',
+ 'allowed_html',
+ 'p,br,b,a[href|target],i,table,tr,td[colspan],div,*[class]'
+ ));
+ $config->set('Attr.AllowedFrameTargets', array('_blank'));
+ $config->set('Cache.DefinitionImpl', null);
+ self::$purifier = new HTMLPurifier($config);
+ }
+ return self::$purifier;
+ }
+}
diff --git a/application/views/scripts/config/index.phtml b/application/views/scripts/config/index.phtml
new file mode 100644
index 0000000..2c47663
--- /dev/null
+++ b/application/views/scripts/config/index.phtml
@@ -0,0 +1,21 @@
+<div class="controls">
+ <?= $tabs ?>
+</div>
+<div class="content">
+ <h2><?= $this->translate('Database backend') ?></h2>
+ <?= $this->qlink(
+ $this->translate('Create a New Resource'),
+ 'config/createresource',
+ null,
+ array(
+ 'class' => 'button-link',
+ 'data-base-target' => '_next',
+ 'icon' => 'plus',
+ 'title' => $this->translate('Create a new resource'),
+ )
+ ) ?>
+ <?= $backendConfig ?>
+
+ <h2><?= $this->translate('Configure module') ?></h2>
+ <?= $globalConfig ?>
+</div>
diff --git a/application/views/scripts/config/monitoring.phtml b/application/views/scripts/config/monitoring.phtml
new file mode 100644
index 0000000..1f6bbde
--- /dev/null
+++ b/application/views/scripts/config/monitoring.phtml
@@ -0,0 +1,7 @@
+<div class="controls">
+ <?= $tabs ?>
+</div>
+<div class="content">
+ <h2><?= $this->translate('Configure integration into the monitoring module') ?></h2>
+ <?= $form ?>
+</div>
diff --git a/application/views/scripts/event/index-plain.phtml b/application/views/scripts/event/index-plain.phtml
new file mode 100644
index 0000000..5b6ea8d
--- /dev/null
+++ b/application/views/scripts/event/index-plain.phtml
@@ -0,0 +1,62 @@
+<?php
+/** @var \Icinga\Module\Eventdb\Event $event */
+/** @var array $additionalColumns */
+/** @var \Icinga\Repository\RepositoryQuery $comments */
+/** @var \Icinga\Repository\RepositoryQuery $groupedEvents */
+
+$commentTypes = array(
+ $this->translate('Comment'),
+ $this->translate('Acknowledgement'),
+ $this->translate('Revocation'),
+);
+
+$displayColumns = array_merge(array('program', 'message', 'facility'), $additionalColumns);
+?>
+<?= $this->translate('Timestamp') ?>: <?= $event->created ?>
+
+<?= $this->translate('Priority') ?>: <?= strtoupper($event->getPriority()) ?>
+<?= $event->ack ? sprintf(' (%s)', $this->translate('Acknowledged')) : '' ?>
+
+<?= $this->translate('Type') ?>: <?= $event->getType() ?>
+
+<?php foreach ($displayColumns as $col): ?>
+<?= $this->columnHeader($col, null, true) ?>: <?= htmlspecialchars($event->offsetGet($col)) ?>
+
+<?php endforeach ?>
+
+<?php if ($comments->hasResult()): ?>
+[ <?= $this->translate('Comments') ?> ]
+
+<?php foreach ($comments as $comment): ?>
+<?= $commentTypes[$comment->type] ?>: <?= $comment->created ?>
+
+<?= $this->translate('User') ?>: <?= $comment->user ?>
+
+<?= $this->translate('Message') ?>: <?= $comment->message ?>
+
+
+<?php endforeach ?>
+
+<?php endif; ?>
+<?php if ($groupedEvents !== null && $groupedEvents->hasResult()): ?>
+[ <?= $this->translate('Grouped Events') ?> ]
+
+<?php foreach ($groupedEvents as $groupedEventData):
+ /** @var \Icinga\Module\Eventdb\Event $groupedEvent */
+ $groupedEvent = $this->event($groupedEventData);
+?>
+<?= $this->translate('Timestamp') ?>: <?= $event->created ?>
+
+<?= $this->translate('Priority') ?>: <?= strtoupper($event->getPriority()) ?>
+<?= $event->ack ? sprintf(' (%s)', $this->translate('Acknowledged')) : '' ?>
+
+<?= $this->translate('Type') ?>: <?= $event->getType() ?>
+
+<?php foreach (array('host_name', 'program', 'message') as $col): ?>
+<?= $this->columnHeader($col, null, true) ?>: <?= htmlspecialchars($event->offsetGet($col)) ?>
+
+<?php endforeach ?>
+
+<?php endforeach ?>
+<?php endif; ?>
+
diff --git a/application/views/scripts/event/index.phtml b/application/views/scripts/event/index.phtml
new file mode 100644
index 0000000..83dcf01
--- /dev/null
+++ b/application/views/scripts/event/index.phtml
@@ -0,0 +1,174 @@
+<?php
+/** @var \Icinga\Module\Eventdb\Event $event */
+/** @var array $additionalColumns */
+/** @var \Icinga\Repository\RepositoryQuery $comments */
+/** @var \Icinga\Repository\RepositoryQuery $groupedEvents */
+
+$commentIcons = array(
+ $this->icon('comment', $this->translate('Comment')),
+ $this->icon('ok', $this->translate('Acknowledgement')),
+ $this->icon('cancel', $this->translate('Revocation'))
+);
+
+if (! $this->compact): ?>
+<div class="controls">
+ <?= $this->tabs ?>
+</div>
+<?php endif ?>
+<div class="content">
+ <table class="event-summary-table">
+ <tr>
+ <td rowspan="2" class="priority-col <?= $event->getPriority() ?> <?= $event->ack ? 'ack' : '' ?>">
+ <div class="priority-label"><?= strtoupper($event->getPriority()) ?></div>
+ <div class="event-meta"><span class="timeago" title="<?= $event->created ?>"><?= $this->timeAgo(strtotime($event->created)) ?></span></div>
+ </td>
+ <td rowspan="2" class="icon-col">
+ <?= $this->icon($event->getTypeIcon(), $event->getType()) ?>
+ <?php if ($event->ack) { echo $this->icon('ok', $this->translate('Acknowledged')); } ?>
+ <?php if ($event->group_autoclear) { echo $this->icon('reschedule', $this->translate('Auto-Clear')); } ?>
+ </td>
+ <?= $this->column('host_name', $event, array('selectable')) ?>
+ </tr>
+ <tr>
+ <?= $this->column('host_address', $event, array('selectable')) ?>
+ </tr>
+ </table>
+ <h2><?= $this->translate('Details') ?></h2>
+ <table class="name-value-table">
+ <?php
+ $displayColumns = array_merge(array('program', 'message', 'facility'), $additionalColumns);
+ foreach ($displayColumns as $column):
+ if ($column === 'message') continue;
+ ?>
+ <tr>
+ <?= $this->columnHeader($column) ?>
+ <?= $this->column($column, $event) ?>
+ </tr>
+ <?php endforeach ?>
+ </table>
+ <?php if ($event->message): ?>
+ <h2><?= $this->translate('Message') ?></h2>
+ <div class="event-message detail preformatted"><?= $this->eventMessage($event->message) ?></div>
+ <?php endif; ?>
+
+ <?php
+ foreach ($extensionsHtml as $extensionHtml) {
+ echo $extensionHtml;
+ }
+ ?>
+
+ <h2><?= $this->translate('Actions') ?></h2>
+ <table class="name-value-table" data-base-target="_next">
+ <tr>
+ <th><?= $this->translate('Other events for') ?></th>
+ <td><?= $this->qlink(
+ $this->translate('Host'),
+ 'eventdb/events',
+ array('host_name' => $event->host_name),
+ array(
+ 'icon' => 'search',
+ 'class' => 'action-link'
+ )
+ ) ?>
+ <?php if ($event->program): ?>
+ <?= $this->qlink(
+ $this->translate('Program'),
+ 'eventdb/events',
+ array(
+ 'program' => $event->program,
+ ),
+ array(
+ 'icon' => 'search',
+ 'class' => 'action-link'
+ )
+ ) ?>
+ <?= $this->qlink(
+ $this->translate('Program and Host'),
+ 'eventdb/events',
+ array(
+ 'host_name' => $event->host_name,
+ 'program' => $event->program,
+ ),
+ array(
+ 'icon' => 'search',
+ 'class' => 'action-link'
+ )
+ ) ?>
+ <?php endif; ?>
+ </td>
+ </tr>
+ <tr>
+ <tr>
+ <th><?= $this->translate('Monitoring') ?></th>
+ <td>
+ <?= $this->qlink(
+ $this->translate('Host service status'),
+ 'eventdb/event/host',
+ array('host' => $event->host_name),
+ array(
+ 'icon' => 'search',
+ 'class' => 'action-link'
+ )
+ ) ?>
+ </td>
+ </tr>
+ </table>
+
+ <?php if (isset($commentForm)): ?>
+ <h2><?= $this->translate('Add comment / acknowledge') ?></h2>
+ <?php if ($event->group_autoclear): ?>
+ <div class="warning">
+ <?= $this->translate('This event is set to auto-clear.') ?>
+ <?= $this->translate('Please only acknowledge manually, if you know what that means.') ?>
+ </div>
+ <?php endif; ?>
+ <div class="comment-form"><?= $commentForm ?></div>
+ <?php endif ?>
+
+ <h2><?= $this->translate('Comments') ?></h2>
+ <?php if (! $comments->hasResult()): ?>
+ <p><?= $this->translate('No comments recorded for this event yet.') ?></p>
+ <?php else: ?>
+ <table class="common-table comments-table">
+ <tbody>
+ <?php foreach ($comments as $comment): ?>
+ <tr>
+ <td class="comment-created timeago" title="<?= $comment->created ?>"><?= $this->timeAgo(strtotime($comment->created)) ?></td>
+ <td class="comment-type"><?= $commentIcons[$comment->type] ?></td>
+ <td class="comment-user"><?= $this->escape($comment->user) ?></td>
+ <td class="comment-message"><?= $this->escape($comment->message) ?></td>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+ <?php endif; ?>
+
+ <?php if ($groupedEvents !== null && $groupedEvents->hasResult()): ?>
+ <h2><?= $this->translate('Grouped Events') ?></h2>
+ <table class="common-table events-table" data-base-target="_next">
+ <tbody>
+ <?php foreach ($groupedEvents as $groupedEventData):
+ /** @var \Icinga\Module\Eventdb\Event $groupedEvent */
+ $groupedEvent = $this->event($groupedEventData);
+ ?>
+ <tr>
+ <td class="priority-col <?= $groupedEvent->getPriority() ?> <?= $groupedEvent->ack ? 'ack' : '' ?>">
+ <div class="priority-label"><?= strtoupper($groupedEvent->getPriority()) ?></div>
+ <div class="event-meta"><span class="timeago" title="<?= $groupedEvent->created ?>"><?= $this->timeAgo(strtotime($groupedEvent->created)) ?></span></div>
+ </td>
+ <td class="icon-col">
+ <?= $this->icon($groupedEvent->getTypeIcon(), $groupedEvent->getType()) ?>
+ <?php if ($groupedEvent->ack) { echo $this->icon('ok', $this->translate('Acknowledged')); } ?>
+ <?php if ($groupedEvent->group_autoclear) { $autoClear = true; echo $this->icon('reschedule', $this->translate('Auto-Clear')); } ?>
+ </td>
+ <?= $this->column('host_name', $groupedEvent) ?>
+ <?= $this->column('program', $groupedEvent) ?>
+ <?= $this->column('message', $groupedEvent) ?>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+ <?php endif; ?>
+
+</div>
+
diff --git a/application/views/scripts/events/details-plain.phtml b/application/views/scripts/events/details-plain.phtml
new file mode 100644
index 0000000..d5eb724
--- /dev/null
+++ b/application/views/scripts/events/details-plain.phtml
@@ -0,0 +1,18 @@
+<?php foreach($events as $eventData):
+ /** @var \Icinga\Module\Eventdb\Event $event */
+ $event = $this->event($eventData);
+ $url = $this->url('eventdb/event', array('id' => $event->id));
+?>
+<?= $this->translate('Timestamp') ?>: <?= $event->created ?>
+
+<?= $this->translate('Priority') ?>: <?= strtoupper($event->getPriority()) ?>
+<?= $event->ack ? sprintf(' (%s)', $this->translate('Acknowledged')) : '' ?>
+
+<?= $this->translate('Type') ?>: <?= $event->getType() ?>
+
+<?php foreach (array('host_name', 'program', 'message') as $col): ?>
+<?= $this->columnHeader($col, null, true) ?>: <?= htmlspecialchars($event->offsetGet($col)) ?>
+
+<?php endforeach ?>
+
+<?php endforeach ?>
diff --git a/application/views/scripts/events/details.phtml b/application/views/scripts/events/details.phtml
new file mode 100644
index 0000000..ee9f3cd
--- /dev/null
+++ b/application/views/scripts/events/details.phtml
@@ -0,0 +1,50 @@
+<?php
+/** @var array $events */
+
+if (! $this->compact):
+?>
+<div class="controls">
+ <?= $this->tabs ?>
+</div>
+<?php endif ?>
+<div class="content">
+ <table class="common-table event-summary-table table-row-selectable" data-base-target="_next">
+ <?php $autoClear = false; foreach($events as $eventData):
+ /** @var \Icinga\Module\Eventdb\Event $event */
+ $event = $this->event($eventData);
+ $url = $this->url('eventdb/event', array('id' => $event->id));
+ ?>
+ <tr href="<?= $url ?>">
+ <td class="priority-col <?= $event->getPriority() ?> <?= $event->ack ? 'ack' : '' ?>">
+ <div class="priority-label"><?= strtoupper($event->getPriority()) ?></div>
+ <div class="event-meta"><span class="timeago" title="<?= $event->created ?>"><?= $this->timeAgo(strtotime($event->created)) ?></span></div>
+ </td>
+ <td class="icon-col">
+ <?= $this->icon($event->getTypeIcon(), $event->getType()) ?>
+ <?php if ($event->ack) { echo $this->icon('ok', $this->translate('Acknowledged')); } ?>
+ <?php if ($event->group_autoclear) { $autoClear = true; echo $this->icon('reschedule', $this->translate('Auto-Clear')); } ?>
+ </td>
+ <?= $this->column('host_name', $event) ?>
+ <?= $this->column('program', $event) ?>
+ <?= $this->column('message', $event) ?>
+ </tr>
+ <?php endforeach; ?>
+ </table>
+
+ <?php
+ foreach ($extensionsHtml as $extensionHtml) {
+ echo $extensionHtml;
+ }
+ ?>
+
+ <?php if (isset($commentForm)): ?>
+ <h3><?= $this->translate('Add comment / acknowledge') ?></h3>
+ <?php if ($autoClear): ?>
+ <div class="warning">
+ <?= $this->translate('At least one event is set to auto-clear.') ?>
+ <?= $this->translate('Please only acknowledge manually, if you know what that means.') ?>
+ </div>
+ <?php endif; ?>
+ <div class="comment-form"><?= $commentForm ?></div>
+ <?php endif ?>
+</div>
diff --git a/application/views/scripts/events/index-plain.phtml b/application/views/scripts/events/index-plain.phtml
new file mode 100644
index 0000000..55f4c2e
--- /dev/null
+++ b/application/views/scripts/events/index-plain.phtml
@@ -0,0 +1,19 @@
+<?php /** @var \Icinga\Repository\RepositoryQuery $events */
+if (! $events->hasResult()): ?>
+<?= $this->translate('No events recorded yet.') ?>
+<?php else:
+$displayColumns = array_merge(array('host_name', 'program', 'message', 'facility'), $additionalColumns);
+foreach ($events as $eventData): /** @var \Icinga\Module\Eventdb\Event $event */ $event = $this->event($eventData); ?>
+<?= $this->translate('Timestamp') ?>: <?= $event->created ?>
+
+<?= $this->translate('Priority') ?>: <?= strtoupper($event->getPriority()) ?>
+<?= $event->ack ? sprintf(' (%s)', $this->translate('Acknowledged')) : '' ?>
+
+<?= $this->translate('Type') ?>: <?= $event->getType() ?>
+
+<?php foreach ($displayColumns as $col): ?>
+<?= $this->columnHeader($col, null, true) ?>: <?= htmlspecialchars($event->offsetGet($col)) ?>
+
+<?php endforeach ?>
+
+<?php endforeach; endif; ?>
diff --git a/application/views/scripts/events/index.phtml b/application/views/scripts/events/index.phtml
new file mode 100644
index 0000000..f1e4da7
--- /dev/null
+++ b/application/views/scripts/events/index.phtml
@@ -0,0 +1,93 @@
+<?php if (! $this->compact): ?>
+<div class="controls">
+ <?= $this->tabs ?>
+ <?php
+ $helpMessage = $this->translate(
+ 'Press and hold the Ctrl key while clicking on rows to select multiple rows or press and hold the Shift key to'
+ . ' select a range of rows',
+ 'Multi-selection help'
+ );
+ ?>
+ <div class="selection-info" title="<?= $this->escape($helpMessage) ?>">
+ <?= sprintf(
+ /// TRANSLATORS: Please leave %s as it is because the selection counter is wrapped in a span tag for updating
+ /// the counter via JavaScript
+ $this->translate('%s row(s) selected', 'Multi-selection count'),
+ '<span class="selection-info-count">0</span>'
+ ) ?>
+ </div>
+ <?= $this->paginator ?>
+ <div class="sort-controls-container">
+ <?= $this->limiter ?>
+ <?= $this->sortBox ?>
+ </div>
+ <?= $this->filterEditor ?>
+ <div class="quick-filter-controls">
+ <?= $this->severityFilterForm ?>
+ <?= $this->ackFilterForm ?>
+ </div>
+</div>
+<?php endif ?>
+<div class="content">
+<?php /** @var \Icinga\Repository\RepositoryQuery $events */if (! $events->hasResult()): ?>
+ <p><?= $this->translate('No events recorded yet.') ?></p>
+</div>
+<?php return; endif; $displayColumns = array_merge(array('host_name', 'program', 'message', 'facility'), $additionalColumns); ?>
+ <table class="common-table table-row-selectable multiselect table-responsive events-table"
+ data-base-target="_next"
+ data-icinga-multiselect-url="<?= $this->href('eventdb/events/details') ?>"
+ data-icinga-multiselect-controllers="<?= $this->href('eventdb/events') ?>"
+ data-icinga-multiselect-data="id">
+ <?php if (! $this->compact): ?>
+ <thead>
+ <tr>
+ <th></th>
+ <th></th>
+ <?php foreach ($displayColumns as $displayColumn): ?>
+ <?= $this->columnHeader($displayColumn) ?>
+ <?php endforeach ?>
+ </tr>
+ </thead>
+ <?php endif; ?>
+ <tbody>
+ <?php
+ foreach ($events as $eventData):
+ /** @var \Icinga\Module\Eventdb\Event $event */
+ $event = $this->event($eventData);
+ $created = $event->created;
+ $createdTs = strtotime($created);
+ $url = $this->url('eventdb/event', array('id' => $event->id));
+ $classes = array('priority-col', $event->getPriority());
+ if ($event->ack) {
+ $classes[] = 'ack';
+ }
+ ?>
+ <tr href="<?= $url ?>">
+ <td class="<?= implode(' ', $classes) ?>">
+ <div class="priority-label"><?= strtoupper($event->getPriority()) ?></div>
+ <div class="event-meta"><a href="<?= $url ?>" class="timeago" title="<?= $created ?>"><?= $this->timeAgo($createdTs) ?></a></div>
+ </td>
+ <td class="icon-col">
+ <?= $this->icon($event->getTypeIcon(), $event->getType()) ?>
+ <?php if ($event->ack) { echo $this->icon('ok', $this->translate('Acknowledged')); } ?>
+ <?php if ($event->group_autoclear) { echo $this->icon('reschedule', $this->translate('Auto-Clear')); } ?>
+ </td>
+ <?php foreach ($displayColumns as $displayColumn): ?>
+ <?= $this->column($displayColumn, $event) ?>
+ <?php endforeach ?>
+ </tr>
+ <?php endforeach ?>
+ </tbody>
+ </table>
+<?php if ($this->compact && $events->hasMore()): ?>
+ <?= $this->qlink(
+ $this->translate('Show More'),
+ $this->url()->without(array('view', 'limit')),
+ null,
+ array(
+ 'data-base-target' => '_next',
+ 'class' => 'action-link'
+ )
+ ) ?>
+<?php endif; ?>
+</div>
diff --git a/application/views/scripts/format/text.phtml b/application/views/scripts/format/text.phtml
new file mode 100644
index 0000000..647914b
--- /dev/null
+++ b/application/views/scripts/format/text.phtml
@@ -0,0 +1,13 @@
+<?php
+/** @var string $text */
+/** @var string|null $partial */
+if (! $this->compact): ?>
+<div class="controls"><?= $this->tabs ?></div>
+<?php endif ?>
+<div class="content">
+<?php if ($partial !== null): ?>
+ <pre class="copyable"><?= $this->partial($partial . '.phtml', null, $this) ?></pre>
+<?php else: ?>
+ <pre class="copyable"><?= htmlspecialchars($text) ?></pre>
+<?php endif ?>
+</div>
diff --git a/configuration.php b/configuration.php
new file mode 100644
index 0000000..0147b89
--- /dev/null
+++ b/configuration.php
@@ -0,0 +1,46 @@
+<?php
+/** @var Icinga\Application\Modules\Module $this */
+
+$config = $this->getConfig();
+
+$url = 'eventdb/events';
+if (($default_filter = $config->get('global', 'default_filter')) !== null) {
+ $url .= '?' . $default_filter;
+}
+
+$section = $this->menuSection('EventDB', array(
+ 'icon' => 'tasks',
+ 'priority' => 200,
+ 'url' => $url,
+));
+
+$this->provideConfigTab('config', array(
+ 'title' => $this->translate('Configure EventDB'),
+ 'label' => $this->translate('Config'),
+ 'url' => 'config'
+));
+$this->provideConfigTab('monitoring', array(
+ 'title' => $this->translate('Configure integration into the monitoring module'),
+ 'label' => $this->translate('Monitoring'),
+ 'url' => 'config/monitoring'
+));
+
+$this->providePermission(
+ 'eventdb/events',
+ $this->translate('Allow to view events')
+);
+
+$this->providePermission(
+ 'eventdb/comments',
+ $this->translate('Allow to view comments')
+);
+
+$this->providePermission(
+ 'eventdb/interact',
+ $this->translate('Allow to acknowledge and comment events')
+);
+
+$this->provideRestriction(
+ 'eventdb/events/filter',
+ $this->translate('Restrict views to the events that match the filter')
+);
diff --git a/doc/02-Configuration.md b/doc/02-Configuration.md
new file mode 100644
index 0000000..0f6b04e
--- /dev/null
+++ b/doc/02-Configuration.md
@@ -0,0 +1,53 @@
+Configuration
+=============
+
+## Database Resource
+
+To let the module know where the events are stored you have to create an SQL
+database resource, with the EventDB schema, and events collected by EventDB.
+
+Access to the EventDB for the module is handled with normal Icingaweb2 resources.
+
+Create the resource and go to the config area to select it:
+
+ Configuration -> Modules -> Eventdb -> Backend
+
+![Configuration Backend](screenshots/configuration-backend.png)
+
+## Monitoring Integration
+
+The EventDB module integrates into Icinga Web 2's monitoring module by default,
+offering action links in host and service detail views.
+
+![Configuration Monitoring](screenshots/configuration-monitoring.png)
+
+### Default actions
+
+By default, every host and services shows an action link to the event list,
+filtered by host name.
+
+### Custom Variable
+
+You can configure a custom variable that enables the integration selectively.
+
+* `_edb` or `vars.edb` will enable the actions only on objects that have the custom var
+* `_edb_filter` or `vars.edb_filter` allows you to pre-filter the linked events
+
+The name of the customvar (`edb`) needs to be configured in the config area of the module.
+
+Also see [Custom Variables](03-CustomVars.md) documentation.
+
+### Always show actions
+
+There are options to always show actions on host or service, even if the custom variable
+is not set.
+
+### Detail view
+
+Icingaweb adds a new feature in 2.5.0 to allow extra content inside the detail views
+of hosts and services.
+
+By default the view filters for not acknowledged events, and shows you the last, most
+critical events first. One can jump to events list or a detail view immediately.
+
+Custom filters can be added in the config area, also the view can be disabled there. \ No newline at end of file
diff --git a/doc/03-CustomVars.md b/doc/03-CustomVars.md
new file mode 100644
index 0000000..cfd4b74
--- /dev/null
+++ b/doc/03-CustomVars.md
@@ -0,0 +1,50 @@
+Custom Variables
+================
+
+The monitoring integration of this module can use custom variables from the
+Icinga context to control display and filtering of the integration.
+
+Also see [Configuration](02-Configuration.md) on how to configure the features.
+
+Custom variables control:
+
+* If the EventDB integration and actions are shown for a host or service
+* How the linked results should be filtered
+
+## Examples
+
+For Icinga 2:
+
+```icinga2
+object Host "test" {
+ import "generic-host"
+
+ address = "127.0.0.1"
+
+ vars.edb = "1"
+ vars.edb_filter = "priority!=7&priority!=5&priority!=6&ack=0"
+ // ...
+}
+```
+
+For Icinga 1.x:
+
+```nagios
+define host {
+ use generic-host
+ host_name test
+
+ address 127.0.0.1
+
+ _edb 1
+ _edb_filter priority!=7&priority!=5&priority!=6&ack=0
+}
+```
+
+**Note:** A filter by `host_name` will always be added, unless you have `host_name` as part of your filter.
+
+## Legacy filters
+
+The module also supports legacy JSON filters from the icinga-web 1.x EventDB module.
+
+Please see the `examples` directory of this module for some supported filters.
diff --git a/doc/09-Security.md b/doc/09-Security.md
new file mode 100644
index 0000000..9aab433
--- /dev/null
+++ b/doc/09-Security.md
@@ -0,0 +1,26 @@
+Security
+============================
+
+The EventDB Module provides permissions and restrictions as described below.
+
+## Permissions
+
+| Name | Description |
+| ---------------- | ----------- |
+| eventdb/events | Allow to view events |
+| eventdb/comments | Allow to view comments |
+| eventdb/interact | Allow to acknowledge and comment events |
+
+## Restrictions
+
+| Name | Description |
+| ----------------------- | ----------- |
+| eventdb/events/filter | Restrict views to the events that match the filter |
+| eventdb/comments/filter | Restrict views to the comments that match the filter |
+
+## Examples
+
+| eventdb/events/filter | Description |
+| -------------------------- | ----------- |
+| type!=syslog | Hide the Syslog events from a role |
+| type=syslog&program=icinga | Show only Icinga-related Syslog events to a role |
diff --git a/doc/10-Screenshots.md b/doc/10-Screenshots.md
new file mode 100644
index 0000000..22905d3
--- /dev/null
+++ b/doc/10-Screenshots.md
@@ -0,0 +1,24 @@
+Screenshots
+===========
+
+**Overview of Events**
+
+![Screenshot](screenshots/overview.png)
+
+**Filtered overview**
+
+![Screenshot](screenshots/overview-filtered.png)
+
+**Detail area**
+
+![Screenshot](screenshots/overview-with-details.png)
+
+**Monitoring actions**
+
+![Screenshot](screenshots/monitoring-actions.png)
+
+**Monitoring detail view**
+
+For Icinga Web 2 >= 2.5.0
+
+![Screenshot](screenshots/monitoring-detailview.png) \ No newline at end of file
diff --git a/doc/screenshots/configuration-backend.png b/doc/screenshots/configuration-backend.png
new file mode 100644
index 0000000..6de9fba
--- /dev/null
+++ b/doc/screenshots/configuration-backend.png
Binary files differ
diff --git a/doc/screenshots/configuration-monitoring.png b/doc/screenshots/configuration-monitoring.png
new file mode 100644
index 0000000..d6c9e8d
--- /dev/null
+++ b/doc/screenshots/configuration-monitoring.png
Binary files differ
diff --git a/doc/screenshots/monitoring-actions.png b/doc/screenshots/monitoring-actions.png
new file mode 100644
index 0000000..724e0a0
--- /dev/null
+++ b/doc/screenshots/monitoring-actions.png
Binary files differ
diff --git a/doc/screenshots/monitoring-detailview.png b/doc/screenshots/monitoring-detailview.png
new file mode 100644
index 0000000..e7ceb83
--- /dev/null
+++ b/doc/screenshots/monitoring-detailview.png
Binary files differ
diff --git a/doc/screenshots/overview-filtered.png b/doc/screenshots/overview-filtered.png
new file mode 100644
index 0000000..d183cd8
--- /dev/null
+++ b/doc/screenshots/overview-filtered.png
Binary files differ
diff --git a/doc/screenshots/overview-with-details.png b/doc/screenshots/overview-with-details.png
new file mode 100644
index 0000000..53a6008
--- /dev/null
+++ b/doc/screenshots/overview-with-details.png
Binary files differ
diff --git a/doc/screenshots/overview.png b/doc/screenshots/overview.png
new file mode 100644
index 0000000..99cc82d
--- /dev/null
+++ b/doc/screenshots/overview.png
Binary files differ
diff --git a/examples/legacy-filter/column-integration.json b/examples/legacy-filter/column-integration.json
new file mode 100644
index 0000000..474031f
--- /dev/null
+++ b/examples/legacy-filter/column-integration.json
@@ -0,0 +1,50 @@
+/*
+ NOTE: this is a JSON file with comments. You might not be able to parse it, depending on the implementation!
+
+ These are the filter features mapped by the edbColumn.js integration.
+
+ See https://git.netways.org/eventdb/eventdb/blob/master/icinga-cronk/EventDB/lib/js/edbColumn.js
+*/
+{
+ "hostFilter": {
+ // Note: `data.host` is by default the hostname, unless set in edb_filter
+ "include_pattern": "{{mapped from data.host}}",
+ "include_pattern_type": "regexp",
+ "exclude_pattern_type": "disabled",
+ "exclude_pattern": false,
+ "include_set": [],
+ "exclude_set": []
+ },
+ "programFilter": {
+ "include_pattern": false,
+ "include_pattern_type": "disabled",
+ "exclude_pattern": false,
+ "exclude_pattern_type": "disabled",
+ "include_set": [ /* mapped from data.programInclusion */ ],
+ "exclude_set": [ /* mapped from data.programExclusion */ ]
+ },
+ "messageFilter": {
+ "items": [ /* mapped from data.msg */ ]
+ },
+ "misc": {
+ "hideAck": false
+ },
+ "sourceExclusion": [ /* mapped from data.sourceExclusion */ ],
+ "priorityExclusion": [ /* mapped from data.priorityExclusion */ ],
+ "facilityExclusion": [ /* mapped from data.facilityExclusion */ ],
+ "timespan": {
+ "from": "{{ mapped from data.startTime }}",
+ "to": -1
+ },
+ "display": {
+ "order": {
+ "field": "created",
+ "dir": "desc"
+ },
+ "group": {
+ "field": null
+ },
+ "count": "id",
+ "limit": 50
+ }
+}
diff --git a/examples/legacy-filter/examples.md b/examples/legacy-filter/examples.md
new file mode 100644
index 0000000..fdcba03
--- /dev/null
+++ b/examples/legacy-filter/examples.md
@@ -0,0 +1,9 @@
+Legacy Filter JSON examples
+===========================
+
+Here are some JSON examples, each line is a single filter used in `edb_filter` custom variable.
+
+ { host: 'otherhostname' }
+ { host: 'specialhostname', priorityExclusion: [] }
+ { "host": ".*", "programInclusion": ["cloud-monitoring"] }
+ { programInclusion: ['test-program'] }
diff --git a/examples/legacy-filter/full-filter.json b/examples/legacy-filter/full-filter.json
new file mode 100644
index 0000000..5d89277
--- /dev/null
+++ b/examples/legacy-filter/full-filter.json
@@ -0,0 +1,77 @@
+/*
+ NOTE: this is a JSON file with comments. You might not be able to parse it, depending on the implementation!
+
+ This is the full filter style, that the old EventDB Cronk supported.
+
+ The old monitoring integration *did not* support the exact filter.
+*/
+{
+ "hostFilter": {
+ "include_pattern": "test",
+ "include_pattern_type": "exact",
+ "exclude_pattern_type": "regexp",
+ "exclude_pattern": "aa.*b",
+ "include_set": [
+ "test"
+ ],
+ "exclude_set": []
+ },
+ "programFilter": {
+ "include_pattern": false,
+ "include_pattern_type": "disabled",
+ "exclude_pattern": "test",
+ "exclude_pattern_type": "contains",
+ "include_set": [
+ ""
+ ],
+ "exclude_set": []
+ },
+ "messageFilter": {
+ "items": [
+ {
+ "type": "exc",
+ "message": "onlydebug",
+ "isRegexp": false
+ },
+ {
+ "type": "inc",
+ "message": "needthis.*",
+ "isRegexp": true
+ }
+ ]
+ },
+ "misc": {
+ "hideAck": true
+ },
+ "sourceExclusion": [
+ "2",
+ "3",
+ "4"
+ ],
+ "priorityExclusion": [
+ "5",
+ "6",
+ "7"
+ ],
+ "facilityExclusion": [
+ "8",
+ "9",
+ "10",
+ "11"
+ ],
+ "timespan": {
+ "from": 1502362200,
+ "to": 1502362200
+ },
+ "display": {
+ "order": {
+ "field": "created",
+ "dir": "desc"
+ },
+ "group": {
+ "field": null
+ },
+ "count": "id",
+ "limit": 200
+ }
+} \ No newline at end of file
diff --git a/library/Eventdb/Data/LegacyFilterParser.php b/library/Eventdb/Data/LegacyFilterParser.php
new file mode 100644
index 0000000..2907202
--- /dev/null
+++ b/library/Eventdb/Data/LegacyFilterParser.php
@@ -0,0 +1,153 @@
+<?php
+/* Icinga Web 2 - EventDB | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Eventdb\Data;
+
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterAnd;
+use Icinga\Data\Filter\FilterOr;
+use Icinga\Exception\InvalidPropertyException;
+
+/**
+ * Class LegacyFilterParser
+ *
+ * Utility class to parse Icinga-web 1.x JSON filters of the EventDB module
+ */
+class LegacyFilterParser
+{
+ /**
+ * @param $json string JSON data
+ * @param $host string Icinga host name (for default host filter and logging)
+ * @param $service string Icinga service name (for logging)
+ *
+ * @return Filter
+ * @throws InvalidPropertyException When filter could not be parsed
+ */
+ static public function parse($json, $host, $service = null)
+ {
+ $json = static::fixJSONQuotes($json);
+
+ $data = json_decode($json, false, 5);
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ throw new InvalidPropertyException(
+ 'Could not decode legacy filter (host=%s)%s (%s): %s',
+ $host,
+ ($service ? ' (service=' . $service . ')' : ''),
+ json_last_error_msg(),
+ $json
+ );
+ }
+
+ $filter = new FilterAnd();
+
+ if (property_exists($data, 'host')) {
+ // Note: we can' support regexp right now, but we replace '.*' to a normal wildcard
+ $data->host = str_replace('.*', '*', $data->host);
+ if ($data->host === '*') {
+ $data->host = null;
+ }
+ } else {
+ $data->host = $host;
+ }
+ if ($data->host !== null) {
+ $filter->andFilter(Filter::expression('host_name', '=', $data->host));
+ }
+
+ static::handleArray($filter, $data, 'programInclusion', 'program');
+ static::handleArray($filter, $data, 'programExclusion', 'program', '!=');
+
+ static::handleArray($filter, $data, 'priorityExclusion', 'priority', '!=');
+ static::handleArray($filter, $data, 'sourceExclusion', 'source', '!=');
+ static::handleArray($filter, $data, 'facilityExclusion', 'facility', '!=');
+
+ // TODO: msg - when really needed
+ // TODO: startTime - when really needed
+
+ // Note: any other field or data part gets ignored...
+
+ return $filter;
+ }
+
+ protected static function handleArray(Filter $filter, $data, $property, $filterAttr, $op = '=')
+ {
+ if (property_exists($data, $property) && ! empty($data->$property)) {
+ if ($op === '!=') {
+ $subFilter = $filter;
+ } else {
+ $subFilter = new FilterOr;
+ }
+
+ /*
+ if (is_array($data->$property) && count($data->$property) === 1) {
+ $data->$property = current($data->$property);
+ }
+ */
+ if (! is_array($data->$property)) {
+ $data->$property = array($data->$property);
+ }
+ foreach ($data->$property as $val) {
+ $subFilter->addFilter(Filter::expression($filterAttr, $op, $val));
+ }
+
+ if ($subFilter !== $filter) {
+ $filters = $subFilter->filters();
+ if ($filter->isChain() && count($filters) > 1) {
+ $filter->andFilter($subFilter);
+ } else {
+ $filter->andFilter(current($filters));
+ }
+ }
+ }
+ }
+
+ /**
+ * @author partially by NikiC https://stackoverflow.com/users/385378/nikic
+ * @source partially from https://stackoverflow.com/a/20440596/449813
+ *
+ * @param $json string
+ *
+ * @return string
+ */
+ public static function fixJSONQuotes($json)
+ {
+ // fix unquoted identifiers
+ $json = preg_replace('/([{,]+)(\s*)([^"]+?)\s*:/', '$1"$3":', $json);
+
+ $regex = <<<'REGEX'
+~
+ "[^"\\]*(?:\\.|[^"\\]*)*"
+ (*SKIP)(*F)
+ | '([^'\\]*(?:\\.|[^'\\]*)*)'
+~x
+REGEX;
+
+ return preg_replace_callback($regex, function ($matches) {
+ return '"' . preg_replace('~\\\\.(*SKIP)(*F)|"~', '\\"', $matches[1]) . '"';
+ }, $json);
+ }
+
+ /**
+ * Basic check if it looks like a JSON filter
+ *
+ * @param $string
+ *
+ * @return bool
+ */
+ static public function isJsonFilter($string)
+ {
+ if (! $string || ! is_string($string)) {
+ return false;
+ }
+
+ $string = trim($string);
+ if (empty($string)) {
+ return false;
+ }
+
+ if (preg_match('/^\{.*\}$/s', $string)) {
+ // looks like JSON data
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/library/Eventdb/Event.php b/library/Eventdb/Event.php
new file mode 100644
index 0000000..a3bba1a
--- /dev/null
+++ b/library/Eventdb/Event.php
@@ -0,0 +1,120 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Eventdb;
+
+use ArrayObject;
+
+class Event extends ArrayObject
+{
+ public static $facilities = array(
+ 0 => 'kernel messages',
+ 1 => 'user-level messages',
+ 2 => 'mail system',
+ 3 => 'system daemons',
+ 4 => 'security/authorization messages',
+ 5 => 'messages generated internally by syslogd',
+ 6 => 'line printer subsystem',
+ 7 => 'network news subsystem',
+ 8 => 'UUCP subsystem',
+ 9 => 'clock daemon',
+ 10 => 'security/authorization messages',
+ 11 => 'FTP daemon',
+ 12 => 'NTP subsystem',
+ 13 => 'log audit',
+ 14 => 'log alert',
+ 15 => 'clock daemon',
+ 16 => 'local use 0',
+ 17 => 'local use 1',
+ 18 => 'local use 2',
+ 19 => 'local use 3',
+ 20 => 'local use 4',
+ 21 => 'local use 5',
+ 22 => 'local use 6',
+ 23 => 'local use 7'
+ );
+
+ public static $priorities = array(
+ 0 => 'emergency',
+ 1 => 'alert',
+ 2 => 'critical',
+ 3 => 'error',
+ 4 => 'warning',
+ 5 => 'notice',
+ 6 => 'info',
+ 7 => 'debug'
+ );
+
+ public static $types = array(
+ 0 => 'syslog',
+ 1 => 'snmp',
+ 2 => 'mail'
+ );
+
+ public static $typeIcons = array(
+ '_default' => 'help',
+ 'syslog' => 'doc-text',
+ 'snmp' => 'plug',
+ 'mail' => 'bell',
+ );
+
+ public function __construct($data)
+ {
+ parent::__construct($data, ArrayObject::ARRAY_AS_PROPS);
+ }
+
+ public function offsetGet($index)
+ {
+ if (! $this->offsetExists($index)) {
+ return null;
+ }
+ $getter = 'get' . ucfirst($index);
+ if (method_exists($this, $getter)) {
+ return $this->$getter();
+ }
+ return parent::offsetGet($index);
+ }
+
+ public function getAck()
+ {
+ return (bool) parent::offsetGet('ack');
+ }
+
+ public function getFacility()
+ {
+ $facility = (int) parent::offsetGet('facility');
+ return array_key_exists($facility, static::$facilities) ? static::$facilities[$facility] : $facility;
+ }
+
+ public function getPriority()
+ {
+ $priority = (int) parent::offsetGet('priority');
+ return array_key_exists($priority, static::$priorities) ? static::$priorities[$priority] : $priority;
+ }
+
+ public function getType()
+ {
+ $type = (int) parent::offsetGet('type');
+ return array_key_exists($type, static::$types) ? static::$types[$type] : $type;
+ }
+
+ public function getTypeIcon()
+ {
+ if (array_key_exists($type = $this->getType(), static::$typeIcons)) {
+ return static::$typeIcons[$type];
+ } else {
+ return static::$typeIcons['_default'];
+ }
+ }
+
+ public static function fromData($data)
+ {
+ return new static($data);
+ }
+
+ public static function getPriorityId($priorityName)
+ {
+ $priorities = array_flip(static::$priorities);
+ return $priorities[$priorityName];
+ }
+}
diff --git a/library/Eventdb/Eventdb.php b/library/Eventdb/Eventdb.php
new file mode 100644
index 0000000..b885258
--- /dev/null
+++ b/library/Eventdb/Eventdb.php
@@ -0,0 +1,184 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Eventdb;
+
+use Icinga\Application\Config;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterExpression;
+use Icinga\Data\ResourceFactory;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Repository\DbRepository;
+use Icinga\Repository\RepositoryQuery;
+
+class Eventdb extends DbRepository
+{
+ /**
+ * {@inheritdoc}
+ */
+ const DATETIME_FORMAT = 'Y-m-d G:i:s';
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $tableAliases = array(
+ 'comment' => 'c',
+ 'event' => 'e',
+ );
+
+ /**
+ * Default query columns
+ *
+ * @var array
+ */
+ protected static $defaultQueryColumns = array(
+ 'event' => array(
+ 'id',
+ 'host_name',
+ 'host_address',
+ 'type',
+ 'facility',
+ 'priority',
+ 'program',
+ 'message',
+ 'alternative_message',
+ 'ack',
+ 'created',
+ 'modified',
+ ),
+ 'comment' => array(
+ 'id',
+ 'event_id',
+ 'type',
+ 'message',
+ 'created',
+ 'modified',
+ 'user'
+ )
+ );
+
+ protected static $edbcQueryColumns = array(
+ 'event' => array(
+ 'group_active',
+ 'group_id',
+ 'group_count',
+ 'group_leader',
+ 'group_autoclear',
+ 'flags',
+ 'alternative_message'
+ )
+ );
+
+ /** @var bool */
+ protected $hasCorrelatorExtensions = null;
+
+ /**
+ * Checks if Event repository has EDBC columns
+ *
+ * @return bool
+ */
+ public function hasCorrelatorExtensions()
+ {
+ if ($this->hasCorrelatorExtensions === null) {
+ $dba = $this->getDataSource()->getDbAdapter();
+ $result = $dba->fetchRow("SHOW COLUMNS FROM `event` LIKE 'group_leader'");
+
+ $this->hasCorrelatorExtensions = ! ! $result;
+ }
+ return $this->hasCorrelatorExtensions;
+ }
+
+ public function filterGroups(RepositoryQuery $query)
+ {
+ if ($this->hasCorrelatorExtensions()) {
+ $query->addFilter(Filter::matchAny(
+ Filter::expression('group_leader', '=', -1),
+ new FilterExpression('group_leader', 'IS', new \Zend_Db_Expr('NULL'))
+ ));
+ }
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function initializeQueryColumns()
+ {
+ $additionalColumns = Config::module('eventdb', 'columns')->keys();
+ $queryColumns = static::$defaultQueryColumns;
+ if ($this->hasCorrelatorExtensions()) {
+ foreach (static::$edbcQueryColumns as $table => $fields) {
+ if (array_key_exists($table, $queryColumns)) {
+ $queryColumns[$table] = array_merge($queryColumns[$table], $fields);
+ } else {
+ $queryColumns[$table] = $fields;
+ }
+ }
+ }
+ if ($additionalColumns !== null) {
+ $eventColumns = $queryColumns['event'];
+ $queryColumns['event'] = array_merge($eventColumns, array_diff($additionalColumns, $eventColumns));
+ }
+ return $queryColumns;
+ }
+
+ /**
+ * Create and return a new instance of the Eventdb
+ *
+ * @param ConfigObject $config The configuration to use, otherwise the module's configuration
+ *
+ * @return static
+ *
+ * @throws ConfigurationError In case no resource has been configured in the module's configuration
+ */
+ public static function fromConfig(ConfigObject $config = null)
+ {
+ if ($config === null) {
+ $moduleConfig = Config::module('eventdb');
+ if (($resourceName = $moduleConfig->get('backend', 'resource')) === null) {
+ throw new ConfigurationError(
+ mt('eventdb', 'You need to configure a resource to access the EventDB database first')
+ );
+ }
+
+ $resource = ResourceFactory::create($resourceName);
+ } else {
+ $resource = ResourceFactory::createResource($config);
+ }
+
+ return new static($resource);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function initializeConversionRules()
+ {
+ return array('event' => array('host_address' => 'ip_address'));
+ }
+
+ /**
+ * Convert an IP address into its human-readable form
+ *
+ * @param string $rawAddress
+ *
+ * @return string
+ */
+ protected function retrieveIpAddress($rawAddress)
+ {
+ return $rawAddress === null ? null : inet_ntop($rawAddress);
+ }
+
+ /**
+ * Convert an IP address into its binary form
+ *
+ * @param string $address
+ *
+ * @return string
+ */
+ protected function persistIpAddress($address)
+ {
+ return $address === null ? null : inet_pton($address);
+ }
+}
diff --git a/library/Eventdb/EventdbController.php b/library/Eventdb/EventdbController.php
new file mode 100644
index 0000000..538553e
--- /dev/null
+++ b/library/Eventdb/EventdbController.php
@@ -0,0 +1,181 @@
+<?php
+/* Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Eventdb;
+
+use Icinga\Application\Icinga;
+use Icinga\Data\QueryInterface;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\Json\JsonEncodeException;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+use Icinga\Web\Controller;
+use Icinga\Web\View;
+
+class EventdbController extends Controller
+{
+ /** @var View */
+ public $view;
+
+ /** @var MonitoringBackend */
+ protected $monitoringBackend;
+
+ /**
+ * Get the EventDB repository
+ *
+ * @return Eventdb
+ */
+ protected function getDb()
+ {
+ return Eventdb::fromConfig();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRestrictions($name, $permission = null)
+ {
+ $restrictions = array();
+ if ($this->Auth()->isAuthenticated()) {
+ foreach ($this->Auth()->getUser()->getRoles() as $role) {
+ if ($permission !== null && ! in_array($permission, $role->getPermissions())) {
+ continue;
+ }
+ $restrictionsFromRole = $role->getRestrictions($name);
+ if (empty($restrictionsFromRole)) {
+ $restrictions = array();
+ break;
+ } else {
+ if (! is_array($restrictionsFromRole)) {
+ $restrictionsFromRole = array($restrictionsFromRole);
+ }
+ $restrictions = array_merge($restrictions, array_values($restrictionsFromRole));
+ }
+ }
+ }
+ return $restrictions;
+ }
+
+ /**
+ * Retrieves the Icinga MonitoringBackend
+ *
+ * @param string|null $name
+ *
+ * @return MonitoringBackend
+ * @throws IcingaException When monitoring is not enabled
+ */
+ protected function monitoringBackend($name = null)
+ {
+ if ($this->monitoringBackend === null) {
+ if (! Icinga::app()->getModuleManager()->hasEnabled('monitoring')) {
+ throw new IcingaException('The module "monitoring" must be enabled and configured!');
+ }
+ $this->monitoringBackend = MonitoringBackend::instance($name);
+ }
+ return $this->monitoringBackend;
+ }
+
+ protected function setViewScript($name)
+ {
+ $this->_helper->viewRenderer->setNoController(true);
+ $this->_helper->viewRenderer->setScriptAction($name);
+ }
+
+ protected function isFormatRequest()
+ {
+ return $this->hasParam('format');
+ }
+
+ protected function isApiRequest()
+ {
+ $format = $this->getParam('format');
+ $header = $this->getRequest()->getHeader('Accept');
+
+ if ($format === 'json' || preg_match('#application/json(;.+)?#', $header)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ protected function isTextRequest()
+ {
+ $format = $this->getParam('format');
+ if ($format === 'text' || $this->isPlainTextRequest()) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ protected function isPlainTextRequest()
+ {
+ $header = $this->getRequest()->getHeader('Accept');
+ if ($header !== null && preg_match('#text/plain#', $header)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Send the user a summary of SQL queries
+ *
+ * @param array|QueryInterface $queries
+ */
+ protected function sendSqlSummary($queries)
+ {
+ if (! is_array($queries)) {
+ $queries = array($queries);
+ }
+
+ $str = '';
+ foreach ($queries as $query) {
+ if ($query !== null) {
+ $str .= wordwrap($query) . "\n\n";
+ }
+ }
+
+ $this->sendText($str);
+ }
+
+ /**
+ * Output JSON data to the requester
+ *
+ * @param mixed $data
+ * @param int $options
+ * @param int $depth
+ *
+ * @throws JsonEncodeException
+ */
+ protected function sendJson($data, $options = 0, $depth = 100)
+ {
+ header('Content-Type: application/json');
+
+ if (defined('JSON_PARTIAL_OUTPUT_ON_ERROR')) {
+ $options |= JSON_PARTIAL_OUTPUT_ON_ERROR;
+ }
+
+ $output = json_encode($data, $options, $depth);
+ if (! $output && json_last_error() !== null) {
+ throw new JsonEncodeException('JSON error: ' . json_last_error_msg());
+ }
+ echo $output;
+ exit;
+ }
+
+ protected function sendText($str, $script = null)
+ {
+ if ($this->isPlainTextRequest()) {
+ if ($script !== null) {
+ echo $this->view->render($this->getViewScript($script, true));
+ } else {
+ echo $str;
+ }
+ exit;
+ } else {
+ $this->view->text = $str;
+ $this->view->partial = $script;
+ $this->setViewScript('format/text');
+ }
+ }
+}
diff --git a/library/Eventdb/Hook/DetailviewExtensionHook.php b/library/Eventdb/Hook/DetailviewExtensionHook.php
new file mode 100644
index 0000000..9fee702
--- /dev/null
+++ b/library/Eventdb/Hook/DetailviewExtensionHook.php
@@ -0,0 +1,124 @@
+<?php
+/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Eventdb\Hook;
+
+use Icinga\Application\ClassLoader;
+use Icinga\Application\Icinga;
+use Icinga\Application\Modules\Module;
+use Icinga\Module\Eventdb\Event;
+use Icinga\Web\View;
+
+/**
+ * Base class for hooks extending the detail view of events
+ *
+ * Extend this class if you want to extend the detail view of events with custom HTML.
+ */
+abstract class DetailviewExtensionHook
+{
+ /**
+ * The view the generated HTML will be included in
+ *
+ * @var View
+ */
+ private $view;
+
+ /**
+ * The module of the derived class
+ *
+ * @var Module
+ */
+ private $module;
+
+ /**
+ * Create a new hook
+ *
+ * @see init() For hook initialization.
+ */
+ final public function __construct()
+ {
+ $this->init();
+ }
+
+ /**
+ * Overwrite this function for hook initialization, e.g. loading the hook's config
+ */
+ protected function init()
+ {
+ }
+
+ /**
+ * Shall return valid HTML to include in the detail view
+ *
+ * @param Event $event The event to generate HTML for
+ *
+ * @return string
+ */
+ abstract public function getHtmlForEvent(Event $event);
+
+ /**
+ * Shall return valid HTML to include in the multi-select view for events
+ *
+ * @param Event[] $events The events to generate HTML for
+ *
+ * @return string
+ */
+ public function getHtmlForEvents($events)
+ {
+ return '';
+ }
+
+ /**
+ * Get {@link view}
+ *
+ * @return View
+ */
+ public function getView()
+ {
+ return $this->view;
+ }
+
+ /**
+ * Set {@link view}
+ *
+ * @param View $view
+ *
+ * @return $this
+ */
+ public function setView($view)
+ {
+ $this->view = $view;
+ return $this;
+ }
+
+ /**
+ * Get the module of the derived class
+ *
+ * @return Module
+ */
+ public function getModule()
+ {
+ if ($this->module === null) {
+ $class = get_class($this);
+ if (ClassLoader::classBelongsToModule($class)) {
+ $this->module = Icinga::app()->getModuleManager()->getModule(ClassLoader::extractModuleName($class));
+ }
+ }
+
+ return $this->module;
+ }
+
+ /**
+ * Set the module of the derived class
+ *
+ * @param Module $module
+ *
+ * @return $this
+ */
+ public function setModule(Module $module)
+ {
+ $this->module = $module;
+
+ return $this;
+ }
+}
diff --git a/library/Eventdb/ProvidedHook/Monitoring/DetailviewExtension.php b/library/Eventdb/ProvidedHook/Monitoring/DetailviewExtension.php
new file mode 100644
index 0000000..e03f401
--- /dev/null
+++ b/library/Eventdb/ProvidedHook/Monitoring/DetailviewExtension.php
@@ -0,0 +1,81 @@
+<?php
+/* Icinga Web 2 - EventDB | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Eventdb\ProvidedHook\Monitoring;
+
+use Icinga\Application\Config;
+use Icinga\Authentication\Auth;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterAnd;
+use Icinga\Module\Eventdb\Eventdb;
+use Icinga\Module\Monitoring\Hook\DetailviewExtensionHook;
+use Icinga\Module\Monitoring\Object\MonitoredObject;
+use Icinga\Web\Navigation\Navigation;
+use Icinga\Web\Url;
+
+/**
+ * Available in icingaweb2 after 2.5.0
+ */
+class DetailviewExtension extends DetailviewExtensionHook
+{
+ public function getHtmlForObject(MonitoredObject $object)
+ {
+ if (! Auth::getInstance()->hasPermission('eventdb/events')) {
+ return '';
+ }
+
+ $config = static::config();
+
+ if ($config->get('detailview_disable') === '1') {
+ return '';
+ }
+
+ $actions = clone EventdbActionHook::getActions($object);
+ if (! $actions->hasRenderableItems()) {
+ // no actions -> no EventDB
+ return '';
+ }
+
+ $htm = '<h2>EventDB</h2>';
+
+ $htm .= '<div class="quick-actions">';
+ $actions->setLayout(Navigation::LAYOUT_TABS);
+ $htm .= $actions->render();
+ $htm .= '</div>';
+
+ $url = Url::fromPath('eventdb/events', array('host_name' => $object->host_name));
+
+ $customFilter = EventdbActionHook::getCustomFilter($object);
+ if ($customFilter === null) {
+ $customFilter = new FilterAnd;
+ }
+ $detailview_filter = $config->get('detailview_filter', 'ack=0');
+ if ($detailview_filter !== null) {
+ $customFilter = $customFilter->andFilter(Filter::fromQueryString($detailview_filter));
+ }
+
+ $htm .= sprintf(
+ '<div class="container" data-last-update="-1" data-icinga-url="%s" data-icinga-refresh="60">',
+ $url->with(array(
+ 'sort' => 'priority',
+ 'dir' => 'asc',
+ 'view' => 'compact',
+ 'limit' => 5,
+ ))->addFilter($customFilter)
+ );
+ $htm .= '<p class="progress-label">' . mt('eventdb', 'Loading') . '<span>.</span><span>.</span><span>.</span></p>';
+ $htm .= '</div>';
+
+ return $htm;
+ }
+
+ protected function eventDb()
+ {
+ return Eventdb::fromConfig();
+ }
+
+ protected static function config()
+ {
+ return Config::module('eventdb')->getSection('monitoring');
+ }
+}
diff --git a/library/Eventdb/ProvidedHook/Monitoring/EventdbActionHook.php b/library/Eventdb/ProvidedHook/Monitoring/EventdbActionHook.php
new file mode 100644
index 0000000..1eb968d
--- /dev/null
+++ b/library/Eventdb/ProvidedHook/Monitoring/EventdbActionHook.php
@@ -0,0 +1,182 @@
+<?php
+/* Icinga Web 2 - EventDB | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Eventdb\ProvidedHook\Monitoring;
+
+use Icinga\Application\Config;
+use Icinga\Application\Logger;
+use Icinga\Authentication\Auth;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterParseException;
+use Icinga\Exception\InvalidPropertyException;
+use Icinga\Module\Eventdb\Data\LegacyFilterParser;
+use Icinga\Module\Monitoring\Object\Host;
+use Icinga\Module\Monitoring\Object\MonitoredObject;
+use Icinga\Module\Monitoring\Object\Service;
+use Icinga\Web\Navigation\Navigation;
+use Icinga\Web\Url;
+use Icinga\Web\UrlParams;
+
+class EventdbActionHook
+{
+ protected static $wantCache = true;
+
+ protected static $cachedNav = array();
+
+ protected static $customFilters = array();
+
+ public static function wantCache($bool=true)
+ {
+ static::$wantCache = $bool;
+ }
+
+ /**
+ * @param MonitoredObject $object
+ * @param bool $no_cache
+ *
+ * @return null|Filter
+ */
+ public static function getCustomFilter(MonitoredObject $object)
+ {
+ if (! Auth::getInstance()->hasPermission('eventdb/events')) {
+ return null;
+ }
+
+ $objectKey = static::getObjectKey($object);
+
+ // check cache if the filter already have been rendered
+ if (static::$wantCache && array_key_exists($objectKey, self::$customFilters)) {
+ return self::$customFilters[$objectKey];
+ }
+
+ $config = static::config();
+ $custom_var = $config->get('custom_var', null);
+
+ $service = null;
+ $edb_filter = null;
+ if ($custom_var !== null && $object instanceof Service) {
+ $edb_filter = $object->{'_service_' . $custom_var . '_filter'};
+ $service = $object->service_description;
+ } elseif ($custom_var !== null && $object instanceof Host) {
+ $edb_filter = $object->{'_host_' . $custom_var . '_filter'};
+ }
+
+ $customFilter = null;
+ if ($edb_filter !== null) {
+ if (LegacyFilterParser::isJsonFilter($edb_filter)) {
+ try {
+ $customFilter = LegacyFilterParser::parse(
+ $edb_filter,
+ $object->host_name,
+ $service
+ );
+ } catch (InvalidPropertyException $e) {
+ Logger::warning($e->getMessage());
+ }
+ } else {
+ try {
+ $customFilter = Filter::fromQueryString($edb_filter);
+
+ if (! in_array('host_name', $customFilter->listFilteredColumns())) {
+ $customFilter = $customFilter->andFilter(Filter::expression('host_name', '=', $object->host_name));
+ }
+ } catch (FilterParseException $e) {
+ Logger::warning('Could not parse custom EventDB filter: %s (%s)', $edb_filter, $e->getMessage());
+ }
+ }
+ }
+
+ return self::$customFilters[$objectKey] = $customFilter;
+ }
+
+ /**
+ * @param MonitoredObject $object Host or Service to render for
+ * @param bool $no_cache Only for testing - to avoid caching
+ *
+ * @return array|Navigation
+ */
+ public static function getActions(MonitoredObject $object)
+ {
+ if (! Auth::getInstance()->hasPermission('eventdb/events')) {
+ return array();
+ }
+
+ $objectKey = static::getObjectKey($object);
+
+ // check cache if the buttons already have been rendered
+ if (static::$wantCache && array_key_exists($objectKey, self::$cachedNav)) {
+ return self::$cachedNav[$objectKey];
+ }
+
+ $nav = new Navigation();
+
+ $config = static::config();
+
+ $custom_var = $config->get('custom_var', null);
+
+ $edb_cv = null;
+ $always_on = null;
+
+ if ($custom_var !== null && $object instanceof Service) {
+ $edb_cv = $object->{'_service_' . $custom_var};
+ $always_on = $config->get('always_on_service', 0);
+ } elseif ($custom_var !== null && $object instanceof Host) {
+ $edb_cv = $object->{'_host_' . $custom_var};
+ $always_on = $config->get('always_on_host', 0);
+ }
+
+ $customFilter = static::getCustomFilter($object);
+ if ($customFilter !== null) {
+ $params = UrlParams::fromQueryString($customFilter->toQueryString());
+ $nav->addItem(
+ 'events_filtered',
+ array(
+ 'label' => mt('eventdb', 'Filtered events'),
+ 'url' => Url::fromPath('eventdb/events')->setParams($params),
+ 'icon' => 'tasks',
+ 'class' => 'action-link',
+ 'priority' => 1
+ )
+ );
+ }
+
+ // show access to all events, if (or)
+ // - custom_var is not configured
+ // - always_on is configured
+ // - custom_var is configured and set on object (to any value)
+ if ($custom_var === null || ! empty($edb_cv) || ! empty($always_on)) {
+ $nav->addItem(
+ 'events',
+ array(
+ 'label' => mt('eventdb', 'All events for host'),
+ 'url' => Url::fromPath(
+ 'eventdb/events',
+ array(
+ 'host_name' => $object->host_name,
+ )
+ ),
+ 'icon' => 'tasks',
+ 'class' => 'action-link',
+ 'priority' => 99
+ )
+ );
+ }
+
+ return self::$cachedNav[$objectKey] = $nav;
+ }
+
+ protected static function getObjectKey(MonitoredObject $object)
+ {
+ $type = $object->getType();
+ $objectKey = sprintf('%!%', $type, $object->host_name);
+ if ($type === 'service') {
+ $objectKey .= '!' . $object->service_description;
+ }
+ return $objectKey;
+ }
+
+ protected static function config()
+ {
+ return Config::module('eventdb')->getSection('monitoring');
+ }
+}
diff --git a/library/Eventdb/ProvidedHook/Monitoring/HostActions.php b/library/Eventdb/ProvidedHook/Monitoring/HostActions.php
new file mode 100644
index 0000000..caf33e3
--- /dev/null
+++ b/library/Eventdb/ProvidedHook/Monitoring/HostActions.php
@@ -0,0 +1,15 @@
+<?php
+/* Icinga Web 2 - EventDB | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Eventdb\ProvidedHook\Monitoring;
+
+use Icinga\Module\Monitoring\Hook\HostActionsHook;
+use Icinga\Module\Monitoring\Object\Host;
+
+class HostActions extends HostActionsHook
+{
+ public function getActionsForHost(Host $host)
+ {
+ return EventdbActionHook::getActions($host);
+ }
+}
diff --git a/library/Eventdb/ProvidedHook/Monitoring/ServiceActions.php b/library/Eventdb/ProvidedHook/Monitoring/ServiceActions.php
new file mode 100644
index 0000000..61a7a87
--- /dev/null
+++ b/library/Eventdb/ProvidedHook/Monitoring/ServiceActions.php
@@ -0,0 +1,15 @@
+<?php
+/* Icinga Web 2 - EventDB | (c) 2017 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Eventdb\ProvidedHook\Monitoring;
+
+use Icinga\Module\Monitoring\Hook\ServiceActionsHook;
+use Icinga\Module\Monitoring\Object\Service;
+
+class ServiceActions extends ServiceActionsHook
+{
+ public function getActionsForService(Service $service)
+ {
+ return EventdbActionHook::getActions($service);
+ }
+}
diff --git a/library/Eventdb/Test/BaseTestCase.php b/library/Eventdb/Test/BaseTestCase.php
new file mode 100644
index 0000000..d24ea49
--- /dev/null
+++ b/library/Eventdb/Test/BaseTestCase.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Icinga\Module\Eventdb\Test;
+
+use Icinga\Application\Icinga;
+use PHPUnit_Framework_TestCase;
+
+abstract class BaseTestCase extends PHPUnit_Framework_TestCase
+{
+ private static $app;
+
+ private $db;
+
+ public function setUp()
+ {
+ $this->app();
+ }
+
+ protected function app()
+ {
+ if (self::$app === null) {
+ self::$app = Icinga::app();
+ }
+
+ return self::$app;
+ }
+}
diff --git a/library/Eventdb/Test/Bootstrap.php b/library/Eventdb/Test/Bootstrap.php
new file mode 100644
index 0000000..848b360
--- /dev/null
+++ b/library/Eventdb/Test/Bootstrap.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Icinga\Module\Eventdb\Test;
+
+use Icinga\Application\EmbeddedWeb;
+use Icinga\Authentication\Auth;
+use Icinga\User;
+
+class Bootstrap
+{
+ public static function web($basedir = null)
+ {
+ error_reporting(E_ALL | E_STRICT);
+ if ($basedir === null) {
+ $basedir = dirname(dirname(dirname(__DIR__)));
+ }
+ $testsDir = $basedir . '/test';
+ require_once 'Icinga/Application/EmbeddedWeb.php';
+
+ if (array_key_exists('ICINGAWEB_CONFIGDIR', $_SERVER)) {
+ $configDir = $_SERVER['ICINGAWEB_CONFIGDIR'];
+ } else {
+ $configDir = $testsDir . '/config';
+ }
+
+ EmbeddedWeb::start($testsDir, $configDir)
+ ->getModuleManager()
+ ->loadModule('eventdb', $basedir)
+ ->loadModule('monitoring', $basedir . '/vendor/icingaweb2/modules/monitoring');
+
+ $user = new User('icingaadmin');
+ $user->setPermissions(array('*'));
+ Auth::getInstance()->setAuthenticated($user);
+ }
+}
diff --git a/library/Eventdb/Test/PseudoHost.php b/library/Eventdb/Test/PseudoHost.php
new file mode 100644
index 0000000..7ebcd8c
--- /dev/null
+++ b/library/Eventdb/Test/PseudoHost.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Icinga\Module\Eventdb\Test;
+
+use Icinga\Module\Monitoring\Object\Host;
+
+class PseudoHost extends Host
+{
+ public function provideCustomVars($vars)
+ {
+ $this->properties = new \stdClass;
+ $this->hostVariables = $this->customvars = $vars;
+ return $this;
+ }
+}
diff --git a/library/Eventdb/Test/PseudoMonitoringBackend.php b/library/Eventdb/Test/PseudoMonitoringBackend.php
new file mode 100644
index 0000000..3ad6de5
--- /dev/null
+++ b/library/Eventdb/Test/PseudoMonitoringBackend.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Icinga\Module\Eventdb\Test;
+
+use Icinga\Data\ConfigObject;
+use Icinga\Module\Monitoring\Backend\MonitoringBackend;
+
+class PseudoMonitoringBackend extends MonitoringBackend
+{
+ public static function dummy()
+ {
+ return new static('dummy', new ConfigObject());
+ }
+}
diff --git a/library/Eventdb/Test/PseudoService.php b/library/Eventdb/Test/PseudoService.php
new file mode 100644
index 0000000..737a8ac
--- /dev/null
+++ b/library/Eventdb/Test/PseudoService.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace Icinga\Module\Eventdb\Test;
+
+use Icinga\Module\Monitoring\Object\Service;
+
+class PseudoService extends Service
+{
+ public function provideCustomVars($vars)
+ {
+ $this->properties = new \stdClass;
+ $this->serviceVariables = $this->customvars = $vars;
+ $this->hostVariables = array();
+ return $this;
+ }
+}
diff --git a/library/Eventdb/Web/EventdbOutputFormat.php b/library/Eventdb/Web/EventdbOutputFormat.php
new file mode 100644
index 0000000..ceec1ce
--- /dev/null
+++ b/library/Eventdb/Web/EventdbOutputFormat.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace Icinga\Module\Eventdb\Web;
+
+use Icinga\Web\Widget\Tabextension\OutputFormat;
+use Icinga\Web\Widget\Tabs;
+
+class EventdbOutputFormat extends OutputFormat
+{
+ /**
+ * TEXT output type
+ */
+ const TYPE_TEXT = 'text';
+
+ /**
+ * Types that are disabled by default
+ *
+ * @var array
+ */
+ protected static $disabledTypes = array(self::TYPE_PDF, self::TYPE_CSV);
+
+ /**
+ * Types that are enabled in addition to default
+ *
+ * @var array
+ */
+ protected $enable = array();
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct($disabled = array(), $enable = array())
+ {
+ $this->enable = $enable;
+ $disabled = array_merge(static::$disabledTypes, $disabled);
+ parent::__construct($disabled);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSupportedTypes()
+ {
+ $supported = parent::getSupportedTypes();
+
+ if (in_array(self::TYPE_TEXT, $this->enable)) {
+ $supported[self::TYPE_TEXT] = array(
+ 'name' => 'text',
+ 'label' => mt('eventdb', 'Text'),
+ 'icon' => 'doc-text',
+ 'urlParams' => array('format' => 'text'),
+ );
+ }
+
+ return $supported;
+ }
+
+ public function apply(Tabs $tabs)
+ {
+ parent::apply($tabs);
+
+ if ($textTab = $tabs->get(self::TYPE_TEXT)) {
+ $textTab->setTargetBlank(false);
+ }
+ }
+}
diff --git a/module.info b/module.info
new file mode 100644
index 0000000..4461db0
--- /dev/null
+++ b/module.info
@@ -0,0 +1,8 @@
+Module: eventdb
+Version: 1.3.0
+Description: EventDB module
+Depends: monitoring>=2.5.0
+Description: Integration to browse and acknowledge event in the EventDB
+ EventDB is a event storage and search addon to Icinga.
+
+ This module provides a simple access and integration with monitoring.
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..f9b092b
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit backupGlobals="false"
+ backupStaticAttributes="false"
+ colors="true"
+ convertErrorsToExceptions="true"
+ convertNoticesToExceptions="true"
+ convertWarningsToExceptions="true"
+ processIsolation="false"
+ stopOnFailure="false"
+ syntaxCheck="false"
+ bootstrap="test/bootstrap.php"
+ >
+ <testsuites>
+ <testsuite name="EventDB PHP Unit tests">
+ <directory suffix=".php">test/php</directory>
+ </testsuite>
+ </testsuites>
+</phpunit>
diff --git a/public/css/module.less b/public/css/module.less
new file mode 100644
index 0000000..9c86a8f
--- /dev/null
+++ b/public/css/module.less
@@ -0,0 +1,285 @@
+@color-priority-emergency: #ff2200;
+@color-priority-emergency-fg: white;
+@color-priority-alert: #ff4400;
+@color-priority-alert-fg: white;
+@color-priority-critical: #ff6600;
+@color-priority-critical-fg: white;
+@color-priority-error: #ff8800;
+@color-priority-error-fg: white;
+@color-priority-warning: #ffaa44; // same as @color-warning
+@color-priority-warning-fg: #6d572b;
+@color-priority-notice: #44bb77; // same as @color-ok;
+@color-priority-notice-fg: white;
+@color-priority-information: #aaaaff;
+@color-priority-information-fg: #32486d;
+@color-priority-debug: #ccc;
+@color-priority-debug-fg: #333;
+
+h2 {
+ margin-top: 1em;
+}
+
+.events-table, .event-summary-table {
+ border-collapse: separate;
+ border-spacing: 0 1px;
+ padding: 0;
+}
+
+.ack-col {
+ color: @icinga-blue;
+}
+
+.priority-col {
+ width: 8em;
+ border-left: 5px solid;
+ white-space: nowrap;
+ text-align: center;
+ margin-right: 1em;
+ padding: 0.33em 1em;
+
+ &.emergency {
+ border-left-color: @color-priority-emergency;
+ background-color: @color-priority-emergency;
+ color: @color-priority-emergency-fg;
+
+ &.ack {
+ color: inherit;
+ background-color: inherit;
+ }
+ }
+
+ &.critical {
+ border-left-color: @color-priority-critical;
+ background-color: @color-priority-critical;
+ color: @color-priority-critical-fg;
+
+ &.ack {
+ color: inherit;
+ background-color: inherit;
+ }
+ }
+
+ &.alert {
+ border-left-color: @color-priority-alert;
+ background-color: @color-priority-alert;
+ color: @color-priority-alert-fg;
+
+ &.ack {
+ color: inherit;
+ background-color: inherit;
+ }
+ }
+
+ &.error {
+ border-left-color: @color-priority-error;
+ background-color: @color-priority-error;
+ color: @color-priority-error-fg;
+
+ &.ack {
+ color: inherit;
+ background-color: inherit;
+ }
+ }
+
+ &.warning {
+ border-left-color: @color-priority-warning;
+ background-color: @color-priority-warning;
+ color: @color-priority-warning-fg;
+
+ &.ack {
+ color: inherit;
+ background-color: inherit;
+ }
+ }
+
+ &.notice {
+ border-left-color: @color-priority-notice;
+ }
+
+ &.information, &.info {
+ border-left-color: @color-priority-information;
+ }
+
+ &.debug {
+ border-left-color: @color-priority-debug;
+ }
+}
+
+.comment-link {
+ .button-link();
+
+ margin-top: 0.5em;
+}
+
+.quick-filter-controls {
+ margin-top: 0.5em;
+ text-align: center;
+}
+
+.ack-filter-form {
+ margin-left: 0.5em;
+}
+
+.severity-filter-form {
+ .control-label-group {
+ display: none;
+ }
+
+ .control-group {
+ display: inline-block;
+ padding: 0;
+ }
+
+ .active {
+ font-weight: bold;
+ }
+
+ .emergency.active {
+ background-color: @color-priority-emergency;
+ border-color: @color-priority-emergency;
+ color: @color-priority-emergency-fg;
+ }
+
+ .alert.active {
+ background-color: @color-priority-alert;
+ border-color: @color-priority-alert;
+ color: @color-priority-alert-fg;
+ }
+
+ .critical.active {
+ background-color: @color-priority-critical;
+ border-color: @color-priority-critical;
+ color: @color-priority-critical-fg;
+ }
+
+ .error.active {
+ background-color: @color-priority-error;
+ border-color: @color-priority-error;
+ color: @color-priority-error-fg;
+ }
+
+ .warning.active {
+ background-color: @color-priority-warning;
+ border-color: @color-priority-warning;
+ color: @color-priority-warning-fg;
+ }
+
+ .notice.active {
+ background-color: @color-priority-notice;
+ border-color: @color-priority-notice;
+ color: @color-priority-notice-fg;
+ }
+
+ .information.active, .info.active {
+ background-color: @color-priority-information;
+ border-color: @color-priority-information;
+ color: @color-priority-information-fg;
+ }
+
+ .debug.active {
+ background-color: @color-priority-debug;
+ border-color: @color-priority-debug;
+ color: @color-priority-debug-fg;
+ }
+}
+
+.events-table, .event-summary-table {
+ .event-message {
+ max-width: 60em;
+ font-family: @font-family-fixed;
+ }
+}
+
+.event-message {
+ &.detail {
+ font-size: 1.2em;
+ border-left: 5px solid #eee;
+ padding: 0.66em 0.33em;
+ }
+
+ a {
+ font-weight: bold;
+ }
+}
+
+.event-summary-table {
+ .event-host_address {
+ font-family: @font-family-fixed;
+ font-size: 0.9em;
+ color: @gray;
+ }
+}
+
+a.action-link {
+ color: @icinga-blue !important;
+ margin-right: 1em;
+}
+
+.name-value-table {
+ th {
+ width: 8em;
+ }
+}
+
+.comments-table {
+ width: auto;
+
+ tr {
+ border-bottom: none;
+ }
+
+ td {
+ padding: 0.5em 0.5em;
+ }
+
+ .comment-created {
+ white-space: nowrap;
+ }
+ .comment-type {
+ max-width: 2em;
+ }
+ .comment-message {
+ min-width: 30em;
+ font-family: @font-family-fixed;
+ }
+}
+
+.comment-form {
+ select {
+ width: auto;
+ }
+
+ input[name=comment] {
+ width: 25em;
+ }
+
+ input[type=submit] {
+ padding: 0.2em 0.5em;
+ }
+}
+
+div.warning {
+ display: inline-block;
+ font-size: 1.2em;
+ font-weight: bold;
+ border: 1px solid @color-priority-warning;
+ border-left: 5px solid @color-priority-warning;
+ margin: 0.5em 0;
+ padding: 0.3em 0.5em;
+}
+
+.copyable-actions {
+ margin-bottom: 0.5em;
+}
+
+input[type=submit].disabled,
+button.disabled {
+ border-color: @gray-light !important;
+ background-color: white !important;
+ color: @gray-light !important;
+
+ &:hover {
+ background: @gray-light !important;
+ color: white;
+ }
+}
diff --git a/public/js/module.js b/public/js/module.js
new file mode 100644
index 0000000..0678421
--- /dev/null
+++ b/public/js/module.js
@@ -0,0 +1,97 @@
+/*! Icinga Web 2 | (c) 2016 Icinga Development Team | GPLv2+ */
+
+;(function(Icinga) {
+
+ var EventDB = function(module) {
+ this.module = module;
+ this.initialize();
+ };
+
+ EventDB.prototype = {
+ initialize: function() {
+ this.module.on('rendered', this.enableCopyable);
+ this.module.on('submit', 'form.severity-filter-form', this.severitySubmit);
+
+ var addCSSRule = function(sheet, selector, rules, index) {
+ if('insertRule' in sheet) {
+ sheet.insertRule(selector + '{' + rules + '}', index);
+ } else if('addRule' in sheet) {
+ sheet.addRule(selector, rules, index);
+ } else {
+ this.module.icinga.logger.debug('Can\'t insert CSS rule');
+ }
+ };
+
+ var sheet = (function() {
+ var style = document.createElement('style');
+ // WebKit hack
+ style.appendChild(document.createTextNode(''));
+ document.head.appendChild(style);
+ return style.sheet;
+ })();
+
+ addCSSRule(
+ sheet,
+ '#layout.twocols.wide-layout #col1.module-eventdb, #layout.twocols.wide-layout #col1.module-eventdb ~ #col2',
+ 'width: 50%',
+ 0
+ );
+ },
+ enableCopyable: function() {
+ var e = this;
+ $('.copyable').each(function() {
+ var $button = $('<a>')
+ .attr('href', '#')
+ .addClass('action-link icon icon-globe copyable-button')
+ .text('Copy text');
+
+ $button.on('click', function() {
+ var $el = $(this).parent().siblings('.copyable');
+ if ($el) {
+ e.selectText($el[0]);
+ document.execCommand('copy');
+ setTimeout(function () {
+ e.clearSelection()
+ }, 500);
+ if (icinga) {
+ icinga.loader.createNotice('info', 'Text copied to clipboard')
+ }
+ }
+ });
+
+ var $div = $('<div>').addClass('copyable-actions').append($button);
+
+ $(this).before($div);
+ });
+ },
+ selectText: function (text) {
+ var doc = document, range, selection;
+ if (doc.body.createTextRange) {
+ range = document.body.createTextRange();
+ range.moveToElementText(text);
+ range.select();
+ } else if (window.getSelection) {
+ selection = window.getSelection();
+ range = document.createRange();
+ range.selectNodeContents(text);
+ selection.removeAllRanges();
+ selection.addRange(range);
+ }
+ },
+ clearSelection: function() {
+ if (document.selection) {
+ document.selection.empty();
+ } else if (window.getSelection) {
+ window.getSelection().removeAllRanges();
+ }
+ },
+ severitySubmit: function(ev) {
+ $(ev.currentTarget)
+ .find('input[type=submit]')
+ .prop('disabled', true)
+ .addClass('disabled');
+ }
+ };
+
+ Icinga.availableModules.eventdb = EventDB;
+}(Icinga));
diff --git a/run.php b/run.php
new file mode 100644
index 0000000..322b34b
--- /dev/null
+++ b/run.php
@@ -0,0 +1,9 @@
+<?php
+
+use Icinga\Application\Modules\Module;
+
+/** @var Module $this */
+$this->provideHook('monitoring/HostActions');
+$this->provideHook('monitoring/ServiceActions');
+
+$this->provideHook('monitoring/DetailviewExtension');
diff --git a/test/bootstrap.php b/test/bootstrap.php
new file mode 100644
index 0000000..0253904
--- /dev/null
+++ b/test/bootstrap.php
@@ -0,0 +1,16 @@
+<?php
+
+use Icinga\Module\Eventdb\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/Eventdb/Test/Bootstrap.php';
+ Bootstrap::web($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/resources.ini b/test/config/resources.ini
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/config/resources.ini
diff --git a/test/php/library/Eventdb/Data/LegacyFilterParserTest.php b/test/php/library/Eventdb/Data/LegacyFilterParserTest.php
new file mode 100644
index 0000000..51b34ab
--- /dev/null
+++ b/test/php/library/Eventdb/Data/LegacyFilterParserTest.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace Tests\Icinga\Module\Eventdb\CustomVariable;
+
+use Icinga\Module\Eventdb\Data\LegacyFilterParser;
+use Icinga\Module\Eventdb\Test\BaseTestCase;
+
+class LegacyFilterParserTest extends BaseTestCase
+{
+ /**
+ * Some filter examples that should work and match the result
+ *
+ * Note: This is not always clean JSON, but it should work!
+ *
+ * @var array
+ */
+ protected $validFilters = array(
+ '{}' => 'host_name=testhost', // default filter
+ '{ host: "test" }' => 'host_name=test',
+ "{ host: 'test' }" => 'host_name=test',
+ "{ host: 'otherhostname' }" => 'host_name=otherhostname',
+ "{ host: 'specialhostname', priorityExclusion: [] }" => 'host_name=specialhostname',
+ "{ host: 'specialhostname', priorityExclusion: [6,7,8] }" => 'host_name=specialhostname&priority!=6&priority!=7&priority!=8',
+ '{ "host": "*" }' => '', // doesn't make much sense, but well...
+ '{ "host": "*", "programInclusion": ["cloud-monitoring"] }' => 'program=cloud-monitoring',
+ '{ "host": ".*", "programInclusion": ["cloud-monitoring"] }' => 'program=cloud-monitoring',
+ '{ "host": "myhost.*.example.com" }' => 'host_name=myhost%2A.example.com',
+ "{ programInclusion: ['test1', 'test2'] }" => 'host_name=testhost&(program=test1|program=test2)',
+ "{ programInclusion: ['test'] }" => 'host_name=testhost&program=test',
+ "{ programExclusion: ['test'] }" => 'host_name=testhost&program!=test',
+ "{ programExclusion: ['test1', 'test2'] }" => 'host_name=testhost&program!=test1&program!=test2',
+ );
+
+ public function testFiltersThatContainSomeJson()
+ {
+ $filters = array(
+ ' { host: "test" } ',
+ ' {} ',
+ '{}',
+ "{\n\"multiline\": 1\n}",
+ );
+ foreach ($filters as $filter) {
+ $this->assertTrue(LegacyFilterParser::isJsonFilter($filter));
+ }
+ }
+
+ public function testFiltersThatDoNotContainJson()
+ {
+ $filters = array(
+ ' {xxxx ',
+ 1337,
+ 'sometext',
+ "{\nbrokenjson\n",
+ );
+ foreach ($filters as $filter) {
+ $this->assertFalse(LegacyFilterParser::isJsonFilter($filter), 'Filter: ' . $filter);
+ }
+
+ }
+
+ public function testParsingFilters()
+ {
+ foreach ($this->validFilters as $json => $result) {
+ $this->assertTrue(
+ LegacyFilterParser::isJsonFilter($json),
+ 'Should be recognized as JSON filter by isJsonFilter'
+ );
+
+ $filter = LegacyFilterParser::parse($json, 'testhost');
+ $this->assertEquals(
+ $result,
+ $filter->toQueryString(),
+ 'Resulting URL filter should match for json: ' . $json
+ );
+ }
+ }
+}
diff --git a/test/php/library/Eventdb/ProvidedHook/Monitoring/EventdbActionHookTest.php b/test/php/library/Eventdb/ProvidedHook/Monitoring/EventdbActionHookTest.php
new file mode 100644
index 0000000..d833d73
--- /dev/null
+++ b/test/php/library/Eventdb/ProvidedHook/Monitoring/EventdbActionHookTest.php
@@ -0,0 +1,186 @@
+<?php
+
+namespace Tests\Icinga\Module\Eventdb\ProvidedHook\Monitoring;
+
+use Icinga\Application\Config;
+use Icinga\Module\Eventdb\ProvidedHook\Monitoring\EventdbActionHook;
+use Icinga\Module\Eventdb\Test\BaseTestCase;
+use Icinga\Module\Eventdb\Test\PseudoHost;
+use Icinga\Module\Eventdb\Test\PseudoMonitoringBackend;
+use Icinga\Module\Eventdb\Test\PseudoService;
+use Icinga\Web\Navigation\NavigationItem;
+
+class EventdbActionHookTest extends BaseTestCase
+{
+ public function setUp()
+ {
+ parent::setUp();
+ EventdbActionHook::wantCache(false);
+ }
+
+ public function testHostWithoutVarsAndNoConfig()
+ {
+ $this->setupConfiguration(null, null, null);
+
+ $nav = EventdbActionHook::getActions($this->buildHost(null, null));
+
+ $items = $nav->getItems();
+ $this->assertCount(1, $items);
+
+ /** @var NavigationItem $navObj */
+ $navObj = current($items);
+
+ $this->assertEquals('host_name=testhost', $navObj->getUrl()->getQueryString());
+ }
+
+ public function testHostWithoutVarsAndNormalConfig()
+ {
+ $this->setupConfiguration();
+
+ $nav = EventdbActionHook::getActions($this->buildHost(null, null));
+
+ $this->assertCount(0, $nav->getItems());
+ }
+
+ public function testHostWithVars()
+ {
+ $this->setupConfiguration();
+
+ $nav = EventdbActionHook::getActions($this->buildHost());
+
+ $items = $nav->getItems();
+ $this->assertCount(1, $items);
+
+ /** @var NavigationItem $navObj */
+ $navObj = current($items);
+
+ $this->assertEquals('host_name=testhost', $navObj->getUrl()->getQueryString());
+ }
+
+ public function testHostWithVarsAlwaysOn()
+ {
+ $this->setupConfiguration('edb', '1');
+
+ $nav = EventdbActionHook::getActions($this->buildHost(null, 'othervar'));
+
+ $this->assertCount(1, $nav->getItems());
+ }
+
+ public function testServiceWithVarsAlwaysOn()
+ {
+ $this->setupConfiguration('edb', null, '1');
+
+ $nav = EventdbActionHook::getActions($this->buildService(null, 'othervar'));
+
+ $this->assertCount(1, $nav->getItems());
+ }
+
+ public function testHostWithLegacyFilter()
+ {
+ $this->setupConfiguration();
+
+ $nav = EventdbActionHook::getActions($this->buildHost("{ host: 'test2', programInclusion: 'test2' }"));
+
+ $items = $nav->getItems();
+ $this->assertCount(2, $items);
+
+ /** @var NavigationItem $navObj */
+ $navObj = current($items);
+
+ $this->assertEquals('host_name=test2&program=test2', $navObj->getUrl()->getQueryString());
+ }
+
+ public function testHostWithFilter()
+ {
+ $this->setupConfiguration();
+
+ $nav = EventdbActionHook::getActions($this->buildHost("program=test3"));
+
+ $items = $nav->getItems();
+ $this->assertCount(2, $items);
+
+ /** @var NavigationItem $navObj */
+ $navObj = current($items);
+
+ $this->assertEquals("program=test3&host_name=testhost", $navObj->getUrl()->getQueryString());
+ }
+
+ public function testHostWithFilterThatFiltersHost()
+ {
+ $this->setupConfiguration();
+
+ $nav = EventdbActionHook::getActions($this->buildHost("host_name=test3&program=test3"));
+
+ $items = $nav->getItems();
+ $this->assertCount(2, $items);
+
+ /** @var NavigationItem $navObj */
+ $navObj = current($items);
+
+ $this->assertEquals("host_name=test3&program=test3", $navObj->getUrl()->getQueryString());
+ }
+
+ protected function configure($settings = array())
+ {
+ $config = Config::module('eventdb');
+ $section = $config->getSection('monitoring');
+ foreach ($settings as $key => $val) {
+ $section->$key = $val;
+ }
+ $config->setSection('monitoring', $section);
+
+ // NOTE: we need to save here, because Config::module always load config from disk
+ $config->saveIni();
+
+ return $this;
+ }
+
+ protected function setupConfiguration($custom_var = 'edb', $always_host = null, $always_service = null)
+ {
+ $this->configure(array(
+ 'custom_var' => $custom_var,
+ 'always_on_host' => $always_host,
+ 'always_on_service' => $always_service,
+ ));
+ }
+
+ protected function monitoringBackend()
+ {
+ return PseudoMonitoringBackend::dummy();
+ }
+
+ protected function buildHost($plainFilter = null, $custom_var = 'edb')
+ {
+ $host = new PseudoHost($this->monitoringBackend(), 'testhost');
+ $host->host_name = 'testhost';
+
+ $vars = array();
+ if ($custom_var !== null) {
+ $vars[$custom_var] = '1';
+ }
+ if ($plainFilter !== null) {
+ $vars[$custom_var . '_filter'] = $plainFilter;
+ }
+ $host->provideCustomVars($vars);
+
+ return $host;
+ }
+
+ protected function buildService($plainFilter = null, $custom_var = 'edb')
+ {
+ $service = new PseudoService($this->monitoringBackend(), 'testhost', 'test');
+ $service->host_name = 'testhost';
+ $service->service_description = 'testhost';
+
+ $vars = array();
+ if ($custom_var !== null) {
+ $vars[$custom_var] = '1';
+ }
+ if ($plainFilter !== null) {
+ $vars[$custom_var . '_filter'] = $plainFilter;
+ }
+ $service->provideCustomVars($vars);
+
+ return $service;
+ }
+}
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..742dfcd
--- /dev/null
+++ b/test/setup_vendor.sh
@@ -0,0 +1,69 @@
+#!/bin/bash
+
+set -ex
+
+MODULE_HOME=${MODULE_HOME:="$(dirname "$(readlink -f $(dirname "$0"))")"}
+PHP_VERSION="$(php -r 'echo phpversion();')"
+
+ICINGAWEB_VERSION=${ICINGAWEB_VERSION:=2.4.1}
+# TODO: Remove for 2.5.0
+if [[ "$ICINGAWEB_VERSION" =~ 2.4.* ]]; then
+ ICINGAWEB_GITREF=${ICINGAWEB_GITREF:="origin/master"}
+fi
+
+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
+
+ ln -svf "${icingaweb_path}" icingaweb2
+fi
+ln -svf "${icingaweb_path}"/library/Icinga
+ln -svf "${icingaweb_path}"/library/vendor/Zend
+
+# 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