├── .mailmap ├── AUTHORS ├── LICENSE ├── README.md ├── application ├── clicommands │ ├── DownloadCommand.php │ ├── ListCommand.php │ └── ScheduleCommand.php ├── controllers │ ├── ConfigController.php │ ├── ReportController.php │ ├── ReportsController.php │ ├── TemplateController.php │ ├── TemplatesController.php │ ├── TimeframeController.php │ └── TimeframesController.php ├── forms │ ├── ConfigureMailForm.php │ └── SelectBackendForm.php └── views │ └── scripts │ └── config │ ├── backend.phtml │ └── mail.phtml ├── config └── systemd │ └── icinga-reporting.service ├── configuration.php ├── doc ├── 02-Installation.md ├── 03-Configuration.md └── 80-Upgrading.md ├── library └── Reporting │ ├── Actions │ └── SendMail.php │ ├── Cli │ └── Command.php │ ├── Common │ └── Macros.php │ ├── Database.php │ ├── Dimensions.php │ ├── Hook │ ├── ActionHook.php │ └── ReportHook.php │ ├── Mail.php │ ├── Model │ ├── Config.php │ ├── Report.php │ ├── Reportlet.php │ ├── Schedule.php │ ├── Schema.php │ ├── Template.php │ └── Timeframe.php │ ├── ProvidedActions.php │ ├── ProvidedHook │ └── DbMigration.php │ ├── ProvidedReports.php │ ├── Report.php │ ├── ReportData.php │ ├── ReportRow.php │ ├── Reportlet.php │ ├── Reports │ └── SystemReport.php │ ├── RetryConnection.php │ ├── Schedule.php │ ├── Str.php │ ├── Timeframe.php │ ├── Timerange.php │ ├── Values.php │ └── Web │ ├── Controller.php │ ├── Forms │ ├── ReportForm.php │ ├── ScheduleForm.php │ ├── SendForm.php │ ├── TemplateForm.php │ └── TimeframeForm.php │ ├── ReportsTimeframesAndTemplatesTabs.php │ └── Widget │ ├── CompatDropdown.php │ ├── CoverPage.php │ ├── HeaderOrFooter.php │ └── Template.php ├── module.info ├── phpcs.xml ├── phpstan-baseline.neon ├── phpstan.neon ├── public ├── css │ ├── module.less │ └── system-report.css └── img │ ├── select-icon-2x.png │ ├── select-icon.png │ ├── textarea-corner-2x.png │ └── textarea-corner.png ├── run.php └── schema ├── mysql-upgrades ├── 0.10.0.sql ├── 0.9.1.sql ├── 1.0.0.sql └── 1.0.3.sql ├── mysql.schema.sql ├── pgsql-upgrades ├── 1.0.0.sql └── 1.0.3.sql └── pgsql.schema.sql /.mailmap: -------------------------------------------------------------------------------- 1 | Damiano Chini 2 | Dirk Götz 3 | Eric Lippmann 4 | Florian Rosenegger 5 | J. Nathanael Philipp 6 | Johannes Meyer 7 | Jonada Hoxha 8 | Mathieu Lu 9 | Michael Friedrich 10 | Nicolai Buchwitz 11 | Ravi Kumar Kempapura Srinivasa 12 | Sukhwinder Dhillon 13 | Timm Ortloff 14 | Valentina Da Rold 15 | Yonas Habteab 16 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Damiano Chini 2 | Dirk Götz 3 | Eric Lippmann 4 | Florian Rosenegger 5 | J. Nathanael Philipp 6 | Johannes Meyer 7 | Jonada Hoxha 8 | MAJ 9 | Mathieu Lu 10 | Michael Friedrich 11 | Nicolai Buchwitz 12 | Ravi Kumar Kempapura Srinivasa 13 | Sukhwinder Dhillon 14 | Timm Ortloff 15 | Valentina Da Rold 16 | Yonas Habteab 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Icinga Reporting 2 | 3 | [![PHP Support](https://img.shields.io/badge/php-%3E%3D%207.2-777BB4?logo=PHP)](https://php.net/) 4 | [![PHP Tests](https://github.com/Icinga/icingaweb2-module-reporting/actions/workflows/php.yml/badge.svg)](https://github.com/Icinga/icingaweb2-module-reporting/actions/workflows/php.yml) 5 | [![Github Tag](https://img.shields.io/github/tag/Icinga/icingaweb2-module-reporting.svg)](https://github.com/Icinga/icingaweb2-module-reporting/releases/latest) 6 | 7 | ![Icinga Logo](https://icinga.com/wp-content/uploads/2014/06/icinga_logo.png) 8 | 9 | Icinga Reporting is the central component for reporting related functionality in the monitoring web frontend and 10 | framework Icinga Web 2. The engine allows you to create reports over a specified time period for ad-hoc and scheduled 11 | generation of reports. Other modules use the provided functionality in order to provide concrete reports. 12 | 13 | ## Host/Service SLA Reports 14 | 15 | With Icinga DB Web there is no additional module required. 16 | 17 | If you are still using the monitoring module, please also install the 18 | [idoreports](https://github.com/Icinga/icingaweb2-module-idoreports) module. 19 | 20 | ## Documentation 21 | 22 | * [Installation](doc/02-Installation.md) 23 | -------------------------------------------------------------------------------- /application/clicommands/DownloadCommand.php: -------------------------------------------------------------------------------- 1 | [--format=] 24 | * 25 | * OPTIONS 26 | * 27 | * --format= 28 | * Download report as PDF, CSV or JSON. Defaults to pdf. 29 | * 30 | * --output= 31 | * Save report to the specified . 32 | * 33 | * EXAMPLES 34 | * 35 | * Download report with ID 1: 36 | * icingacli reporting download 1 37 | * 38 | * Download report with ID 1 as CSV: 39 | * icingacli reporting download 1 --format=csv 40 | * 41 | * Download report with ID 1 as JSON to the specified file: 42 | * icingacli reporting download 1 --format=json --output=sla.json 43 | */ 44 | public function defaultAction() 45 | { 46 | $id = $this->params->getStandalone(); 47 | if ($id === null) { 48 | $this->fail($this->translate('Argument id is mandatory')); 49 | } 50 | 51 | /** @var Model\Report $report */ 52 | $report = Model\Report::on(Database::get()) 53 | ->with('timeframe') 54 | ->filter(Filter::equal('id', $id)) 55 | ->first(); 56 | 57 | if ($report === null) { 58 | throw new NotFoundError('Report not found'); 59 | } 60 | 61 | $report = Report::fromModel($report); 62 | 63 | /** @var string $format */ 64 | $format = $this->params->get('format', 'pdf'); 65 | $format = strtolower($format); 66 | switch ($format) { 67 | case 'pdf': 68 | $content = Pdfexport::first()->htmlToPdf($report->toPdf()); 69 | break; 70 | case 'csv': 71 | $content = $report->toCsv(); 72 | break; 73 | case 'json': 74 | $content = $report->toJson(); 75 | break; 76 | default: 77 | throw new InvalidArgumentException(sprintf('Format %s is not supported', $format)); 78 | } 79 | 80 | /** @var string $output */ 81 | $output = $this->params->get('output'); 82 | if ($output === null) { 83 | $name = sprintf( 84 | '%s (%s) %s', 85 | $report->getName(), 86 | $report->getTimeframe()->getName(), 87 | date('Y-m-d H:i') 88 | ); 89 | 90 | $output = "$name.$format"; 91 | } elseif (is_dir($output)) { 92 | $this->fail($this->translate(sprintf('%s is a directory', $output))); 93 | } 94 | 95 | file_put_contents($output, $content); 96 | echo "$output\n"; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /application/clicommands/ListCommand.php: -------------------------------------------------------------------------------- 1 | 25 | * Sort the reports by the given column. Defaults to id. 26 | * 27 | * --direction= 28 | * Sort the reports by the specified sort column in ascending or descending order. Defaults to asc. 29 | * 30 | * --filter= 31 | * Filter the reports by the specified report name. Performs a wildcard search by default. 32 | * 33 | * EXAMPLES 34 | * 35 | * Sort the reports by name: 36 | * icingacli reporting list --sort=name 37 | * 38 | * Sort the reports by author in descending order: 39 | * icingacli reporting list --sort=author --direction=DESC 40 | * 41 | * Filter the reports that contain "Host" in the report name: 42 | * icingacli reporting list --filter=Host 43 | * 44 | * Filter the reports that begin with "Service": 45 | * icingacli reporting list --filter=Service* 46 | * 47 | * Filter the reports that end with "SLA": 48 | * icingacli reporting list --filter=*SLA 49 | */ 50 | public function indexAction() 51 | { 52 | /** @var string $sort */ 53 | $sort = $this->params->get('sort', 'id'); 54 | $sort = strtolower($sort); 55 | 56 | if ($sort !== 'id' && $sort !== 'name' && $sort !== 'author') { 57 | throw new InvalidArgumentException(sprintf('Sorting by %s is not supported', $sort)); 58 | } 59 | 60 | $direction = $this->params->get('direction', 'ASC'); 61 | 62 | $reports = Model\Report::on(Database::get()); 63 | $reports 64 | ->with(['reportlets']) 65 | ->orderBy($sort, $direction); 66 | 67 | $filter = $this->params->get('filter'); 68 | if ($filter !== null) { 69 | if (strpos($filter, '*') === false) { 70 | $filter = '*' . $filter . '*'; 71 | } 72 | $reports->filter(Filter::like('name', $filter)); 73 | } 74 | 75 | if ($reports->count() === 0) { 76 | print $this->translate("No reports found\n"); 77 | exit; 78 | } 79 | 80 | $dataCallbacks = [ 81 | 'ID' => function ($report) { 82 | return $report->id; 83 | }, 84 | 'Name' => function ($report) { 85 | return $report->name; 86 | }, 87 | 'Author' => function ($report) { 88 | return $report->author; 89 | }, 90 | 'Type' => function ($report) { 91 | return (new $report->reportlets->class())->getName(); 92 | } 93 | ]; 94 | 95 | $this->outputTable($reports, $dataCallbacks); 96 | } 97 | 98 | protected function outputTable($reports, array $dataCallbacks) 99 | { 100 | $columnsAndLengths = []; 101 | foreach ($dataCallbacks as $key => $_) { 102 | $columnsAndLengths[$key] = strlen($key); 103 | } 104 | 105 | $rows = []; 106 | foreach ($reports as $report) { 107 | $row = []; 108 | foreach ($dataCallbacks as $key => $callback) { 109 | $row[] = $callback($report); 110 | $columnsAndLengths[$key] = max($columnsAndLengths[$key], mb_strlen($callback($report))); 111 | } 112 | 113 | $rows[] = $row; 114 | } 115 | 116 | $format = '|'; 117 | $beautifier = '|'; 118 | foreach ($columnsAndLengths as $length) { 119 | $headerFormat = " %-" . sprintf('%ss |', $length); 120 | $format .= $headerFormat; 121 | $beautifier .= sprintf($headerFormat, str_repeat('-', $length)); 122 | } 123 | $format .= "\n"; 124 | $beautifier .= "\n"; 125 | 126 | printf($format, ...array_keys($columnsAndLengths)); 127 | print $beautifier; 128 | 129 | foreach ($rows as $row) { 130 | printf($format, ...$row); 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /application/clicommands/ScheduleCommand.php: -------------------------------------------------------------------------------- 1 | attachJobsLogging($scheduler); 37 | 38 | /** @var Schedule[] $runningSchedules */ 39 | $runningSchedules = []; 40 | // Check for configuration changes every 5 minutes to make sure new jobs are scheduled, updated and deleted 41 | // jobs are cancelled. 42 | $watchdog = function () use (&$watchdog, $scheduler, &$runningSchedules) { 43 | $schedules = []; 44 | try { 45 | // Since this is a long-running daemon, the resources or module config may change meanwhile. 46 | // Therefore, reload the resources and module config from disk each time (at 5m intervals) 47 | // before reconnecting to the database. 48 | ResourceFactory::setConfig(Config::app('resources', true)); 49 | Config::module('reporting', 'config', true); 50 | 51 | $schedules = $this->fetchSchedules(); 52 | } catch (Throwable $err) { 53 | Logger::error('Failed to fetch report schedules from the database: %s', $err); 54 | Logger::debug($err->getTraceAsString()); 55 | } 56 | 57 | $outdated = array_diff_key($runningSchedules, $schedules); 58 | foreach ($outdated as $schedule) { 59 | Logger::info( 60 | 'Removing %s as it either no longer exists in the database or its config has been changed', 61 | $schedule->getName() 62 | ); 63 | 64 | $scheduler->remove($schedule); 65 | 66 | unset($runningSchedules[$schedule->getUuid()->toString()]); 67 | } 68 | 69 | $newSchedules = array_diff_key($schedules, $runningSchedules); 70 | foreach ($newSchedules as $key => $schedule) { 71 | $config = $schedule->getConfig(); 72 | $frequency = $config['frequency']; 73 | 74 | try { 75 | /** @var Frequency $type */ 76 | $type = $config['frequencyType']; 77 | $frequency = $type::fromJson($frequency); 78 | } catch (Exception $err) { 79 | Logger::error( 80 | '%s has invalid schedule expression %s: %s', 81 | $schedule->getName(), 82 | $frequency, 83 | $err->getMessage() 84 | ); 85 | 86 | continue; 87 | } 88 | 89 | $scheduler->schedule($schedule, $frequency); 90 | 91 | $runningSchedules[$key] = $schedule; 92 | } 93 | 94 | Loop::addTimer(5 * 60, $watchdog); 95 | }; 96 | Loop::futureTick($watchdog); 97 | } 98 | 99 | /** 100 | * Fetch schedules from the database 101 | * 102 | * @return Schedule[] 103 | */ 104 | protected function fetchSchedules(): array 105 | { 106 | $schedules = []; 107 | $query = Model\Schedule::on(Database::get())->with(['report.timeframe', 'report']); 108 | 109 | foreach ($query as $schedule) { 110 | $schedule = Schedule::fromModel($schedule, Report::fromModel($schedule->report)); 111 | $schedules[$schedule->getUuid()->toString()] = $schedule; 112 | } 113 | 114 | return $schedules; 115 | } 116 | 117 | protected function attachJobsLogging(Scheduler $scheduler) 118 | { 119 | $scheduler->on(Scheduler::ON_TASK_FAILED, function (Task $job, Throwable $e) { 120 | Logger::error('Failed to run job %s: %s', $job->getName(), $e->getMessage()); 121 | Logger::debug($e->getTraceAsString()); 122 | }); 123 | 124 | $scheduler->on(Scheduler::ON_TASK_RUN, function (Task $job, ExtendedPromiseInterface $_) { 125 | Logger::info('Running job %s', $job->getName()); 126 | }); 127 | 128 | $scheduler->on(Scheduler::ON_TASK_SCHEDULED, function (Task $job, DateTime $dateTime) { 129 | Logger::info('Scheduling job %s to run at %s', $job->getName(), $dateTime->format('Y-m-d H:i:s')); 130 | }); 131 | 132 | $scheduler->on(Scheduler::ON_TASK_EXPIRED, function (Task $task, DateTime $dateTime) { 133 | Logger::info( 134 | sprintf('Detaching expired job %s at %s', $task->getName(), $dateTime->format('Y-m-d H:i:s')) 135 | ); 136 | }); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /application/controllers/ConfigController.php: -------------------------------------------------------------------------------- 1 | assertPermission('config/modules'); 17 | 18 | parent::init(); 19 | } 20 | 21 | public function backendAction(): void 22 | { 23 | $form = (new SelectBackendForm()) 24 | ->setIniConfig(Config::module('reporting')); 25 | 26 | $form->handleRequest(); 27 | 28 | $this->view->tabs = $this->Module()->getConfigTabs()->activate('backend'); 29 | $this->view->form = $form; 30 | } 31 | 32 | public function mailAction(): void 33 | { 34 | $form = (new ConfigureMailForm()) 35 | ->setIniConfig(Config::module('reporting')); 36 | 37 | $form->handleRequest(); 38 | 39 | $this->view->tabs = $this->Module()->getConfigTabs()->activate('mail'); 40 | $this->view->form = $form; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /application/controllers/ReportController.php: -------------------------------------------------------------------------------- 1 | params->getRequired('id'); 35 | 36 | /** @var Model\Report $report */ 37 | $report = Model\Report::on(Database::get()) 38 | ->with(['timeframe']) 39 | ->filter(Filter::equal('id', $reportId)) 40 | ->first(); 41 | 42 | if ($report === null) { 43 | $this->httpNotFound($this->translate('Report not found')); 44 | } 45 | 46 | $this->report = Report::fromModel($report); 47 | } 48 | 49 | public function indexAction(): void 50 | { 51 | $this->addTitleTab($this->report->getName()); 52 | 53 | $this->controls->getAttributes()->add('class', 'default-layout'); 54 | $this->addControl($this->assembleActions()); 55 | 56 | if ($this->isXhr()) { 57 | /** @var string $contentId */ 58 | $contentId = $this->content->getAttributes()->get('id')->getValue(); 59 | $this->sendExtraUpdates([ 60 | $contentId => Url::fromPath('reporting/report/content', ['id' => $this->report->getId()]) 61 | ]); 62 | 63 | // Will be replaced once the report content is rendered 64 | $this->addContent(new HtmlElement('div')); 65 | } else { 66 | Environment::raiseExecutionTime(); 67 | Environment::raiseMemoryLimit(); 68 | 69 | try { 70 | $this->addContent($this->report->toHtml()); 71 | } catch (Exception $e) { 72 | $this->addContent(Error::show($e)); 73 | } 74 | } 75 | } 76 | 77 | public function contentAction(): void 78 | { 79 | Environment::raiseExecutionTime(); 80 | Environment::raiseMemoryLimit(); 81 | 82 | $this->view->compact = true; 83 | $this->_helper->layout()->disableLayout(); 84 | 85 | try { 86 | $this->getDocument()->addHtml($this->report->toHtml()); 87 | } catch (Exception $e) { 88 | $this->getDocument()->addHtml(Error::show($e)); 89 | } 90 | } 91 | 92 | public function cloneAction(): void 93 | { 94 | $this->assertPermission('reporting/reports'); 95 | $this->addTitleTab($this->translate('Clone Report')); 96 | 97 | $values = ['timeframe' => (string) $this->report->getTimeframe()->getId()]; 98 | 99 | $reportlet = $this->report->getReportlets()[0]; 100 | 101 | $values['reportlet'] = $reportlet->getClass(); 102 | 103 | foreach ($reportlet->getConfig() as $name => $value) { 104 | if ($name === 'name') { 105 | if (preg_match('/(?:Clone )(\d+)$/', $value, $m)) { 106 | $value = preg_replace('/\d+$/', (string) ((int) $m[1] + 1), $value); 107 | } else { 108 | $value .= ' Clone 1'; 109 | } 110 | } 111 | 112 | $values[$name] = $value; 113 | } 114 | 115 | $form = (new ReportForm(Database::get())) 116 | ->setSubmitButtonLabel($this->translate('Clone Report')) 117 | ->setAction((string) Url::fromRequest()) 118 | ->populate($values) 119 | ->on(ReportForm::ON_SUCCESS, function (ReportForm $form) { 120 | Notification::success($this->translate('Cloned report successfully')); 121 | 122 | $this->sendExtraUpdates(['#col1']); 123 | 124 | $this->redirectNow(Url::fromPath('reporting/report', ['id' => $form->getId()])); 125 | }) 126 | ->handleRequest($this->getServerRequest()); 127 | 128 | $this->addContent($form); 129 | } 130 | 131 | public function editAction(): void 132 | { 133 | $this->assertPermission('reporting/reports'); 134 | $this->addTitleTab($this->translate('Edit Report')); 135 | 136 | $values = [ 137 | 'name' => $this->report->getName(), 138 | // TODO(el): Must cast to string here because ipl/html does not 139 | // support integer return values for attribute callbacks 140 | 'timeframe' => (string) $this->report->getTimeframe()->getId(), 141 | ]; 142 | 143 | $reportlet = $this->report->getReportlets()[0]; 144 | 145 | $values['reportlet'] = $reportlet->getClass(); 146 | 147 | foreach ($reportlet->getConfig() as $name => $value) { 148 | $values[$name] = $value; 149 | } 150 | 151 | $form = ReportForm::fromId($this->report->getId(), Database::get()) 152 | ->setAction((string) Url::fromRequest()) 153 | ->populate($values) 154 | ->on(ReportForm::ON_SUCCESS, function (ReportForm $form) { 155 | $pressedButton = $form->getPressedSubmitElement(); 156 | if ($pressedButton && $pressedButton->getName() === 'remove') { 157 | Notification::success($this->translate('Removed report successfully')); 158 | 159 | $this->switchToSingleColumnLayout(); 160 | } else { 161 | Notification::success($this->translate('Updated report successfully')); 162 | 163 | $this->closeModalAndRefreshRemainingViews( 164 | Url::fromPath('reporting/report', ['id' => $this->report->getId()]) 165 | ); 166 | } 167 | }) 168 | ->handleRequest($this->getServerRequest()); 169 | 170 | $this->addContent($form); 171 | } 172 | 173 | public function sendAction(): void 174 | { 175 | $this->addTitleTab($this->translate('Send Report')); 176 | 177 | Environment::raiseExecutionTime(); 178 | Environment::raiseMemoryLimit(); 179 | 180 | $form = (new SendForm()) 181 | ->setReport($this->report) 182 | ->setAction((string) Url::fromRequest()) 183 | ->on(SendForm::ON_SUCCESS, function () { 184 | $this->closeModalAndRefreshRelatedView( 185 | Url::fromPath('reporting/report', ['id' => $this->report->getId()]) 186 | ); 187 | }) 188 | ->handleRequest($this->getServerRequest()); 189 | 190 | $this->addContent($form); 191 | } 192 | 193 | public function scheduleAction(): void 194 | { 195 | $this->assertPermission('reporting/schedules'); 196 | $this->addTitleTab($this->translate('Schedule')); 197 | 198 | $form = ScheduleForm::fromReport($this->report); 199 | $form->setAction((string) Url::fromRequest()) 200 | ->on(ScheduleForm::ON_SUCCESS, function () use ($form) { 201 | $pressedButton = $form->getPressedSubmitElement(); 202 | if ($pressedButton) { 203 | $pressedButton = $pressedButton->getName(); 204 | } 205 | 206 | if ($pressedButton === 'remove') { 207 | Notification::success($this->translate('Removed schedule successfully')); 208 | } elseif ($pressedButton === 'send') { 209 | Notification::success($this->translate('Report sent successfully')); 210 | } elseif ($this->report->getSchedule() !== null) { 211 | Notification::success($this->translate('Updated schedule successfully')); 212 | } else { 213 | Notification::success($this->translate('Created schedule successfully')); 214 | } 215 | 216 | $this->closeModalAndRefreshRelatedView( 217 | Url::fromPath('reporting/report', ['id' => $this->report->getId()]) 218 | ); 219 | }) 220 | ->handleRequest($this->getServerRequest()); 221 | 222 | $this->addContent($form); 223 | 224 | $parts = $form->getPartUpdates(); 225 | if (! empty($parts)) { 226 | $this->sendMultipartUpdate(...$parts); 227 | } 228 | } 229 | 230 | public function downloadAction(): void 231 | { 232 | $type = $this->params->getRequired('type'); 233 | 234 | Environment::raiseExecutionTime(); 235 | Environment::raiseMemoryLimit(); 236 | 237 | $name = sprintf( 238 | '%s (%s) %s', 239 | $this->report->getName(), 240 | $this->report->getTimeframe()->getName(), 241 | date('Y-m-d H:i') 242 | ); 243 | 244 | switch ($type) { 245 | case 'pdf': 246 | /** @var Hook\PdfexportHook $exports */ 247 | $exports = Pdfexport::first(); 248 | $exports->streamPdfFromHtml($this->report->toPdf(), $name); 249 | exit; 250 | case 'csv': 251 | $response = $this->getResponse(); 252 | $response 253 | ->setHeader('Content-Type', 'text/csv') 254 | ->setHeader('Cache-Control', 'no-store') 255 | ->setHeader( 256 | 'Content-Disposition', 257 | 'attachment; filename=' . $name . '.csv' 258 | ) 259 | ->appendBody($this->report->toCsv()) 260 | ->sendResponse(); 261 | exit; 262 | case 'json': 263 | $response = $this->getResponse(); 264 | $response 265 | ->setHeader('Content-Type', 'application/json') 266 | ->setHeader('Cache-Control', 'no-store') 267 | ->setHeader( 268 | 'Content-Disposition', 269 | 'inline; filename=' . $name . '.json' 270 | ) 271 | ->appendBody($this->report->toJson()) 272 | ->sendResponse(); 273 | exit; 274 | } 275 | } 276 | 277 | protected function assembleActions(): ActionBar 278 | { 279 | $reportId = $this->report->getId(); 280 | 281 | $download = (new CompatDropdown('Download')) 282 | ->addLink( 283 | 'PDF', 284 | Url::fromPath('reporting/report/download?type=pdf', ['id' => $reportId]), 285 | null, 286 | ['target' => '_blank'] 287 | ); 288 | 289 | if ($this->report->providesData()) { 290 | $download->addLink( 291 | 'CSV', 292 | Url::fromPath('reporting/report/download?type=csv', ['id' => $reportId]), 293 | null, 294 | ['target' => '_blank'] 295 | ); 296 | $download->addLink( 297 | 'JSON', 298 | Url::fromPath('reporting/report/download?type=json', ['id' => $reportId]), 299 | null, 300 | ['target' => '_blank'] 301 | ); 302 | } 303 | 304 | $actions = new ActionBar(); 305 | 306 | if ($this->hasPermission('reporting/reports')) { 307 | $actions->addHtml( 308 | (new ActionLink( 309 | $this->translate('Modify'), 310 | Url::fromPath('reporting/report/edit', ['id' => $reportId]), 311 | 'edit' 312 | ))->openInModal() 313 | ); 314 | 315 | $actions->addHtml( 316 | (new ActionLink( 317 | $this->translate('Clone'), 318 | Url::fromPath('reporting/report/clone', ['id' => $reportId]), 319 | 'clone' 320 | ))->openInModal() 321 | ); 322 | } 323 | 324 | if ($this->hasPermission('reporting/schedules')) { 325 | $actions->addHtml( 326 | (new ActionLink( 327 | $this->translate('Schedule'), 328 | Url::fromPath('reporting/report/schedule', ['id' => $reportId]), 329 | 'calendar-empty' 330 | ))->openInModal() 331 | ); 332 | } 333 | 334 | $actions 335 | ->add($download) 336 | ->addHtml( 337 | (new ActionLink( 338 | $this->translate('Send'), 339 | Url::fromPath('reporting/report/send', ['id' => $reportId]), 340 | 'forward' 341 | ))->openInModal() 342 | ); 343 | 344 | return $actions; 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /application/controllers/ReportsController.php: -------------------------------------------------------------------------------- 1 | createTabs()->activate('reports'); 26 | 27 | if ($this->hasPermission('reporting/reports')) { 28 | $this->addControl( 29 | (new ButtonLink( 30 | $this->translate('New Report'), 31 | Url::fromPath('reporting/reports/new'), 32 | 'plus' 33 | ))->openInModal() 34 | ); 35 | } 36 | 37 | $tableRows = []; 38 | 39 | $reports = Report::on(Database::get()) 40 | ->withColumns(['report.timeframe.name']); 41 | 42 | $sortControl = $this->createSortControl( 43 | $reports, 44 | [ 45 | 'name' => $this->translate('Name'), 46 | 'author' => $this->translate('Author'), 47 | 'ctime' => $this->translate('Created At'), 48 | 'mtime' => $this->translate('Modified At') 49 | ] 50 | ); 51 | 52 | $this->addControl($sortControl); 53 | 54 | /** @var Report $report */ 55 | foreach ($reports as $report) { 56 | $url = Url::fromPath('reporting/report', ['id' => $report->id])->getAbsoluteUrl('&'); 57 | 58 | $tableRows[] = Html::tag('tr', ['href' => $url], [ 59 | Html::tag('td', null, $report->name), 60 | Html::tag('td', null, $report->author), 61 | Html::tag('td', null, $report->timeframe->name), 62 | Html::tag('td', null, $report->ctime->format('Y-m-d H:i')), 63 | Html::tag('td', null, $report->mtime->format('Y-m-d H:i')), 64 | ]); 65 | } 66 | 67 | if (! empty($tableRows)) { 68 | $table = Html::tag( 69 | 'table', 70 | ['class' => 'common-table table-row-selectable', 'data-base-target' => '_next'], 71 | [ 72 | Html::tag( 73 | 'thead', 74 | null, 75 | Html::tag( 76 | 'tr', 77 | null, 78 | [ 79 | Html::tag('th', null, 'Name'), 80 | Html::tag('th', null, 'Author'), 81 | Html::tag('th', null, 'Timeframe'), 82 | Html::tag('th', null, 'Date Created'), 83 | Html::tag('th', null, 'Date Modified'), 84 | Html::tag('th') 85 | ] 86 | ) 87 | ), 88 | Html::tag('tbody', null, $tableRows) 89 | ] 90 | ); 91 | 92 | $this->addContent($table); 93 | } else { 94 | $this->addContent(Html::tag('p', null, 'No reports created yet.')); 95 | } 96 | } 97 | 98 | public function newAction(): void 99 | { 100 | $this->assertPermission('reporting/reports'); 101 | $this->addTitleTab($this->translate('New Report')); 102 | 103 | switch ($this->params->shift('report')) { 104 | case 'host': 105 | $class = HostSlaReport::class; 106 | break; 107 | case 'service': 108 | $class = ServiceSlaReport::class; 109 | break; 110 | default: 111 | $class = null; 112 | break; 113 | } 114 | 115 | $form = (new ReportForm(Database::get())) 116 | ->setAction((string) Url::fromRequest()) 117 | ->setRenderCreateAndShowButton($class !== null) 118 | ->populate([ 119 | 'filter' => $this->params->shift('filter'), 120 | 'reportlet' => $class 121 | ]) 122 | ->on(ReportForm::ON_SUCCESS, function (ReportForm $form) { 123 | Notification::success($this->translate('Created report successfully')); 124 | 125 | $pressedButton = $form->getPressedSubmitElement(); 126 | if ($pressedButton && $pressedButton->getName() !== 'create_show') { 127 | $this->closeModalAndRefreshRelatedView(Url::fromPath('reporting/reports')); 128 | } else { 129 | $this->redirectNow( 130 | Url::fromPath( 131 | sprintf( 132 | 'reporting/reports#!%s', 133 | Url::fromPath('reporting/report', ['id' => $form->getId()])->getAbsoluteUrl() 134 | ) 135 | ) 136 | ); 137 | } 138 | }) 139 | ->handleRequest($this->getServerRequest()); 140 | 141 | $this->addContent($form); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /application/controllers/TemplateController.php: -------------------------------------------------------------------------------- 1 | filter(Filter::equal('id', $this->params->getRequired('id'))) 35 | ->first(); 36 | 37 | if ($template === null) { 38 | throw new Exception('Template not found'); 39 | } 40 | 41 | $this->template = $template; 42 | } 43 | 44 | public function indexAction(): void 45 | { 46 | $this->addTitleTab($this->translate('Preview')); 47 | 48 | $this->controls->getAttributes()->add('class', 'default-layout'); 49 | $this->addControl($this->createActionBars()); 50 | 51 | $template = Template::fromModel($this->template) 52 | ->setMacros([ 53 | 'date' => (new DateTime())->format('jS M, Y'), 54 | 'time_frame' => 'Time Frame', 55 | 'time_frame_absolute' => 'Time Frame (absolute)', 56 | 'title' => 'Icinga Report Preview' 57 | ]) 58 | ->setPreview(true); 59 | 60 | $this->addContent($template); 61 | } 62 | 63 | public function editAction(): void 64 | { 65 | $this->assertPermission('reporting/templates'); 66 | $this->addTitleTab($this->translate('Edit Template')); 67 | 68 | $form = TemplateForm::fromTemplate($this->template) 69 | ->setAction((string) Url::fromRequest()) 70 | ->on(TemplateForm::ON_SUCCESS, function (Form $form) { 71 | $pressedButton = $form->getPressedSubmitElement(); 72 | if ($pressedButton && $pressedButton->getName() === 'remove') { 73 | Notification::success($this->translate('Removed template successfully')); 74 | 75 | $this->switchToSingleColumnLayout(); 76 | } else { 77 | Notification::success($this->translate('Updated template successfully')); 78 | 79 | $this->closeModalAndRefreshRemainingViews( 80 | Url::fromPath('reporting/template', ['id' => $this->template->id]) 81 | ); 82 | } 83 | }) 84 | ->handleRequest(ServerRequest::fromGlobals()); 85 | 86 | $this->addContent($form); 87 | } 88 | 89 | protected function createActionBars(): ValidHtml 90 | { 91 | $actions = new ActionBar(); 92 | $actions->addHtml( 93 | (new ActionLink( 94 | $this->translate('Modify'), 95 | Url::fromPath('reporting/template/edit', ['id' => $this->template->id]), 96 | 'edit' 97 | ))->openInModal() 98 | ); 99 | 100 | return $actions; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /application/controllers/TemplatesController.php: -------------------------------------------------------------------------------- 1 | createTabs()->activate('templates'); 25 | 26 | $canManage = $this->hasPermission('reporting/templates'); 27 | 28 | if ($canManage) { 29 | $this->addControl( 30 | (new ButtonLink( 31 | $this->translate('New Template'), 32 | Url::fromPath('reporting/templates/new'), 33 | 'plus' 34 | ))->openInModal() 35 | ); 36 | } 37 | 38 | $templates = Model\Template::on(Database::get()); 39 | 40 | $sortControl = $this->createSortControl( 41 | $templates, 42 | [ 43 | 'name' => $this->translate('Name'), 44 | 'author' => $this->translate('Author'), 45 | 'ctime' => $this->translate('Created At'), 46 | 'mtime' => $this->translate('Modified At') 47 | ] 48 | ); 49 | 50 | $this->addControl($sortControl); 51 | 52 | $tableRows = []; 53 | 54 | /** @var Model\Template $template */ 55 | foreach ($templates as $template) { 56 | // Preview URL 57 | $subjectLink = new Link($template->name, Url::fromPath('reporting/template', ['id' => $template->id])); 58 | $tableRows[] = Html::tag('tr', null, [ 59 | Html::tag('td', null, $subjectLink), 60 | Html::tag('td', null, $template->author), 61 | Html::tag('td', null, $template->ctime->format('Y-m-d H:i')), 62 | Html::tag('td', null, $template->mtime->format('Y-m-d H:i')) 63 | ]); 64 | } 65 | 66 | if (! empty($tableRows)) { 67 | $table = Html::tag( 68 | 'table', 69 | ['class' => 'common-table table-row-selectable', 'data-base-target' => '_next'], 70 | [ 71 | Html::tag( 72 | 'thead', 73 | null, 74 | Html::tag( 75 | 'tr', 76 | null, 77 | [ 78 | Html::tag('th', null, 'Name'), 79 | Html::tag('th', null, 'Author'), 80 | Html::tag('th', null, 'Date Created'), 81 | Html::tag('th', null, 'Date Modified') 82 | ] 83 | ) 84 | ), 85 | Html::tag('tbody', null, $tableRows) 86 | ] 87 | ); 88 | 89 | $this->addContent($table); 90 | } else { 91 | $this->addContent(Html::tag('p', null, 'No templates created yet.')); 92 | } 93 | } 94 | 95 | public function newAction(): void 96 | { 97 | $this->assertPermission('reporting/templates'); 98 | $this->addTitleTab($this->translate('New Template')); 99 | 100 | $form = (new TemplateForm()) 101 | ->setAction((string) Url::fromRequest()) 102 | ->on(TemplateForm::ON_SUCCESS, function () { 103 | Notification::success($this->translate('Created template successfully')); 104 | 105 | $this->closeModalAndRefreshRelatedView(Url::fromPath('reporting/templates')); 106 | }) 107 | ->handleRequest($this->getServerRequest()); 108 | 109 | $this->addContent($form); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /application/controllers/TimeframeController.php: -------------------------------------------------------------------------------- 1 | filter(Filter::equal('id', $this->params->getRequired('id'))) 28 | ->first(); 29 | 30 | if ($timeframe === null) { 31 | throw new Exception('Timeframe not found'); 32 | } 33 | 34 | $this->timeframe = Timeframe::fromModel($timeframe); 35 | } 36 | 37 | public function editAction(): void 38 | { 39 | $this->assertPermission('reporting/timeframes'); 40 | $this->addTitleTab($this->translate('Edit Time Frame')); 41 | 42 | $values = [ 43 | 'name' => $this->timeframe->getName(), 44 | 'start' => $this->timeframe->getStart(), 45 | 'end' => $this->timeframe->getEnd() 46 | ]; 47 | 48 | $form = TimeframeForm::fromId($this->timeframe->getId()) 49 | ->setAction((string) Url::fromRequest()) 50 | ->populate($values) 51 | ->on(TimeframeForm::ON_SUCCESS, function (Form $form) { 52 | $pressedButton = $form->getPressedSubmitElement(); 53 | if ($pressedButton && $pressedButton->getName() === 'remove') { 54 | Notification::success($this->translate('Removed timeframe successfully')); 55 | } else { 56 | Notification::success($this->translate('Update timeframe successfully')); 57 | } 58 | 59 | $this->switchToSingleColumnLayout(); 60 | })->handleRequest($this->getServerRequest()); 61 | 62 | $this->addContent($form); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /application/controllers/TimeframesController.php: -------------------------------------------------------------------------------- 1 | createTabs()->activate('timeframes'); 25 | 26 | $canManage = $this->hasPermission('reporting/timeframes'); 27 | 28 | if ($canManage) { 29 | $this->addControl( 30 | (new ButtonLink( 31 | $this->translate('New Timeframe'), 32 | Url::fromPath('reporting/timeframes/new'), 33 | 'plus' 34 | ))->openInModal() 35 | ); 36 | } 37 | 38 | $tableRows = []; 39 | 40 | $timeframes = Model\Timeframe::on(Database::get()); 41 | 42 | $sortControl = $this->createSortControl( 43 | $timeframes, 44 | [ 45 | 'name' => $this->translate('Name'), 46 | 'ctime' => $this->translate('Created At'), 47 | 'mtime' => $this->translate('Modified At') 48 | ] 49 | ); 50 | 51 | $this->addControl($sortControl); 52 | 53 | foreach ($timeframes as $timeframe) { 54 | $subject = $timeframe->name; 55 | 56 | if ($canManage) { 57 | $subject = new Link( 58 | $timeframe->name, 59 | Url::fromPath('reporting/timeframe/edit', ['id' => $timeframe->id]) 60 | ); 61 | } 62 | 63 | $tableRows[] = Html::tag('tr', null, [ 64 | Html::tag('td', null, $subject), 65 | Html::tag('td', null, $timeframe->start), 66 | Html::tag('td', null, $timeframe->end), 67 | Html::tag('td', null, $timeframe->ctime->format('Y-m-d H:i')), 68 | Html::tag('td', null, $timeframe->mtime->format('Y-m-d H:i')) 69 | ]); 70 | } 71 | 72 | if (! empty($tableRows)) { 73 | $table = Html::tag( 74 | 'table', 75 | [ 76 | 'class' => 'common-table table-row-selectable', 77 | 'data-base-target' => '_next' 78 | ], 79 | [ 80 | Html::tag( 81 | 'thead', 82 | null, 83 | Html::tag( 84 | 'tr', 85 | null, 86 | [ 87 | Html::tag('th', null, 'Name'), 88 | Html::tag('th', null, 'Start'), 89 | Html::tag('th', null, 'End'), 90 | Html::tag('th', null, 'Date Created'), 91 | Html::tag('th', null, 'Date Modified') 92 | ] 93 | ) 94 | ), 95 | Html::tag('tbody', null, $tableRows) 96 | ] 97 | ); 98 | 99 | $this->addContent($table); 100 | } else { 101 | $this->addContent(Html::tag('p', null, 'No timeframes created yet.')); 102 | } 103 | } 104 | 105 | public function newAction(): void 106 | { 107 | $this->assertPermission('reporting/timeframes'); 108 | $this->addTitleTab($this->translate('New Timeframe')); 109 | 110 | $form = (new TimeframeForm()) 111 | ->setAction((string) Url::fromRequest()) 112 | ->on(TimeframeForm::ON_SUCCESS, function () { 113 | Notification::success($this->translate('Created timeframe successfully')); 114 | 115 | $this->closeModalAndRefreshRelatedView(Url::fromPath('reporting/timeframes')); 116 | })->handleRequest($this->getServerRequest()); 117 | 118 | $this->addContent($form); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /application/forms/ConfigureMailForm.php: -------------------------------------------------------------------------------- 1 | setName('reporting_mail'); 14 | $this->setSubmitLabel($this->translate('Save Changes')); 15 | } 16 | 17 | public function createElements(array $formData) 18 | { 19 | $this->addElement('text', 'mail_from', [ 20 | 'label' => $this->translate('From'), 21 | 'placeholder' => 'reporting@icinga' 22 | ]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /application/forms/SelectBackendForm.php: -------------------------------------------------------------------------------- 1 | setName('reporting_backend'); 15 | $this->setSubmitLabel($this->translate('Save Changes')); 16 | } 17 | 18 | public function createElements(array $formData) 19 | { 20 | $dbResources = ResourceFactory::getResourceConfigs('db')->keys(); 21 | $options = array_combine($dbResources, $dbResources); 22 | 23 | $default = null; 24 | if (isset($options['reporting'])) { 25 | $default = 'reporting'; 26 | } 27 | 28 | $this->addElement('select', 'backend_resource', [ 29 | 'label' => $this->translate('Database'), 30 | 'description' => $this->translate('Database resource'), 31 | 'multiOptions' => $options, 32 | 'value' => $default, 33 | 'required' => true 34 | ]); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /application/views/scripts/config/backend.phtml: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 6 |
7 | -------------------------------------------------------------------------------- /application/views/scripts/config/mail.phtml: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 6 |
7 | -------------------------------------------------------------------------------- /config/systemd/icinga-reporting.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Icinga Reporting Scheduler 3 | 4 | [Service] 5 | Type=simple 6 | ExecStart=/usr/bin/icingacli reporting schedule run 7 | Restart=on-success 8 | 9 | [Install] 10 | WantedBy=multi-user.target 11 | -------------------------------------------------------------------------------- /configuration.php: -------------------------------------------------------------------------------- 1 | provideCssFile('system-report.css'); 10 | 11 | $this->menuSection(N_('Reporting'), ['icon' => 'fa-chart-simple', 'priority' => 100]) 12 | ->add(N_('Reports'), ['url' => 'reporting/reports', 'priority' => 10]); 13 | 14 | $this->provideConfigTab('backend', array( 15 | 'title' => $this->translate('Configure the database backend'), 16 | 'label' => $this->translate('Backend'), 17 | 'url' => 'config/backend' 18 | )); 19 | 20 | $this->provideConfigTab('mail', array( 21 | 'title' => $this->translate('Configure mail'), 22 | 'label' => $this->translate('Mail'), 23 | 'url' => 'config/mail' 24 | )); 25 | 26 | $this->providePermission( 27 | 'reporting/reports', 28 | $this->translate('Allow managing reports') 29 | ); 30 | 31 | $this->providePermission( 32 | 'reporting/schedules', 33 | $this->translate('Allow managing schedules') 34 | ); 35 | 36 | $this->providePermission( 37 | 'reporting/templates', 38 | $this->translate('Allow managing templates') 39 | ); 40 | 41 | $this->providePermission( 42 | 'reporting/timeframes', 43 | $this->translate('Allow managing timeframes') 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /doc/02-Installation.md: -------------------------------------------------------------------------------- 1 | # Installing Icinga Reporting 2 | 3 | Please see the Icinga Web documentation on 4 | [how to install modules](https://icinga.com/docs/icinga-web-2/latest/doc/08-Modules/#installation) from source. 5 | Make sure you use `reporting` as the module name. The following requirements must also be met. 6 | 7 | ## Requirements 8 | 9 | * PHP (≥7.2) 10 | * MySQL or PostgreSQL PDO PHP libraries 11 | * The following PHP modules must be installed: `mbstring` 12 | * [Icinga Web](https://github.com/Icinga/icingaweb2) (≥2.9) 13 | * [Icinga PHP Library (ipl)](https://github.com/Icinga/icinga-php-library) (≥0.13.0) 14 | * [Icinga PHP Thirdparty](https://github.com/Icinga/icinga-php-thirdparty) (≥0.12.0) 15 | 16 | ## Setting up the Database 17 | 18 | ### Setting up a MySQL or MariaDB Database 19 | 20 | The module needs a MySQL/MariaDB database with the schema that's provided in the `schema/mysql.schema.sql` file. 21 | 22 | You can use the following sample command for creating the MySQL/MariaDB database. Please change the password: 23 | 24 | ``` 25 | CREATE DATABASE reporting; 26 | GRANT SELECT, INSERT, UPDATE, DELETE, DROP, CREATE, ALTER, CREATE VIEW, INDEX, EXECUTE ON reporting.* TO reporting@localhost IDENTIFIED BY 'secret'; 27 | ``` 28 | 29 | After, you can import the schema using the following command: 30 | 31 | ``` 32 | mysql -p -u root reporting < /usr/share/icingaweb2/modules/reporting/schema/mysql.schema.sql 33 | ``` 34 | 35 | ## Setting up a PostgreSQL Database 36 | 37 | The module needs a PostgreSQL database with the schema that's provided in the `schema/pgsql.schema.sql` file. 38 | 39 | You can use the following sample command for creating the PostgreSQL database. Please change the password: 40 | 41 | ```sql 42 | CREATE USER reporting WITH PASSWORD 'secret'; 43 | CREATE DATABASE reporting 44 | WITH OWNER reporting 45 | ENCODING 'UTF8' 46 | LC_COLLATE = 'en_US.UTF-8' 47 | LC_CTYPE = 'en_US.UTF-8'; 48 | ``` 49 | 50 | After, you can import the schema using the following command: 51 | 52 | ``` 53 | psql -U reporting reporting -a -f /usr/share/icingaweb2/modules/reporting/schema/pgsql.schema.sql 54 | ``` 55 | 56 | This concludes the installation. Now continue with the [configuration](03-Configuration.md). 57 | -------------------------------------------------------------------------------- /doc/03-Configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | Icinga Reporting is configured via the web interface. Below you will find an overview of the necessary settings. 4 | 5 | ## Backend 6 | 7 | Icinga Reporting stores all its configuration in the database, therefore you need to create and configure a database 8 | resource for it. 9 | 10 | 1. Create a new resource for Icinga Reporting via the `Configuration -> Application -> Resources` menu. 11 | 12 | 2. Configure the resource you just created as the database connection for Icinga Reporting using the 13 | `Configuration → Modules → reporting → Backend` menu. If you've used `reporting` 14 | as name for the resource, this is optional. 15 | 16 | ## Mail 17 | 18 | At `Configuration -> Modules -> reporting -> Mail` you can configure the address 19 | that is used as the sender's address (From) in E-mails. 20 | 21 | ## Permissions 22 | 23 | There are four permissions that can be used to control what can be managed by whom. 24 | 25 | | Permission | Applies to | 26 | |----------------------|-----------------------------------| 27 | | reporting/reports | Reports (create, edit, delete) | 28 | | reporting/schedules | Schedules (create, edit, delete) | 29 | | reporting/templates | Templates (create, edit, delete) | 30 | | reporting/timeframes | Timeframes (create, edit, delete) | 31 | 32 | ## Icinga Reporting Daemon 33 | 34 | There is a daemon for generating and distributing reports on a schedule if configured: 35 | 36 | ``` 37 | icingacli reporting schedule run 38 | ``` 39 | 40 | This command schedules the execution of all applicable reports. 41 | 42 | The `systemd` service of this module uses this command as well. 43 | 44 | To configure this as a `systemd` service, copy the example service definition from 45 | `/usr/share/icingaweb2/modules/reporting/config/systemd/icinga-reporting.service` 46 | to `/etc/systemd/system/icinga-reporting.service`. 47 | 48 | You can run the following command to enable and start the daemon. 49 | 50 | ``` 51 | systemctl enable --now icinga-reporting.service 52 | ``` 53 | -------------------------------------------------------------------------------- /doc/80-Upgrading.md: -------------------------------------------------------------------------------- 1 | # Upgrading Icinga Reporting 2 | 3 | !!! info 4 | 5 | If you have Icinga Web v2.12 or newer installed, you can perform database migrations in the UI. 6 | 7 | 8 | 9 | **Note:** If you haven't installed this module from packages, then please adapt the database schema 10 | path to the correct installation path. 11 | 12 | 13 | 14 | ## Upgrading to Version 1.0.3 15 | 16 | Icinga Reporting version 1.0.3 requires a schema update for the database. 17 | It fixes the `end` time of the preconfigured `Current Week` timeframe. 18 | 19 | You may use the following command to apply the database schema upgrade file: 20 | 21 | **MySQL:** 22 | 23 | ``` 24 | # mysql -u root -p reporting < /usr/share/icingaweb2/modules/reporting/schema/mysql-upgrades/1.0.3.sql 25 | ``` 26 | 27 | **PostgreSQL:** 28 | 29 | ``` 30 | # psql -U postgres -d reporting -f /usr/share/icingaweb2/modules/reporting/schema/pgsql-upgrades/1.0.3.sql 31 | ``` 32 | 33 | ## Upgrading to Version 1.0.0 34 | 35 | Icinga Reporting version 1.0.0 requires a schema update for the database. 36 | 37 | > **Note** 38 | > 39 | > If you're not using Icinga Web migration automation, you may need to [populate](https://dev.mysql.com/doc/refman/8.0/en/time-zone-support.html#time-zone-installation) 40 | > all the system named time zone information into your MSQL/MariaDB server. Otherwise, the migration may not succeed. 41 | 42 | You may use the following command to apply the database schema upgrade file: 43 | 44 | ``` 45 | # mysql -u root -p reporting < /usr/share/icingaweb2/modules/reporting/schema/mysql-upgrades/1.0.0.sql 46 | ``` 47 | 48 | ## Upgrading to Version 0.10.0 49 | 50 | Icinga Reporting version 0.10.0 requires a schema update for the database. 51 | A new table `template`, linked to table `report`, has been introduced. 52 | Please find the upgrade script in **schema/mysql-upgrades**. 53 | 54 | You may use the following command to apply the database schema upgrade file: 55 | 56 | ``` 57 | # mysql -u root -p reporting < /usr/share/icingaweb2/modules/reporting/schema/mysql-upgrades/0.10.0.sql 58 | ``` 59 | 60 | ## Upgrading to Version 0.9.1 61 | 62 | Icinga Reporting version 0.9.1 requires a schema update for the database. 63 | The schema has been adjusted so that it is no longer necessary to adjust server settings 64 | if you're using a version of MySQL < 5.7 or MariaDB < 10.2. 65 | Further, the start dates for the provided time frames **Last Year** and **Current Year** have been fixed. 66 | Please find the upgrade script in **schema/mysql-migrations**. 67 | 68 | You may use the following command to apply the database schema upgrade file: 69 | 70 | ``` 71 | # mysql -u root -p reporting < /usr/share/icingaweb2/modules/reporting/schema/mysql-upgrades/0.9.1.sql 72 | ``` 73 | -------------------------------------------------------------------------------- /library/Reporting/Actions/SendMail.php: -------------------------------------------------------------------------------- 1 | getName(), 31 | $report->getTimeframe()->getName(), 32 | date('Y-m-d H:i') 33 | ); 34 | 35 | $mail = new Mail(); 36 | 37 | $mail->setFrom( 38 | Config::module('reporting', 'config', true)->get('mail', 'from', 'reporting@icinga') 39 | ); 40 | 41 | if (isset($config['subject'])) { 42 | $mail->setSubject($config['subject']); 43 | } 44 | 45 | /** @var array $recipients */ 46 | $recipients = preg_split('/[\s,]+/', $config['recipients']); 47 | $recipients = array_filter($recipients); 48 | 49 | switch ($config['type']) { 50 | case 'pdf': 51 | /** @var Pdfexport $exporter */ 52 | $exporter = Pdfexport::first(); 53 | $exporter->asyncHtmlToPdf($report->toPdf())->then( 54 | function ($pdf) use ($mail, $name, $recipients) { 55 | $mail->attachPdf($pdf, $name); 56 | $mail->send(null, $recipients); 57 | } 58 | )->otherwise(function (Throwable $e) { 59 | Logger::error($e); 60 | Logger::debug($e->getTraceAsString()); 61 | }); 62 | 63 | return; 64 | case 'csv': 65 | $mail->attachCsv($report->toCsv(), $name); 66 | 67 | break; 68 | case 'json': 69 | $mail->attachJson($report->toJson(), $name); 70 | 71 | break; 72 | default: 73 | throw new \InvalidArgumentException(); 74 | } 75 | 76 | $mail->send(null, $recipients); 77 | } 78 | 79 | public function initConfigForm(Form $form, Report $report) 80 | { 81 | $types = ['pdf' => 'PDF']; 82 | 83 | if ($report->providesData()) { 84 | $types['csv'] = 'CSV'; 85 | $types['json'] = 'JSON'; 86 | } 87 | 88 | $form->addElement('select', 'type', [ 89 | 'required' => true, 90 | 'label' => t('Type'), 91 | 'options' => $types 92 | ]); 93 | 94 | $form->addElement('text', 'subject', [ 95 | 'label' => t('Subject'), 96 | 'placeholder' => Mail::DEFAULT_SUBJECT 97 | ]); 98 | 99 | $form->addElement('textarea', 'recipients', [ 100 | 'required' => true, 101 | 'label' => t('Recipients'), 102 | 'validators' => [ 103 | new CallbackValidator(function ($value, CallbackValidator $validator): bool { 104 | $mailValidator = new EmailAddressValidator(); 105 | $mails = Str::trimSplit($value); 106 | foreach ($mails as $mail) { 107 | if (! $mailValidator->isValid($mail)) { 108 | $validator->addMessage(...$mailValidator->getMessages()); 109 | 110 | return false; 111 | } 112 | } 113 | 114 | return true; 115 | }) 116 | ] 117 | ]); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /library/Reporting/Cli/Command.php: -------------------------------------------------------------------------------- 1 | getModuleManager()->loadEnabledModules(); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /library/Reporting/Common/Macros.php: -------------------------------------------------------------------------------- 1 | macros[$name] ?: null; 17 | } 18 | 19 | /** 20 | * @return mixed 21 | */ 22 | public function getMacros() 23 | { 24 | return $this->macros; 25 | } 26 | 27 | /** 28 | * @param mixed $macros 29 | * 30 | * @return $this 31 | */ 32 | public function setMacros($macros) 33 | { 34 | $this->macros = $macros; 35 | 36 | return $this; 37 | } 38 | 39 | public function resolveMacros($subject) 40 | { 41 | $macros = []; 42 | 43 | foreach ((array) $this->macros as $key => $value) { 44 | $macros['${' . $key . '}'] = $value; 45 | } 46 | 47 | return str_replace(array_keys($macros), array_values($macros), $subject); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /library/Reporting/Database.php: -------------------------------------------------------------------------------- 1 | get('backend', 'resource', 'reporting') 41 | ) 42 | ); 43 | 44 | $config->options = [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_OBJ]; 45 | if ($config->db === 'mysql') { 46 | $config->options[PDO::MYSQL_ATTR_INIT_COMMAND] = "SET SESSION SQL_MODE='STRICT_TRANS_TABLES" 47 | . ",NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'"; 48 | } 49 | 50 | return new RetryConnection($config); 51 | } 52 | 53 | /** 54 | * List all reporting timeframes 55 | * 56 | * @return array 57 | */ 58 | public static function listTimeframes(): array 59 | { 60 | return self::list( 61 | (new Sql\Select()) 62 | ->from('timeframe') 63 | ->columns(['id', 'name']) 64 | ); 65 | } 66 | 67 | /** 68 | * List all reporting templates 69 | * 70 | * @return array 71 | */ 72 | public static function listTemplates(): array 73 | { 74 | return self::list( 75 | (new Sql\Select()) 76 | ->from('template') 77 | ->columns(['id', 'name']) 78 | ); 79 | } 80 | 81 | /** 82 | * Helper method for list templates and timeframes 83 | * 84 | * @param Sql\Select $select 85 | * 86 | * @return array 87 | */ 88 | private static function list(Sql\Select $select): array 89 | { 90 | $result = []; 91 | /** @var stdClass $row */ 92 | foreach (self::get()->select($select) as $row) { 93 | /** @var int $id */ 94 | $id = $row->id; 95 | /** @var string $name */ 96 | $name = $row->name; 97 | 98 | $result[$id] = $name; 99 | } 100 | 101 | return $result; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /library/Reporting/Dimensions.php: -------------------------------------------------------------------------------- 1 | dimensions; 14 | } 15 | 16 | public function setDimensions(array $dimensions) 17 | { 18 | $this->dimensions = $dimensions; 19 | 20 | return $this; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /library/Reporting/Hook/ActionHook.php: -------------------------------------------------------------------------------- 1 | getDeclaringClass()->getName() !== self::class; 80 | } 81 | 82 | /** 83 | * Get whether the report provides HTML 84 | * 85 | * @return bool 86 | */ 87 | public function providesHtml() 88 | { 89 | try { 90 | $method = new \ReflectionMethod($this, 'getHtml'); 91 | } catch (\ReflectionException $e) { 92 | return false; 93 | } 94 | 95 | return $method->getDeclaringClass()->getName() !== self::class; 96 | } 97 | 98 | /** 99 | * Get the module name of the report 100 | * 101 | * @return string 102 | */ 103 | final public function getModuleName() 104 | { 105 | return ClassLoader::extractModuleName(get_class($this)); 106 | } 107 | 108 | /** 109 | * Get all provided reports 110 | * 111 | * @return ReportHook[] 112 | */ 113 | final public static function getReports() 114 | { 115 | return Hook::all('reporting/Report'); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /library/Reporting/Mail.php: -------------------------------------------------------------------------------- 1 | from)) { 37 | return $this->from; 38 | } 39 | 40 | if (isset($_SERVER['SERVER_ADMIN'])) { 41 | $this->from = $_SERVER['SERVER_ADMIN']; 42 | 43 | return $this->from; 44 | } 45 | 46 | foreach (['HTTP_HOST', 'SERVER_NAME', 'HOSTNAME'] as $key) { 47 | if (isset($_SERVER[$key])) { 48 | $this->from = 'icinga-reporting@' . $_SERVER[$key]; 49 | 50 | return $this->from; 51 | } 52 | } 53 | 54 | $this->from = 'icinga-reporting@localhost'; 55 | 56 | return $this->from; 57 | } 58 | 59 | /** 60 | * Set the from part 61 | * 62 | * @param string $from 63 | * 64 | * @return $this 65 | */ 66 | public function setFrom($from) 67 | { 68 | $this->from = $from; 69 | 70 | return $this; 71 | } 72 | 73 | /** 74 | * Get the subject 75 | * 76 | * @return string 77 | */ 78 | public function getSubject() 79 | { 80 | return $this->subject; 81 | } 82 | 83 | /** 84 | * Set the subject 85 | * 86 | * @param string $subject 87 | * 88 | * @return $this 89 | */ 90 | public function setSubject($subject) 91 | { 92 | $this->subject = $subject; 93 | 94 | return $this; 95 | } 96 | 97 | /** 98 | * Get the mail transport 99 | * 100 | * @return Zend_Mail_Transport_Sendmail 101 | */ 102 | public function getTransport() 103 | { 104 | if (! isset($this->transport)) { 105 | $this->transport = new Zend_Mail_Transport_Sendmail('-f ' . escapeshellarg($this->getFrom())); 106 | } 107 | 108 | return $this->transport; 109 | } 110 | 111 | public function attachCsv($csv, $filename) 112 | { 113 | if (is_array($csv)) { 114 | $csv = Str::putcsv($csv); 115 | } 116 | 117 | $attachment = new Zend_Mime_Part($csv); 118 | 119 | $attachment->type = 'text/csv'; 120 | $attachment->disposition = Zend_Mime::DISPOSITION_ATTACHMENT; 121 | $attachment->encoding = Zend_Mime::ENCODING_BASE64; 122 | $attachment->filename = basename($filename, '.csv') . '.csv'; 123 | 124 | $this->attachments[] = $attachment; 125 | 126 | return $this; 127 | } 128 | 129 | public function attachJson($json, $filename) 130 | { 131 | if (is_array($json)) { 132 | $json = json_encode($json); 133 | } 134 | 135 | $attachment = new Zend_Mime_Part($json); 136 | 137 | $attachment->type = 'application/json'; 138 | $attachment->disposition = Zend_Mime::DISPOSITION_ATTACHMENT; 139 | $attachment->encoding = Zend_Mime::ENCODING_BASE64; 140 | $attachment->filename = basename($filename, '.json') . '.json'; 141 | 142 | $this->attachments[] = $attachment; 143 | 144 | return $this; 145 | } 146 | 147 | public function attachPdf($pdf, $filename) 148 | { 149 | $attachment = new Zend_Mime_Part($pdf); 150 | 151 | $attachment->type = 'application/pdf'; 152 | $attachment->disposition = Zend_Mime::DISPOSITION_ATTACHMENT; 153 | $attachment->encoding = Zend_Mime::ENCODING_BASE64; 154 | $attachment->filename = basename($filename, '.pdf') . '.pdf'; 155 | 156 | $this->attachments[] = $attachment; 157 | 158 | return $this; 159 | } 160 | 161 | public function send($body, $recipient) 162 | { 163 | $mail = new Zend_Mail('UTF-8'); 164 | 165 | $mail->setFrom($this->getFrom(), ''); 166 | $mail->addTo($recipient); 167 | $mail->setSubject($this->getSubject()); 168 | 169 | if ($body && (strlen($body) !== strlen(strip_tags($body)))) { 170 | $mail->setBodyHtml($body); 171 | } else { 172 | $mail->setBodyText($body ?? ''); 173 | } 174 | 175 | foreach ($this->attachments as $attachment) { 176 | $mail->addAttachment($attachment); 177 | } 178 | 179 | $mail->send($this->getTransport()); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /library/Reporting/Model/Config.php: -------------------------------------------------------------------------------- 1 | add(new MillisecondTimestamp([ 38 | 'ctime', 39 | 'mtime' 40 | ])); 41 | } 42 | 43 | public function createRelations(Relations $relations) 44 | { 45 | $relations->belongsTo('reportlet', Reportlet::class); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /library/Reporting/Model/Report.php: -------------------------------------------------------------------------------- 1 | add(new MillisecondTimestamp([ 56 | 'ctime', 57 | 'mtime' 58 | ])); 59 | } 60 | 61 | public function createRelations(Relations $relations) 62 | { 63 | $relations->belongsTo('timeframe', Timeframe::class); 64 | $relations->belongsTo('template', Template::class) 65 | ->setJoinType('LEFT'); 66 | 67 | $relations->hasOne('schedule', Schedule::class) 68 | ->setJoinType('LEFT'); 69 | $relations->hasMany('reportlets', Reportlet::class); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /library/Reporting/Model/Reportlet.php: -------------------------------------------------------------------------------- 1 | add(new MillisecondTimestamp([ 37 | 'ctime', 38 | 'mtime' 39 | ])); 40 | } 41 | 42 | public function createRelations(Relations $relations) 43 | { 44 | $relations->belongsTo('report', Report::class); 45 | 46 | $relations->hasMany('config', Config::class); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /library/Reporting/Model/Schedule.php: -------------------------------------------------------------------------------- 1 | add(new MillisecondTimestamp([ 39 | 'ctime', 40 | 'mtime' 41 | ])); 42 | } 43 | 44 | public function createRelations(Relations $relations) 45 | { 46 | $relations->belongsTo('report', Report::class); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /library/Reporting/Model/Schema.php: -------------------------------------------------------------------------------- 1 | add(new BoolCast(['success'])); 47 | $behaviors->add(new MillisecondTimestamp(['timestamp'])); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /library/Reporting/Model/Template.php: -------------------------------------------------------------------------------- 1 | add(new MillisecondTimestamp([ 43 | 'ctime', 44 | 'mtime' 45 | ])); 46 | } 47 | 48 | public function createRelations(Relations $relations) 49 | { 50 | $relations->hasMany('report', Report::class) 51 | ->setJoinType('LEFT'); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /library/Reporting/Model/Timeframe.php: -------------------------------------------------------------------------------- 1 | add(new MillisecondTimestamp([ 44 | 'ctime', 45 | 'mtime' 46 | ])); 47 | } 48 | 49 | public function createRelations(Relations $relations) 50 | { 51 | $relations->hasMany('report', Report::class); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /library/Reporting/ProvidedActions.php: -------------------------------------------------------------------------------- 1 | $action) { 16 | $actions[$class] = $action->getName(); 17 | } 18 | 19 | return $actions; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /library/Reporting/ProvidedHook/DbMigration.php: -------------------------------------------------------------------------------- 1 | translate('Icinga Reporting'); 18 | } 19 | 20 | public function providedDescriptions(): array 21 | { 22 | return [ 23 | '0.9.1' => $this->translate( 24 | 'Modifies all columns that uses current_timestamp to unix_timestamp and alters the database' 25 | . ' engine of some tables.' 26 | ), 27 | '0.10.0' => $this->translate('Creates the template table and adjusts some column types'), 28 | '1.0.0' => $this->translate('Migrates all your configured report schedules to the new config.'), 29 | '1.0.3' => $this->translate('Fix the `end` time of preconfigured `Current Week` timeframe.'), 30 | ]; 31 | } 32 | 33 | protected function getSchemaQuery(): Query 34 | { 35 | return Schema::on($this->getDb()); 36 | } 37 | 38 | public function getDb(): Connection 39 | { 40 | return Database::get(); 41 | } 42 | 43 | public function getVersion(): string 44 | { 45 | if ($this->version === null) { 46 | $conn = $this->getDb(); 47 | $schema = $this->getSchemaQuery() 48 | ->columns(['version', 'success']) 49 | ->orderBy('id', SORT_DESC) 50 | ->limit(2); 51 | 52 | if (static::tableExists($conn, $schema->getModel()->getTableName())) { 53 | /** @var Schema $version */ 54 | foreach ($schema as $version) { 55 | if ($version->success) { 56 | $this->version = $version->version; 57 | 58 | break; 59 | } 60 | } 61 | 62 | if (! $this->version) { 63 | // Schema version table exist, but the user has probably deleted the entry! 64 | $this->version = '1.0.0'; 65 | } 66 | } elseif (static::tableExists($conn, 'template')) { 67 | // We have added Postgres support and the template table with 0.10.0. 68 | // So, use this as the last (migrated) version. 69 | $this->version = '0.10.0'; 70 | } elseif (static::getColumnType($conn, 'timeframe', 'name') === 'varchar(128)') { 71 | // Upgrade script 0.9.1 alters the timeframe.name column from `varchar(255)` -> `varchar(128)`. 72 | // Therefore, we can safely use this as the last migrated version. 73 | $this->version = '0.9.1'; 74 | } else { 75 | // Use the initial version as the last migrated schema version! 76 | $this->version = '0.9.0'; 77 | } 78 | } 79 | 80 | return $this->version; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /library/Reporting/ProvidedReports.php: -------------------------------------------------------------------------------- 1 | $report) { 16 | $reports[$class] = $report->getName(); 17 | } 18 | 19 | return $reports; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /library/Reporting/Report.php: -------------------------------------------------------------------------------- 1 | id = $reportModel->id; 54 | $report->name = $reportModel->name; 55 | $report->author = $reportModel->author; 56 | $report->timeframe = Timeframe::fromModel($reportModel->timeframe); 57 | 58 | $template = $reportModel->template->first(); 59 | if ($template !== null) { 60 | $report->template = Template::fromModel($template); 61 | } 62 | 63 | $reportlets = []; 64 | foreach ($reportModel->reportlets as $reportlet) { 65 | $reportlet->report_name = $reportModel->name; 66 | $reportlet->report_id = $reportModel->id; 67 | $reportlets[] = Reportlet::fromModel($reportlet); 68 | } 69 | 70 | if (empty($reportlets)) { 71 | throw new Exception('No reportlets configured'); 72 | } 73 | 74 | $report->reportlets = $reportlets; 75 | 76 | $schedule = $reportModel->schedule->first(); 77 | if ($schedule !== null) { 78 | $report->schedule = Schedule::fromModel($schedule, $report); 79 | } 80 | 81 | return $report; 82 | } 83 | 84 | /** 85 | * @return int 86 | */ 87 | public function getId() 88 | { 89 | return $this->id; 90 | } 91 | 92 | /** 93 | * @return string 94 | */ 95 | public function getName() 96 | { 97 | return $this->name; 98 | } 99 | 100 | /** 101 | * @return string 102 | */ 103 | public function getAuthor() 104 | { 105 | return $this->author; 106 | } 107 | 108 | /** 109 | * @return Timeframe 110 | */ 111 | public function getTimeframe() 112 | { 113 | return $this->timeframe; 114 | } 115 | 116 | /** 117 | * @return Reportlet[] 118 | */ 119 | public function getReportlets() 120 | { 121 | return $this->reportlets; 122 | } 123 | 124 | /** 125 | * @return Schedule 126 | */ 127 | public function getSchedule() 128 | { 129 | return $this->schedule; 130 | } 131 | 132 | /** 133 | * @return Template 134 | */ 135 | public function getTemplate() 136 | { 137 | return $this->template; 138 | } 139 | 140 | public function providesData() 141 | { 142 | foreach ($this->getReportlets() as $reportlet) { 143 | $implementation = $reportlet->getImplementation(); 144 | 145 | if ($implementation->providesData()) { 146 | return true; 147 | } 148 | } 149 | 150 | return false; 151 | } 152 | 153 | /** 154 | * @return HtmlDocument 155 | */ 156 | public function toHtml() 157 | { 158 | $timerange = $this->getTimeframe()->getTimerange(); 159 | 160 | $html = new HtmlDocument(); 161 | 162 | foreach ($this->getReportlets() as $reportlet) { 163 | $implementation = $reportlet->getImplementation(); 164 | 165 | $html->add($implementation->getHtml($timerange, $reportlet->getConfig())); 166 | } 167 | 168 | return $html; 169 | } 170 | 171 | /** 172 | * @return string 173 | */ 174 | public function toCsv() 175 | { 176 | $timerange = $this->getTimeframe()->getTimerange(); 177 | $convertFloats = version_compare(PHP_VERSION, '8.0.0', '<'); 178 | 179 | $csv = []; 180 | 181 | foreach ($this->getReportlets() as $reportlet) { 182 | $implementation = $reportlet->getImplementation(); 183 | 184 | if ($implementation->providesData()) { 185 | $data = $implementation->getData($timerange, $reportlet->getConfig()); 186 | $csv[] = array_merge($data->getDimensions(), $data->getValues()); 187 | 188 | $hosts = []; 189 | $isServiceExport = false; 190 | $config = $reportlet->getConfig(); 191 | $exportTotalEnabled = isset($config['export_total']) && $config['export_total']; 192 | if ($exportTotalEnabled) { 193 | $isServiceExport = $reportlet->getClass() === ServiceSlaReport::class; 194 | } 195 | 196 | foreach ($data->getRows() as $row) { 197 | $values = $row->getValues(); 198 | if ($convertFloats) { 199 | foreach ($values as &$value) { 200 | if (is_float($value)) { 201 | $value = sprintf('%.4F', $value); 202 | } 203 | } 204 | } 205 | 206 | if ($isServiceExport) { 207 | $hosts[$row->getDimensions()[0]] = true; 208 | } 209 | 210 | $csv[] = array_merge($row->getDimensions(), $values); 211 | } 212 | 213 | if ($exportTotalEnabled) { 214 | $precision = $config['sla_precision'] ?? SlaReport::DEFAULT_REPORT_PRECISION; 215 | $total = [$isServiceExport ? count($hosts) : $data->count()]; 216 | if ($isServiceExport) { 217 | $total[] = $data->count(); 218 | } 219 | $total[] = round($data->getAverages()[0], $precision); 220 | 221 | $csv[] = $total; 222 | } 223 | 224 | break; 225 | } 226 | } 227 | 228 | return Str::putcsv($csv); 229 | } 230 | 231 | /** 232 | * @return string 233 | */ 234 | public function toJson() 235 | { 236 | $timerange = $this->getTimeframe()->getTimerange(); 237 | 238 | $json = []; 239 | 240 | foreach ($this->getReportlets() as $reportlet) { 241 | $implementation = $reportlet->getImplementation(); 242 | 243 | if ($implementation->providesData()) { 244 | $data = $implementation->getData($timerange, $reportlet->getConfig()); 245 | $dimensions = $data->getDimensions(); 246 | $values = $data->getValues(); 247 | 248 | $hosts = []; 249 | $isServiceExport = false; 250 | $config = $reportlet->getConfig(); 251 | $exportTotalEnabled = isset($config['export_total']) && $config['export_total']; 252 | if ($exportTotalEnabled) { 253 | $isServiceExport = $reportlet->getClass() === ServiceSlaReport::class; 254 | } 255 | 256 | foreach ($data->getRows() as $row) { 257 | $json[] = array_combine($dimensions, $row->getDimensions()) 258 | + array_combine($values, $row->getValues()); 259 | 260 | if ($isServiceExport) { 261 | $hosts[$row->getDimensions()[0]] = true; 262 | } 263 | } 264 | 265 | if ($exportTotalEnabled) { 266 | $total = [t('Total Hosts') => $isServiceExport ? count($hosts) : $data->count()]; 267 | if ($isServiceExport) { 268 | $total[t('Total Services')] = $data->count(); 269 | } 270 | 271 | $precision = $config['sla_precision'] ?? SlaReport::DEFAULT_REPORT_PRECISION; 272 | $total[t('Total SLA Averages')] = round($data->getAverages()[0], $precision); 273 | 274 | $json[] = $total; 275 | } 276 | 277 | break; 278 | } 279 | } 280 | 281 | return json_encode($json); 282 | } 283 | 284 | /** 285 | * @return PrintableHtmlDocument 286 | * 287 | * @throws Exception 288 | */ 289 | public function toPdf() 290 | { 291 | $html = (new PrintableHtmlDocument()) 292 | ->setTitle($this->getName()) 293 | ->addAttributes(['class' => 'icinga-module module-reporting']) 294 | ->addHtml($this->toHtml()); 295 | 296 | if ($this->template !== null) { 297 | $this->template->setMacros([ 298 | 'title' => $this->name, 299 | 'date' => (new DateTime())->format('jS M, Y'), 300 | 'time_frame' => $this->timeframe->getName(), 301 | 'time_frame_absolute' => sprintf( 302 | 'From %s to %s', 303 | $this->timeframe->getTimerange()->getStart()->format('r'), 304 | $this->timeframe->getTimerange()->getEnd()->format('r') 305 | ) 306 | ]); 307 | 308 | $html->setCoverPage($this->template->getCoverPage()->setMacros($this->template->getMacros())); 309 | $html->setHeader($this->template->getHeader()->setMacros($this->template->getMacros())); 310 | $html->setFooter($this->template->getFooter()->setMacros($this->template->getMacros())); 311 | } 312 | 313 | return $html; 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /library/Reporting/ReportData.php: -------------------------------------------------------------------------------- 1 | rows; 18 | } 19 | 20 | public function setRows(array $rows) 21 | { 22 | $this->rows = $rows; 23 | 24 | return $this; 25 | } 26 | 27 | public function getAverages() 28 | { 29 | $totals = $this->getTotals(); 30 | $averages = []; 31 | $count = \count($this); 32 | 33 | foreach ($totals as $total) { 34 | $averages[] = $total / $count; 35 | } 36 | 37 | return $averages; 38 | } 39 | 40 | // public function getMaximums() 41 | // { 42 | // } 43 | 44 | // public function getMinimums() 45 | // { 46 | // } 47 | 48 | public function getTotals() 49 | { 50 | $totals = []; 51 | 52 | foreach ((array) $this->getRows() as $row) { 53 | $i = 0; 54 | foreach ((array) $row->getValues() as $value) { 55 | if (! isset($totals[$i])) { 56 | $totals[$i] = $value; 57 | } else { 58 | $totals[$i] += $value; 59 | } 60 | 61 | ++$i; 62 | } 63 | } 64 | 65 | return $totals; 66 | } 67 | 68 | public function count(): int 69 | { 70 | return count((array) $this->getRows()); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /library/Reporting/ReportRow.php: -------------------------------------------------------------------------------- 1 | class = $reportletModel->class; 26 | 27 | $reportletConfig = [ 28 | 'name' => $reportletModel->report_name, 29 | 'id' => $reportletModel->report_id 30 | ]; 31 | 32 | foreach ($reportletModel->config as $config) { 33 | $reportletConfig[$config->name] = $config->value; 34 | } 35 | 36 | $reportlet->config = $reportletConfig; 37 | 38 | return $reportlet; 39 | } 40 | 41 | /** 42 | * @return string 43 | */ 44 | public function getClass() 45 | { 46 | return $this->class; 47 | } 48 | 49 | /** 50 | * @return array 51 | */ 52 | public function getConfig() 53 | { 54 | return $this->config; 55 | } 56 | 57 | /** 58 | * @return \Icinga\Module\Reporting\Hook\ReportHook 59 | */ 60 | public function getImplementation() 61 | { 62 | $class = $this->getClass(); 63 | 64 | return new $class(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /library/Reporting/Reports/SystemReport.php: -------------------------------------------------------------------------------- 1 | isCli()) { 27 | $doc = new \DOMDocument(); 28 | @$doc->loadHTML($html); 29 | 30 | $style = $doc->getElementsByTagName('style')->item(0); 31 | $style->parentNode->removeChild($style); 32 | 33 | $title = $doc->getElementsByTagName('title')->item(0); 34 | $title->parentNode->removeChild($title); 35 | 36 | $meta = $doc->getElementsByTagName('meta')->item(0); 37 | $meta->parentNode->removeChild($meta); 38 | 39 | $doc->getElementsByTagName('div')->item(0)->setAttribute('class', 'system-report'); 40 | 41 | $html = $doc->saveHTML(); 42 | } else { 43 | $html = nl2br($html); 44 | } 45 | 46 | return new HtmlString($html); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /library/Reporting/RetryConnection.php: -------------------------------------------------------------------------------- 1 | getMessage(), [ 17 | 'server has gone away', 18 | 'no connection to the server', 19 | 'Lost connection', 20 | 'Error while sending', 21 | 'is dead or not enabled', 22 | 'decryption failed or bad record mac', 23 | 'server closed the connection unexpectedly', 24 | 'SSL connection has been closed unexpectedly', 25 | 'Error writing data to the connection', 26 | 'Resource deadlock avoided', 27 | 'Transaction() on null', 28 | 'child connection forced to terminate due to client_idle_limit', 29 | 'query_wait_timeout', 30 | 'reset by peer', 31 | 'Physical connection is not usable', 32 | 'TCP Provider: Error code 0x68', 33 | 'ORA-03114', 34 | 'Packets out of order. Expected', 35 | 'Adaptive Server connection failed', 36 | 'Communication link failure', 37 | ]); 38 | 39 | if (! $lostConnection) { 40 | throw $e; 41 | } 42 | 43 | $this->disconnect(); 44 | 45 | try { 46 | $this->connect(); 47 | } catch (\Exception $e) { 48 | $noConnection = Str::contains($e->getMessage(), [ 49 | 'No such file or directory', 50 | 'Connection refused' 51 | ]); 52 | 53 | if (! $noConnection) { 54 | throw $e; 55 | } 56 | 57 | \sleep(10); 58 | 59 | $this->connect(); 60 | } 61 | 62 | $sth = parent::prepexec($stmt, $values); 63 | } 64 | 65 | return $sth; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /library/Reporting/Schedule.php: -------------------------------------------------------------------------------- 1 | action = $action; 38 | $this->config = $config; 39 | ksort($this->config); 40 | 41 | $this 42 | ->setName($name) 43 | ->setReport($report) 44 | ->setUuid(Uuid::fromBytes($this->getChecksum())); 45 | } 46 | 47 | /** 48 | * Create schedule from the given model 49 | * 50 | * @param Model\Schedule $scheduleModel 51 | * 52 | * @return static 53 | */ 54 | 55 | public static function fromModel(Model\Schedule $scheduleModel, Report $report): self 56 | { 57 | $config = Json::decode($scheduleModel->config ?? [], true); 58 | $schedule = new static("Schedule{$scheduleModel->id}", $scheduleModel->action, $config, $report); 59 | $schedule->id = $scheduleModel->id; 60 | 61 | return $schedule; 62 | } 63 | 64 | /** 65 | * Get the id of this schedule 66 | * 67 | * @return int 68 | */ 69 | public function getId(): int 70 | { 71 | return $this->id; 72 | } 73 | 74 | /** 75 | * Get the action hook class of this schedule 76 | * 77 | * @return string 78 | */ 79 | public function getAction(): string 80 | { 81 | return $this->action; 82 | } 83 | 84 | /** 85 | * Get the config of this schedule 86 | * 87 | * @return array 88 | */ 89 | public function getConfig(): array 90 | { 91 | return $this->config; 92 | } 93 | 94 | /** 95 | * Get the report this schedule belongs to 96 | * 97 | * @return Report 98 | */ 99 | public function getReport(): Report 100 | { 101 | return $this->report; 102 | } 103 | 104 | /** 105 | * Set the report this schedule belongs to 106 | * 107 | * @param Report $report 108 | * 109 | * @return $this 110 | */ 111 | public function setReport(Report $report): self 112 | { 113 | $this->report = $report; 114 | 115 | return $this; 116 | } 117 | 118 | /** 119 | * Get the checksum of this schedule 120 | * 121 | * @return string 122 | */ 123 | public function getChecksum(): string 124 | { 125 | return md5( 126 | $this->getName() . $this->getReport()->getName() . $this->getAction() . Json::encode($this->getConfig()), 127 | true 128 | ); 129 | } 130 | 131 | public function run(): ExtendedPromiseInterface 132 | { 133 | $deferred = new Promise\Deferred(); 134 | Loop::futureTick(function () use ($deferred) { 135 | $action = $this->getAction(); 136 | /** @var ActionHook $actionHook */ 137 | $actionHook = new $action(); 138 | 139 | try { 140 | $actionHook->execute($this->getReport(), $this->getConfig()); 141 | } catch (Exception $err) { 142 | $deferred->reject($err); 143 | 144 | return; 145 | } 146 | 147 | $deferred->resolve(); 148 | }); 149 | 150 | /** @var ExtendedPromiseInterface $promise */ 151 | $promise = $deferred->promise(); 152 | 153 | return $promise; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /library/Reporting/Str.php: -------------------------------------------------------------------------------- 1 | id = $timeframeModel->id; 38 | $timeframe->name = $timeframeModel->name; 39 | $timeframe->title = $timeframeModel->title; 40 | $timeframe->start = $timeframeModel->start; 41 | $timeframe->end = $timeframeModel->end; 42 | 43 | return $timeframe; 44 | } 45 | 46 | /** 47 | * @return int 48 | */ 49 | public function getId() 50 | { 51 | return $this->id; 52 | } 53 | 54 | /** 55 | * @return string 56 | */ 57 | public function getName() 58 | { 59 | return $this->name; 60 | } 61 | 62 | /** 63 | * @return string 64 | */ 65 | public function getTitle() 66 | { 67 | return $this->title; 68 | } 69 | 70 | /** 71 | * @return string 72 | */ 73 | public function getStart() 74 | { 75 | return $this->start; 76 | } 77 | 78 | /** 79 | * @return string 80 | */ 81 | public function getEnd() 82 | { 83 | return $this->end; 84 | } 85 | 86 | public function getTimerange() 87 | { 88 | $start = new \DateTime($this->getStart()); 89 | $end = new \DateTime($this->getEnd()); 90 | 91 | return new Timerange($start, $end); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /library/Reporting/Timerange.php: -------------------------------------------------------------------------------- 1 | start = $start; 18 | $this->end = $end; 19 | } 20 | 21 | /** 22 | * @return \DateTime 23 | */ 24 | public function getStart() 25 | { 26 | return $this->start; 27 | } 28 | 29 | /** 30 | * @return \DateTime 31 | */ 32 | public function getEnd() 33 | { 34 | return $this->end; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /library/Reporting/Values.php: -------------------------------------------------------------------------------- 1 | values; 14 | } 15 | 16 | public function setValues(array $values) 17 | { 18 | $this->values = $values; 19 | 20 | return $this; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /library/Reporting/Web/Controller.php: -------------------------------------------------------------------------------- 1 | db = $db; 37 | } 38 | 39 | /** 40 | * Create a new form instance with the given report id 41 | * 42 | * @param int $id 43 | * @param RetryConnection $db 44 | * 45 | * @return static 46 | */ 47 | public static function fromId(int $id, RetryConnection $db): self 48 | { 49 | $form = new static($db); 50 | $form->id = $id; 51 | 52 | return $form; 53 | } 54 | 55 | public function getId(): ?int 56 | { 57 | return $this->id; 58 | } 59 | 60 | /** 61 | * Set the label of the submit button 62 | * 63 | * @param string $label 64 | * 65 | * @return $this 66 | */ 67 | public function setSubmitButtonLabel(string $label): self 68 | { 69 | $this->submitButtonLabel = $label; 70 | 71 | return $this; 72 | } 73 | 74 | /** 75 | * Get the label of the submit button 76 | * 77 | * @return string 78 | */ 79 | public function getSubmitButtonLabel(): string 80 | { 81 | if ($this->submitButtonLabel !== null) { 82 | return $this->submitButtonLabel; 83 | } 84 | 85 | return $this->id === null ? $this->translate('Create Report') : $this->translate('Update Report'); 86 | } 87 | 88 | /** 89 | * Set whether the create and show submit button should be rendered 90 | * 91 | * @param bool $renderCreateAndShowButton 92 | * 93 | * @return $this 94 | */ 95 | public function setRenderCreateAndShowButton(bool $renderCreateAndShowButton): self 96 | { 97 | $this->renderCreateAndShowButton = $renderCreateAndShowButton; 98 | 99 | return $this; 100 | } 101 | 102 | public function hasBeenSubmitted(): bool 103 | { 104 | return $this->hasBeenSent() && ( 105 | $this->getPopulatedValue('submit') 106 | || $this->getPopulatedValue('create_show') 107 | || $this->getPopulatedValue('remove') 108 | ); 109 | } 110 | 111 | protected function assemble(): void 112 | { 113 | $this->addElement('text', 'name', [ 114 | 'required' => true, 115 | 'label' => $this->translate('Name'), 116 | 'description' => $this->translate( 117 | 'A unique name of this report. It is used when exporting to pdf, json or csv format' 118 | . ' and also when listing the reports in the cli' 119 | ), 120 | 'validators' => [ 121 | 'Callback' => function ($value, CallbackValidator $validator) { 122 | if (strpos($value, '..') !== false) { 123 | $validator->addMessage( 124 | $this->translate('Double dots are not allowed in the report name') 125 | ); 126 | 127 | return false; 128 | } 129 | 130 | $filter = Filter::all(Filter::equal('name', $value)); 131 | if ($this->id) { 132 | $filter->add(Filter::unequal('id', $this->id)); 133 | } 134 | 135 | $report = Report::on($this->db) 136 | ->columns('1') 137 | ->filter($filter) 138 | ->first(); 139 | 140 | if ($report !== null) { 141 | $validator->addMessage( 142 | $this->translate('A report with this name already exists') 143 | ); 144 | 145 | return false; 146 | } 147 | 148 | return true; 149 | } 150 | ] 151 | ]); 152 | 153 | $this->addElement('select', 'timeframe', [ 154 | 'required' => true, 155 | 'class' => 'autosubmit', 156 | 'label' => $this->translate('Timeframe'), 157 | 'options' => [null => $this->translate('Please choose')] + Database::listTimeframes(), 158 | 'description' => $this->translate( 159 | 'Specifies the time frame in which this report is to be generated' 160 | ) 161 | ]); 162 | 163 | $this->addElement('select', 'template', [ 164 | 'label' => $this->translate('Template'), 165 | 'options' => [null => $this->translate('Please choose')] + Database::listTemplates(), 166 | 'description' => $this->translate( 167 | 'Specifies the template to use when exporting this report to pdf. (Default Icinga template)' 168 | ) 169 | ]); 170 | 171 | $this->addElement('select', 'reportlet', [ 172 | 'required' => true, 173 | 'class' => 'autosubmit', 174 | 'label' => $this->translate('Report'), 175 | 'options' => [null => $this->translate('Please choose')] + $this->listReports(), 176 | 'description' => $this->translate('Specifies the type of the reportlet to be generated') 177 | ]); 178 | 179 | $values = $this->getValues(); 180 | 181 | if (isset($values['reportlet'])) { 182 | $config = new Form(); 183 | 184 | /** @var \Icinga\Module\Reporting\Hook\ReportHook $reportlet */ 185 | $reportlet = new $values['reportlet'](); 186 | 187 | $reportlet->initConfigForm($config); 188 | 189 | foreach ($config->getElements() as $element) { 190 | $this->addElement($element); 191 | } 192 | } 193 | 194 | $this->addElement('submit', 'submit', [ 195 | 'label' => $this->getSubmitButtonLabel() 196 | ]); 197 | 198 | if ($this->id !== null) { 199 | $removeButton = $this->createElement('submit', 'remove', [ 200 | 'label' => $this->translate('Remove Report'), 201 | 'class' => 'btn-remove', 202 | 'formnovalidate' => true 203 | ]); 204 | $this->registerElement($removeButton); 205 | 206 | /** @var HtmlDocument $wrapper */ 207 | $wrapper = $this->getElement('submit')->getWrapper(); 208 | $wrapper->prepend($removeButton); 209 | } elseif ($this->renderCreateAndShowButton) { 210 | $createAndShow = $this->createElement('submit', 'create_show', [ 211 | 'label' => $this->translate('Create and Show'), 212 | ]); 213 | $this->registerElement($createAndShow); 214 | 215 | /** @var HtmlDocument $wrapper */ 216 | $wrapper = $this->getElement('submit')->getWrapper(); 217 | $wrapper->prepend($createAndShow); 218 | } 219 | } 220 | 221 | public function onSuccess(): void 222 | { 223 | $db = Database::get(); 224 | 225 | if ($this->getPopulatedValue('remove')) { 226 | $db->delete('report', ['id = ?' => $this->id]); 227 | 228 | return; 229 | } 230 | 231 | $values = $this->getValues(); 232 | 233 | $now = time() * 1000; 234 | 235 | $db->beginTransaction(); 236 | 237 | if ($this->id === null) { 238 | $db->insert('report', [ 239 | 'name' => $values['name'], 240 | 'author' => Auth::getInstance()->getUser()->getUsername(), 241 | 'timeframe_id' => $values['timeframe'], 242 | 'template_id' => $values['template'], 243 | 'ctime' => $now, 244 | 'mtime' => $now 245 | ]); 246 | 247 | $reportId = $db->lastInsertId(); 248 | } else { 249 | $db->update('report', [ 250 | 'name' => $values['name'], 251 | 'timeframe_id' => $values['timeframe'], 252 | 'template_id' => $values['template'], 253 | 'mtime' => $now 254 | ], ['id = ?' => $this->id]); 255 | 256 | $reportId = $this->id; 257 | } 258 | 259 | unset($values['name']); 260 | unset($values['timeframe']); 261 | 262 | if ($this->id !== null) { 263 | $db->delete('reportlet', ['report_id = ?' => $reportId]); 264 | } 265 | 266 | $db->insert('reportlet', [ 267 | 'report_id' => $reportId, 268 | 'class' => $values['reportlet'], 269 | 'ctime' => $now, 270 | 'mtime' => $now 271 | ]); 272 | 273 | $reportletId = $db->lastInsertId(); 274 | 275 | unset($values['reportlet']); 276 | 277 | foreach ($values as $name => $value) { 278 | $db->insert('config', [ 279 | 'reportlet_id' => $reportletId, 280 | 'name' => $name, 281 | 'value' => $value, 282 | 'ctime' => $now, 283 | 'mtime' => $now 284 | ]); 285 | } 286 | 287 | $db->commitTransaction(); 288 | 289 | $this->id = $reportId; 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /library/Reporting/Web/Forms/ScheduleForm.php: -------------------------------------------------------------------------------- 1 | scheduleElement = new ScheduleElement('schedule_element'); 38 | /** @var Web $app */ 39 | $app = Icinga::app(); 40 | $this->scheduleElement->setIdProtector([$app->getRequest(), 'protectId']); 41 | } 42 | 43 | public function getPartUpdates(): array 44 | { 45 | return $this->scheduleElement->prepareMultipartUpdate($this->getRequest()); 46 | } 47 | 48 | /** 49 | * Create a new form instance with the given report 50 | * 51 | * @param Report $report 52 | * 53 | * @return static 54 | */ 55 | public static function fromReport(Report $report): self 56 | { 57 | $form = new static(); 58 | $form->report = $report; 59 | 60 | $schedule = $report->getSchedule(); 61 | if ($schedule !== null) { 62 | $config = $schedule->getConfig(); 63 | $config['action'] = $schedule->getAction(); 64 | 65 | /** @var Frequency $type */ 66 | $type = $config['frequencyType']; 67 | $config['schedule_element'] = $type::fromJson($config['frequency']); 68 | 69 | unset($config['frequency']); 70 | unset($config['frequencyType']); 71 | 72 | $form->populate($config); 73 | } 74 | 75 | return $form; 76 | } 77 | 78 | public function hasBeenSubmitted(): bool 79 | { 80 | return $this->hasBeenSent() && ( 81 | $this->getPopulatedValue('submit') 82 | || $this->getPopulatedValue('remove') 83 | || $this->getPopulatedValue('send') 84 | ); 85 | } 86 | 87 | protected function assemble() 88 | { 89 | $this->addElement('select', 'action', [ 90 | 'required' => true, 91 | 'class' => 'autosubmit', 92 | 'options' => array_merge([null => $this->translate('Please choose')], $this->listActions()), 93 | 'label' => $this->translate('Action'), 94 | 'description' => $this->translate('Specifies an action to be triggered by the scheduler') 95 | ]); 96 | 97 | $values = $this->getValues(); 98 | 99 | if (isset($values['action'])) { 100 | $config = new Form(); 101 | // $config->populate($this->getValues()); 102 | 103 | /** @var ActionHook $action */ 104 | $action = new $values['action'](); 105 | 106 | $action->initConfigForm($config, $this->report); 107 | 108 | foreach ($config->getElements() as $element) { 109 | $this->addElement($element); 110 | } 111 | } 112 | 113 | $this->addHtml(HtmlElement::create('div', ['class' => 'schedule-element-separator'])); 114 | $this->addElement($this->scheduleElement); 115 | 116 | $schedule = $this->report->getSchedule(); 117 | $this->addElement('submit', 'submit', [ 118 | 'label' => $schedule === null ? $this->translate('Create Schedule') : $this->translate('Update Schedule') 119 | ]); 120 | 121 | if ($schedule !== null) { 122 | $sendButton = $this->createElement('submit', 'send', [ 123 | 'label' => $this->translate('Send Report Now'), 124 | 'formnovalidate' => true 125 | ]); 126 | $this->registerElement($sendButton); 127 | 128 | /** @var HtmlDocument $wrapper */ 129 | $wrapper = $this->getElement('submit')->getWrapper(); 130 | $wrapper->prepend($sendButton); 131 | 132 | $removeButton = $this->createElement('submit', 'remove', [ 133 | 'label' => $this->translate('Remove Schedule'), 134 | 'class' => 'btn-remove', 135 | 'formnovalidate' => true 136 | ]); 137 | $this->registerElement($removeButton); 138 | $wrapper->prepend($removeButton); 139 | } 140 | } 141 | 142 | public function onSuccess() 143 | { 144 | $db = Database::get(); 145 | $schedule = $this->report->getSchedule(); 146 | if ($this->getPopulatedValue('remove')) { 147 | $db->delete('schedule', ['id = ?' => $schedule->getId()]); 148 | 149 | return; 150 | } 151 | 152 | $values = $this->getValues(); 153 | if ($this->getPopulatedValue('send')) { 154 | $action = new $values['action'](); 155 | $action->execute($this->report, $values); 156 | 157 | return; 158 | } 159 | 160 | $action = $values['action']; 161 | unset($values['action']); 162 | unset($values['schedule_element']); 163 | 164 | $frequency = $this->scheduleElement->getValue(); 165 | $values['frequency'] = Json::encode($frequency); 166 | $values['frequencyType'] = get_php_type($frequency); 167 | $config = Json::encode($values); 168 | 169 | $db->beginTransaction(); 170 | 171 | if ($schedule === null) { 172 | $now = (new DateTime())->getTimestamp() * 1000; 173 | $db->insert('schedule', [ 174 | 'author' => Auth::getInstance()->getUser()->getUsername(), 175 | 'report_id' => $this->report->getId(), 176 | 'ctime' => $now, 177 | 'mtime' => $now, 178 | 'action' => $action, 179 | 'config' => $config 180 | ]); 181 | } else { 182 | $db->update('schedule', [ 183 | 'action' => $action, 184 | 'config' => $config 185 | ], ['id = ?' => $schedule->getId()]); 186 | } 187 | 188 | $db->commitTransaction(); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /library/Reporting/Web/Forms/SendForm.php: -------------------------------------------------------------------------------- 1 | report = $report; 22 | 23 | return $this; 24 | } 25 | 26 | protected function assemble() 27 | { 28 | (new SendMail())->initConfigForm($this, $this->report); 29 | 30 | $this->addElement('submit', 'submit', [ 31 | 'label' => $this->translate('Send Report') 32 | ]); 33 | } 34 | 35 | public function onSuccess() 36 | { 37 | $values = $this->getValues(); 38 | 39 | $sendMail = new SendMail(); 40 | 41 | $sendMail->execute($this->report, $values); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /library/Reporting/Web/Forms/TemplateForm.php: -------------------------------------------------------------------------------- 1 | template; 24 | } 25 | 26 | /** 27 | * Create a new form instance with the given report 28 | * 29 | * @param $template 30 | * 31 | * @return static 32 | */ 33 | public static function fromTemplate($template): self 34 | { 35 | $form = new static(); 36 | 37 | $template->settings = Json::decode($template->settings, true); 38 | $form->template = $template; 39 | 40 | if ($template->settings) { 41 | /** @var array $settings */ 42 | $settings = $template->settings; 43 | $form->populate(array_filter($settings, function ($value) { 44 | // Don't populate files 45 | return ! is_array($value); 46 | })); 47 | } 48 | 49 | return $form; 50 | } 51 | 52 | public function hasBeenSubmitted(): bool 53 | { 54 | return $this->hasBeenSent() && ($this->getPopulatedValue('submit') || $this->getPopulatedValue('remove')); 55 | } 56 | 57 | protected function assemble() 58 | { 59 | $this->setAttribute('enctype', 'multipart/form-data'); 60 | 61 | $this->add(Html::tag('h2', 'Template Settings')); 62 | 63 | $this->addElement('text', 'name', [ 64 | 'label' => $this->translate('Name'), 65 | 'placeholder' => $this->translate('Template name'), 66 | 'required' => true 67 | ]); 68 | 69 | $this->add(Html::tag('h2', $this->translate('Cover Page Settings'))); 70 | 71 | $this->addElement('file', 'cover_page_background_image', [ 72 | 'label' => $this->translate('Background Image'), 73 | 'accept' => ['image/png', 'image/jpeg', 'image/jpg'], 74 | 'destination' => sys_get_temp_dir() 75 | ]); 76 | 77 | if ( 78 | $this->template !== null 79 | && isset($this->template->settings['cover_page_background_image']) 80 | ) { 81 | $this->add(Html::tag( 82 | 'p', 83 | ['class' => 'override-uploaded-file-hint'], 84 | $this->translate('Upload a new background image to override the existing one') 85 | )); 86 | 87 | $this->addElement('checkbox', 'remove_cover_page_background_image', [ 88 | 'label' => $this->translate('Remove background image') 89 | ]); 90 | } 91 | 92 | $this->addElement('file', 'cover_page_logo', [ 93 | 'label' => $this->translate('Logo'), 94 | 'accept' => ['image/png', 'image/jpeg', 'image/jpg'], 95 | 'destination' => sys_get_temp_dir() 96 | ]); 97 | 98 | if ( 99 | $this->template !== null 100 | && isset($this->template->settings['cover_page_logo']) 101 | ) { 102 | $this->add(Html::tag( 103 | 'p', 104 | ['class' => 'override-uploaded-file-hint'], 105 | $this->translate('Upload a new logo to override the existing one') 106 | )); 107 | 108 | $this->addElement('checkbox', 'remove_cover_page_logo', [ 109 | 'label' => $this->translate('Remove Logo') 110 | ]); 111 | } 112 | 113 | $this->addElement('textarea', 'title', [ 114 | 'label' => $this->translate('Title'), 115 | 'placeholder' => $this->translate('Report title') 116 | ]); 117 | 118 | $this->addElement('text', 'color', [ 119 | 'label' => $this->translate('Color'), 120 | 'placeholder' => $this->translate('CSS color code'), 121 | 'validators' => [new CallbackValidator(function ($value, $validator) { 122 | if (strpos($value, ':') !== false) { 123 | $validator->addMessage($this->translate('Please enter a valid CSS color code')); 124 | 125 | return false; 126 | } 127 | 128 | return true; 129 | })] 130 | ]); 131 | 132 | $this->add(Html::tag('h2', $this->translate('Header Settings'))); 133 | 134 | $this->addColumnSettings('header_column1', $this->translate('Column 1')); 135 | $this->addColumnSettings('header_column2', $this->translate('Column 2')); 136 | $this->addColumnSettings('header_column3', $this->translate('Column 3')); 137 | 138 | $this->add(Html::tag('h2', $this->translate('Footer Settings'))); 139 | 140 | $this->addColumnSettings('footer_column1', $this->translate('Column 1')); 141 | $this->addColumnSettings('footer_column2', $this->translate('Column 2')); 142 | $this->addColumnSettings('footer_column3', $this->translate('Column 3')); 143 | 144 | $this->addElement('submit', 'submit', [ 145 | 'label' => $this->template === null 146 | ? $this->translate('Create Template') 147 | : $this->translate('Update Template') 148 | ]); 149 | 150 | if ($this->template !== null) { 151 | $removeButton = $this->createElement('submit', 'remove', [ 152 | 'label' => $this->translate('Remove Template'), 153 | 'class' => 'btn-remove', 154 | 'formnovalidate' => true 155 | ]); 156 | $this->registerElement($removeButton); 157 | 158 | /** @var HtmlDocument $wrapper */ 159 | $wrapper = $this->getElement('submit')->getWrapper(); 160 | $wrapper->prepend($removeButton); 161 | } 162 | } 163 | 164 | public function onSuccess() 165 | { 166 | if ($this->getPopulatedValue('remove')) { 167 | Database::get()->delete('template', ['id = ?' => $this->template->id]); 168 | 169 | return; 170 | } 171 | 172 | ini_set('upload_max_filesize', '10M'); 173 | 174 | $settings = $this->getValues(); 175 | 176 | try { 177 | foreach ($settings as $name => $setting) { 178 | if ($setting instanceof UploadedFile) { 179 | $settings[$name] = [ 180 | 'mime_type' => $setting->getClientMediaType(), 181 | 'size' => $setting->getSize(), 182 | 'content' => base64_encode((string) $setting->getStream()) 183 | ]; 184 | } 185 | } 186 | 187 | $db = Database::get(); 188 | 189 | $now = time() * 1000; 190 | 191 | if ($this->template === null) { 192 | $db->insert('template', [ 193 | 'name' => $settings['name'], 194 | 'author' => Auth::getInstance()->getUser()->getUsername(), 195 | 'settings' => json_encode($settings), 196 | 'ctime' => $now, 197 | 'mtime' => $now 198 | ]); 199 | } else { 200 | if ($this->getValue('remove_cover_page_background_image', 'n') === 'y') { 201 | unset($settings['cover_page_background_image']); 202 | unset($settings['remove_cover_page_background_image']); 203 | } elseif ( 204 | ! isset($settings['cover_page_background_image']) 205 | && isset($this->template->settings['cover_page_background_image']) 206 | ) { 207 | $settings['cover_page_background_image'] = $this->template->settings['cover_page_background_image']; 208 | } 209 | 210 | if ($this->getValue('remove_cover_page_logo', 'n') === 'y') { 211 | unset($settings['cover_page_logo']); 212 | unset($settings['remove_cover_page_logo']); 213 | } elseif ( 214 | ! isset($settings['cover_page_logo']) 215 | && isset($this->template->settings['cover_page_logo']) 216 | ) { 217 | $settings['cover_page_logo'] = $this->template->settings['cover_page_logo']; 218 | } 219 | 220 | foreach (['header', 'footer'] as $headerOrFooter) { 221 | for ($i = 1; $i <= 3; ++$i) { 222 | $type = "{$headerOrFooter}_column{$i}_type"; 223 | 224 | if ($settings[$type] === 'image') { 225 | $value = "{$headerOrFooter}_column{$i}_value"; 226 | 227 | if ( 228 | ! isset($settings[$value]) 229 | && isset($this->template->settings[$value]) 230 | ) { 231 | $settings[$value] = $this->template->settings[$value]; 232 | } 233 | } 234 | } 235 | } 236 | 237 | $db->update('template', [ 238 | 'name' => $settings['name'], 239 | 'settings' => json_encode($settings), 240 | 'mtime' => $now 241 | ], ['id = ?' => $this->template->id]); 242 | } 243 | } catch (Exception $e) { 244 | die($e->getMessage()); 245 | } 246 | } 247 | 248 | protected function addColumnSettings($name, $label) 249 | { 250 | $type = "{$name}_type"; 251 | $value = "{$name}_value"; 252 | 253 | $this->addElement('select', $type, [ 254 | 'class' => 'autosubmit', 255 | 'label' => $label, 256 | 'options' => [ 257 | null => 'None', 258 | 'text' => 'Text', 259 | 'image' => 'Image', 260 | 'variable' => 'Variable' 261 | ] 262 | ]); 263 | 264 | $valueType = $this->getValue($type, 'none'); 265 | $populated = $this->getPopulatedValue($value); 266 | if ( 267 | ($valueType === 'image' && ! $populated instanceof UploadedFile) 268 | || ($valueType !== 'image' && $populated instanceof UploadedFile) 269 | ) { 270 | $this->clearPopulatedValue($value); 271 | } 272 | 273 | switch ($this->getValue($type, 'none')) { 274 | case 'image': 275 | $this->addElement('file', $value, [ 276 | 'label' => 'Image', 277 | 'accept' => ['image/png', 'image/jpeg', 'image/jpg'], 278 | 'destination' => sys_get_temp_dir() 279 | ]); 280 | 281 | if ( 282 | $this->template !== null 283 | && $this->template->settings[$type] === 'image' 284 | && isset($this->template->settings[$value]) 285 | ) { 286 | $this->add(Html::tag( 287 | 'p', 288 | ['class' => 'override-uploaded-file-hint'], 289 | 'Upload a new image to override the existing one' 290 | )); 291 | } 292 | break; 293 | case 'variable': 294 | $this->addElement('select', $value, [ 295 | 'label' => 'Variable', 296 | 'options' => [ 297 | 'report_title' => 'Report Title', 298 | 'time_frame' => 'Time Frame', 299 | 'time_frame_absolute' => 'Time Frame (absolute)', 300 | 'page_number' => 'Page Number', 301 | 'total_number_of_pages' => 'Total Number of Pages', 302 | 'page_of' => 'Page Number + Total Number of Pages', 303 | 'date' => 'Date' 304 | ], 305 | 'value' => 'report_title' 306 | ]); 307 | break; 308 | case 'text': 309 | $this->addElement('text', $value, [ 310 | 'label' => 'Text', 311 | 'placeholder' => 'Column text' 312 | ]); 313 | break; 314 | } 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /library/Reporting/Web/Forms/TimeframeForm.php: -------------------------------------------------------------------------------- 1 | id = $id; 32 | 33 | return $form; 34 | } 35 | 36 | public function hasBeenSubmitted(): bool 37 | { 38 | return $this->hasBeenSent() && ($this->getPopulatedValue('submit') || $this->getPopulatedValue('remove')); 39 | } 40 | 41 | protected function assemble() 42 | { 43 | $this->addElement('text', 'name', [ 44 | 'required' => true, 45 | 'label' => $this->translate('Name'), 46 | 'description' => $this->translate('A unique name of this timeframe') 47 | ]); 48 | 49 | $start = $this->getPopulatedValue('start', new DateTime('00:00:00')); 50 | $canBeConverted = $start instanceof DateTime 51 | || DateTime::createFromFormat(LocalDateTimeElement::FORMAT, $start) !== false; 52 | $relativeStart = $this->getPopulatedValue('relative-start', $canBeConverted ? 'n' : 'y'); 53 | $this->addElement('checkbox', 'relative-start', [ 54 | 'required' => false, 55 | 'class' => 'autosubmit', 56 | 'value' => $relativeStart, 57 | 'label' => $this->translate('Relative Start') 58 | ]); 59 | 60 | if ($relativeStart === 'n') { 61 | if (! $start instanceof DateTime) { 62 | $start = (new DateTime($start))->format(LocalDateTimeElement::FORMAT); 63 | $this->clearPopulatedValue('start'); 64 | } 65 | 66 | $this->addElement( 67 | new LocalDateTimeElement('start', [ 68 | 'required' => true, 69 | 'value' => $start, 70 | 'label' => $this->translate('Start'), 71 | 'description' => $this->translate('Specifies the start time of this timeframe') 72 | ]) 73 | ); 74 | } else { 75 | $this->addElement('text', 'start', [ 76 | 'required' => true, 77 | 'label' => $this->translate('Start'), 78 | 'placeholder' => $this->translate('First day of this month'), 79 | 'description' => $this->translate('Specifies the start time of this timeframe'), 80 | 'validators' => [ 81 | new CallbackValidator(function ($value, CallbackValidator $validator) { 82 | if ($value !== null) { 83 | try { 84 | new DateTime($value); 85 | } catch (Exception $_) { 86 | $validator->addMessage($this->translate('Invalid textual date time')); 87 | 88 | return false; 89 | } 90 | } 91 | 92 | return true; 93 | }) 94 | ] 95 | ]); 96 | } 97 | 98 | $end = $this->getPopulatedValue('end', new DateTime('23:59:59')); 99 | $canBeConverted = $end instanceof DateTime 100 | || DateTime::createFromFormat(LocalDateTimeElement::FORMAT, $end) !== false; 101 | $relativeEnd = $this->getPopulatedValue('relative-end', $canBeConverted ? 'n' : 'y'); 102 | if ($relativeStart === 'y') { 103 | $this->addElement('checkbox', 'relative-end', [ 104 | 'required' => false, 105 | 'class' => 'autosubmit', 106 | 'value' => $relativeEnd, 107 | 'label' => $this->translate('Relative End') 108 | ]); 109 | } 110 | 111 | $endDateValidator = new CallbackValidator(function ($value, CallbackValidator $validator) { 112 | if (! $value instanceof DateTime) { 113 | try { 114 | $value = new DateTime($value); 115 | } catch (Exception $_) { 116 | $validator->addMessage($this->translate('Invalid textual date time')); 117 | 118 | return false; 119 | } 120 | } 121 | 122 | $start = $this->getValue('start'); 123 | if (! $start instanceof DateTime) { 124 | $start = new DateTime($start); 125 | } 126 | 127 | if ($value <= $start) { 128 | $validator->addMessage($this->translate('End time must be greater than start time')); 129 | 130 | return false; 131 | } 132 | 133 | return true; 134 | }); 135 | 136 | if ($relativeEnd === 'n' || $relativeStart === 'n') { 137 | if (! $end instanceof DateTime) { 138 | $end = (new DateTime($end))->format(LocalDateTimeElement::FORMAT); 139 | $this->clearPopulatedValue('end'); 140 | } 141 | 142 | $this->addElement( 143 | new LocalDateTimeElement('end', [ 144 | 'required' => true, 145 | 'value' => $end, 146 | 'label' => $this->translate('End'), 147 | 'description' => $this->translate('Specifies the end time of this timeframe'), 148 | 'validators' => [$endDateValidator] 149 | ]) 150 | ); 151 | } else { 152 | $this->addElement('text', 'end', [ 153 | 'required' => true, 154 | 'label' => $this->translate('End'), 155 | 'placeholder' => $this->translate('Last day of this month'), 156 | 'description' => $this->translate('Specifies the end time of this timeframe'), 157 | 'validators' => [$endDateValidator] 158 | ]); 159 | } 160 | 161 | $this->addElement('submit', 'submit', [ 162 | 'label' => $this->id === null 163 | ? $this->translate('Create Time Frame') 164 | : $this->translate('Update Time Frame') 165 | ]); 166 | 167 | if ($this->id !== null) { 168 | $removeButton = $this->createElement('submit', 'remove', [ 169 | 'label' => $this->translate('Remove Time Frame'), 170 | 'class' => 'btn-remove', 171 | 'formnovalidate' => true 172 | ]); 173 | $this->registerElement($removeButton); 174 | 175 | /** @var HtmlDocument $wrapper */ 176 | $wrapper = $this->getElement('submit')->getWrapper(); 177 | $wrapper->prepend($removeButton); 178 | } 179 | } 180 | 181 | public function onSuccess() 182 | { 183 | $db = Database::get(); 184 | 185 | if ($this->getPopulatedValue('remove')) { 186 | $db->delete('timeframe', ['id = ?' => $this->id]); 187 | 188 | return; 189 | } 190 | 191 | $values = $this->getValues(); 192 | if ($values['start'] instanceof DateTime) { 193 | $values['start'] = $values['start']->format(LocalDateTimeElement::FORMAT); 194 | } 195 | 196 | if ($values['end'] instanceof DateTime) { 197 | $values['end'] = $values['end']->format(LocalDateTimeElement::FORMAT); 198 | } 199 | 200 | $now = time() * 1000; 201 | 202 | $end = $db->quoteIdentifier('end'); 203 | 204 | if ($this->id === null) { 205 | $db->insert('timeframe', [ 206 | 'name' => $values['name'], 207 | 'start' => $values['start'], 208 | $end => $values['end'], 209 | 'ctime' => $now, 210 | 'mtime' => $now 211 | ]); 212 | } else { 213 | $db->update('timeframe', [ 214 | 'name' => $values['name'], 215 | 'start' => $values['start'], 216 | $end => $values['end'], 217 | 'mtime' => $now 218 | ], ['id = ?' => $this->id]); 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /library/Reporting/Web/ReportsTimeframesAndTemplatesTabs.php: -------------------------------------------------------------------------------- 1 | getTabs(); 17 | $tabs->getAttributes()->set('data-base-target', '_main'); 18 | 19 | $tabs->add('reports', [ 20 | 'title' => $this->translate('Show reports'), 21 | 'label' => $this->translate('Reports'), 22 | 'url' => 'reporting/reports' 23 | ]); 24 | 25 | $tabs->add('timeframes', [ 26 | 'title' => $this->translate('Show time frames'), 27 | 'label' => $this->translate('Time Frames'), 28 | 'url' => 'reporting/timeframes' 29 | ]); 30 | 31 | $tabs->add('templates', [ 32 | 'title' => $this->translate('Show templates'), 33 | 'label' => $this->translate('Templates'), 34 | 'url' => 'reporting/templates' 35 | ]); 36 | 37 | return $tabs; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /library/Reporting/Web/Widget/CompatDropdown.php: -------------------------------------------------------------------------------- 1 | 'dropdown-item']); 15 | if (! empty($attributes)) { 16 | $link->addAttributes($attributes); 17 | } 18 | 19 | $this->links[] = $link; 20 | 21 | return $this; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /library/Reporting/Web/Widget/CoverPage.php: -------------------------------------------------------------------------------- 1 | 'cover-page page']; 30 | 31 | /** 32 | * @return bool 33 | */ 34 | public function hasBackgroundImage() 35 | { 36 | return $this->backgroundImage !== null; 37 | } 38 | 39 | /** 40 | * @return ?array 41 | */ 42 | public function getBackgroundImage() 43 | { 44 | return $this->backgroundImage; 45 | } 46 | 47 | /** 48 | * @param ?array $backgroundImage 49 | * 50 | * @return $this 51 | */ 52 | public function setBackgroundImage($backgroundImage) 53 | { 54 | $this->backgroundImage = $backgroundImage; 55 | 56 | return $this; 57 | } 58 | 59 | /** 60 | * @return bool 61 | */ 62 | public function hasColor() 63 | { 64 | return $this->color !== null; 65 | } 66 | 67 | /** 68 | * @return ?string 69 | */ 70 | public function getColor() 71 | { 72 | return $this->color; 73 | } 74 | 75 | /** 76 | * @param ?string $color 77 | * 78 | * @return $this 79 | */ 80 | public function setColor($color) 81 | { 82 | if ($color !== null && strpos($color, ':') !== false) { 83 | throw new InvalidArgumentException('Invalid color code'); 84 | } 85 | 86 | $this->color = $color; 87 | 88 | return $this; 89 | } 90 | 91 | /** 92 | * @return bool 93 | */ 94 | public function hasLogo() 95 | { 96 | return $this->logo !== null; 97 | } 98 | 99 | /** 100 | * @return ?array 101 | */ 102 | public function getLogo() 103 | { 104 | return $this->logo; 105 | } 106 | 107 | /** 108 | * @param ?array $logo 109 | * 110 | * @return $this 111 | */ 112 | public function setLogo($logo) 113 | { 114 | $this->logo = $logo; 115 | 116 | return $this; 117 | } 118 | 119 | /** 120 | * @return bool 121 | */ 122 | public function hasTitle() 123 | { 124 | return $this->title !== null; 125 | } 126 | 127 | /** 128 | * @return ?string 129 | */ 130 | public function getTitle() 131 | { 132 | return $this->title; 133 | } 134 | 135 | /** 136 | * @param ?string $title 137 | * 138 | * @return $this 139 | */ 140 | public function setTitle($title) 141 | { 142 | $this->title = $title; 143 | 144 | return $this; 145 | } 146 | 147 | protected function assemble() 148 | { 149 | if ($this->hasBackgroundImage()) { 150 | $coverPageBackground = (new StyleWithNonce()) 151 | ->setModule('reporting') 152 | ->addFor($this, [ 153 | 'background-image' => sprintf("url('%s')", Template::getDataUrl($this->getBackgroundImage())) 154 | ]); 155 | 156 | $this->addHtml($coverPageBackground); 157 | } 158 | 159 | $content = Html::tag('div', ['class' => 'cover-page-content']); 160 | if ($this->hasColor()) { 161 | $coverPageLogo = (new StyleWithNonce()) 162 | ->setModule('reporting') 163 | ->addFor($content, ['color' => Html::escape($this->getColor())]); 164 | 165 | $content->addHtml($coverPageLogo); 166 | } 167 | 168 | if ($this->hasLogo()) { 169 | $content->add(Html::tag( 170 | 'img', 171 | [ 172 | 'class' => 'logo', 173 | 'src' => Template::getDataUrl($this->getLogo()) 174 | ] 175 | )); 176 | } 177 | 178 | if ($this->hasTitle()) { 179 | $title = array_map(function ($part) { 180 | $part = trim($part); 181 | 182 | if (! $part) { 183 | return Html::tag('br'); 184 | } else { 185 | return Html::tag('div', null, $part); 186 | } 187 | }, explode("\n", $this->resolveMacros($this->getTitle()))); 188 | 189 | $content->add(Html::tag( 190 | 'h2', 191 | $title 192 | )); 193 | } 194 | 195 | $this->add($content); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /library/Reporting/Web/Widget/HeaderOrFooter.php: -------------------------------------------------------------------------------- 1 | type = $type; 26 | $this->data = $data; 27 | } 28 | 29 | protected function resolveVariable($variable) 30 | { 31 | switch ($variable) { 32 | case 'report_title': 33 | $resolved = Html::tag('span', ['class' => 'title']); 34 | break; 35 | case 'time_frame': 36 | $resolved = Html::tag('p', $this->getMacro('time_frame')); 37 | break; 38 | case 'time_frame_absolute': 39 | $resolved = Html::tag('p', $this->getMacro('time_frame_absolute')); 40 | break; 41 | case 'page_number': 42 | $resolved = Html::tag('span', ['class' => 'pageNumber']); 43 | break; 44 | case 'total_number_of_pages': 45 | $resolved = Html::tag('span', ['class' => 'totalPages']); 46 | break; 47 | case 'page_of': 48 | $resolved = Html::tag('p', Html::sprintf( 49 | '%s / %s', 50 | Html::tag('span', ['class' => 'pageNumber']), 51 | Html::tag('span', ['class' => 'totalPages']) 52 | )); 53 | break; 54 | case 'date': 55 | $resolved = Html::tag('span', ['class' => 'date']); 56 | break; 57 | default: 58 | $resolved = $variable; 59 | break; 60 | } 61 | 62 | return $resolved; 63 | } 64 | 65 | protected function createColumn(array $data, $key) 66 | { 67 | $typeKey = "{$key}_type"; 68 | $valueKey = "{$key}_value"; 69 | $type = $data[$typeKey] ?? null; 70 | 71 | switch ($type) { 72 | case 'text': 73 | $column = Html::tag('p', $data[$valueKey]); 74 | break; 75 | case 'image': 76 | $column = Html::tag('img', ['height' => 13, 'src' => Template::getDataUrl($data[$valueKey])]); 77 | break; 78 | case 'variable': 79 | $column = $this->resolveVariable($data[$valueKey]); 80 | break; 81 | default: 82 | $column = Html::tag('div'); 83 | break; 84 | } 85 | 86 | return $column; 87 | } 88 | 89 | protected function assemble() 90 | { 91 | for ($i = 1; $i <= 3; ++$i) { 92 | $this->add($this->createColumn($this->data, "{$this->type}_column{$i}")); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /library/Reporting/Web/Widget/Template.php: -------------------------------------------------------------------------------- 1 | 'template']; 18 | 19 | /** @var CoverPage */ 20 | protected $coverPage; 21 | 22 | /** @var HeaderOrFooter */ 23 | protected $header; 24 | 25 | /** @var HeaderOrFooter */ 26 | protected $footer; 27 | 28 | protected $preview; 29 | 30 | public static function getDataUrl(array $image = null) 31 | { 32 | if (empty($image)) { 33 | return 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; 34 | } 35 | 36 | return sprintf('data:%s;base64,%s', $image['mime_type'], $image['content']); 37 | } 38 | 39 | /** 40 | * Create template from the given model 41 | * 42 | * @param Model\Template $templateModel 43 | * 44 | * @return static 45 | */ 46 | public static function fromModel(Model\Template $templateModel): self 47 | { 48 | $template = new static(); 49 | 50 | $templateModel->settings = json_decode($templateModel->settings, true); 51 | 52 | $coverPage = (new CoverPage()) 53 | ->setColor($templateModel->settings['color']) 54 | ->setTitle($templateModel->settings['title']); 55 | 56 | if (isset($templateModel->settings['cover_page_background_image'])) { 57 | $coverPage->setBackgroundImage($templateModel->settings['cover_page_background_image']); 58 | } 59 | 60 | if (isset($templateModel->settings['cover_page_logo'])) { 61 | $coverPage->setLogo($templateModel->settings['cover_page_logo']); 62 | } 63 | 64 | $template 65 | ->setCoverPage($coverPage) 66 | ->setHeader(new HeaderOrFooter(HeaderOrFooter::HEADER, $templateModel->settings)) 67 | ->setFooter(new HeaderOrFooter(HeaderOrFooter::FOOTER, $templateModel->settings)); 68 | 69 | return $template; 70 | } 71 | 72 | /** 73 | * @return CoverPage 74 | */ 75 | public function getCoverPage() 76 | { 77 | return $this->coverPage; 78 | } 79 | 80 | /** 81 | * @param CoverPage $coverPage 82 | * 83 | * @return $this 84 | */ 85 | public function setCoverPage(CoverPage $coverPage) 86 | { 87 | $this->coverPage = $coverPage; 88 | 89 | return $this; 90 | } 91 | 92 | /** 93 | * @return HeaderOrFooter 94 | */ 95 | public function getHeader() 96 | { 97 | return $this->header; 98 | } 99 | 100 | /** 101 | * @param HeaderOrFooter $header 102 | * 103 | * @return $this 104 | */ 105 | public function setHeader($header) 106 | { 107 | $this->header = $header; 108 | 109 | return $this; 110 | } 111 | 112 | /** 113 | * @return HeaderOrFooter 114 | */ 115 | public function getFooter() 116 | { 117 | return $this->footer; 118 | } 119 | 120 | /** 121 | * @param HeaderOrFooter $footer 122 | * 123 | * @return $this 124 | */ 125 | public function setFooter($footer) 126 | { 127 | $this->footer = $footer; 128 | 129 | return $this; 130 | } 131 | 132 | /** 133 | * @return mixed 134 | */ 135 | public function getPreview() 136 | { 137 | return $this->preview; 138 | } 139 | 140 | /** 141 | * @param mixed $preview 142 | * 143 | * @return $this 144 | */ 145 | public function setPreview($preview) 146 | { 147 | $this->preview = $preview; 148 | 149 | return $this; 150 | } 151 | 152 | protected function assemble() 153 | { 154 | if ($this->preview) { 155 | $this->getAttributes()->add('class', 'preview'); 156 | } 157 | 158 | $this->add($this->getCoverPage()->setMacros($this->macros)); 159 | 160 | // $page = Html::tag( 161 | // 'div', 162 | // ['class' => 'main'], 163 | // Html::tag('div', ['class' => 'page-content'], [ 164 | // $this->header->setMacros($this->macros), 165 | // Html::tag( 166 | // 'div', 167 | // [ 168 | // 'class' => 'main' 169 | // ] 170 | // ), 171 | // $this->footer->setMacros($this->macros) 172 | // ]) 173 | // ); 174 | // 175 | // $this->add($page); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /module.info: -------------------------------------------------------------------------------- 1 | Module: Reporting 2 | Version: 1.0.5 3 | Requires: 4 | Libraries: icinga-php-library (>=0.13.0), icinga-php-thirdparty (>=0.12.0) 5 | Modules: pdfexport (>=0.11.0) 6 | Description: Reporting 7 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ./ 5 | 6 | vendor/* 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - phpstan-baseline.neon 3 | 4 | parameters: 5 | level: max 6 | 7 | checkFunctionNameCase: true 8 | checkInternalClassCaseSensitivity: true 9 | treatPhpDocTypesAsCertain: false 10 | 11 | paths: 12 | - application 13 | - library 14 | 15 | scanDirectories: 16 | - /icingaweb2 17 | - /usr/share/icinga-php/ipl 18 | - /usr/share/icinga-php/vendor 19 | - /usr/share/icingaweb2-modules/icingadb 20 | - /usr/share/icingaweb2-modules/pdfexport 21 | 22 | ignoreErrors: 23 | - 24 | messages: 25 | - '#Unsafe usage of new static\(\)#' 26 | - '#. but return statement is missing#' 27 | reportUnmatched: false 28 | 29 | - '#Call to an undefined method Icinga\\Module\\Reporting\\RetryConnection::lastInsertId\(\)#' 30 | 31 | - '#Call to an undefined method Zend_Controller_Action_HelperBroker::layout\(\)#' 32 | 33 | universalObjectCratesClasses: 34 | - Icinga\Web\View 35 | - ipl\Orm\Model 36 | -------------------------------------------------------------------------------- /public/css/module.less: -------------------------------------------------------------------------------- 1 | // Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 2 | 3 | .content:focus { 4 | outline: none; 5 | } 6 | 7 | .sla-column { 8 | border-radius: 0.5em; 9 | color: @text-color-on-icinga-blue; 10 | text-align: center; 11 | width: 10em; 12 | 13 | &.ok { 14 | background-color: @color-ok; 15 | } 16 | 17 | &.nok { 18 | background-color: @color-critical; 19 | } 20 | 21 | &.unknown { 22 | background-color: @state-unknown; 23 | } 24 | } 25 | 26 | .sla-table { 27 | td:nth-child(1), 28 | td:nth-child(2) { 29 | word-break: break-word; 30 | } 31 | 32 | td:nth-child(3) { 33 | white-space: nowrap; 34 | } 35 | } 36 | 37 | .sla-table > tbody::before { 38 | content: "\200C"; 39 | display: block; 40 | line-height: 0.5em; 41 | } 42 | 43 | /* Stuff that's missing in ipl <= 0.8 START */ 44 | 45 | .dropdown { 46 | display: inline-block; 47 | position: relative; 48 | 49 | .dropdown-toggle::after { 50 | content: ""; 51 | display: inline-block; 52 | width: 0; 53 | height: 0; 54 | margin-left: .255em; 55 | vertical-align: .255em; 56 | border-top: .3em solid; 57 | border-right: .3em solid transparent; 58 | border-bottom: 0; 59 | border-left: .3em solid transparent; 60 | } 61 | 62 | .dropdown-menu { 63 | display: none; 64 | min-width: 10em; 65 | border: 1px solid @gray-light; 66 | background: @body-bg-color; 67 | margin: -.25em; 68 | border-radius: .25em; 69 | padding: .25em; 70 | position: absolute; 71 | } 72 | 73 | &:hover > .dropdown-menu { 74 | display: block; 75 | box-shadow: 0 0 2em 0 rgba(0,0,0,.2); 76 | } 77 | 78 | .dropdown-item { 79 | display: block; 80 | padding: .5em; 81 | margin: -.25em; 82 | 83 | &.action-link:hover { 84 | padding: .5em; 85 | background: @tr-hover-color; 86 | .rounded-corners(0) 87 | } 88 | } 89 | } 90 | 91 | .action-bar { 92 | line-height: 2em; 93 | 94 | .dropdown:first-child:hover .dropdown-menu { 95 | left: .25em; 96 | } 97 | 98 | .dropdown:last-child:hover .dropdown-menu { 99 | right: .25em; 100 | } 101 | 102 | > *:not(:last-child) { 103 | margin-right: .5em; 104 | } 105 | } 106 | 107 | /* Stuff that's missing in ipl <= 0.8 END */ 108 | 109 | @font-family-print: "Helvetica Neue", "Helvetica", "Arial", sans-serif; 110 | 111 | .page-size-a4 { 112 | background-color: white; 113 | box-shadow: 0 0 0.25cm rgba(0,0,0,0.5); 114 | display: block; 115 | margin: 0 auto 0.5cm; 116 | font-family: @font-family-print; 117 | page-break-after: always; 118 | height: 29.7cm; 119 | width: 21cm; 120 | } 121 | 122 | .page-content { 123 | display: flex; 124 | flex-direction: column; 125 | } 126 | 127 | .cover-page { 128 | background-repeat: no-repeat; 129 | background-position: center; 130 | } 131 | 132 | .cover-page-content { 133 | display: flex; 134 | align-items: center; 135 | justify-content: center; 136 | flex-direction: column; 137 | 138 | height: 100%; 139 | width: 100%; 140 | 141 | h2 { 142 | text-align: center; 143 | } 144 | } 145 | 146 | @gutter: 0.5em; 147 | 148 | .grid { 149 | display: flex; 150 | justify-content: space-between; 151 | 152 | &.with-gutters { 153 | margin-left: -0.5 * @gutter; 154 | margin-right: -0.5 * @gutter; 155 | 156 | > * { 157 | margin-left: 0.5 * @gutter; 158 | margin-right: 0.5 * @gutter; 159 | } 160 | } 161 | } 162 | 163 | .main { 164 | flex: 1; 165 | 166 | display: flex; 167 | align-items: center; 168 | flex-direction: column; 169 | justify-content: space-around; 170 | } 171 | 172 | .preview .main { 173 | background-image: url(../static/img?file=graph-dummy.svg&module_name=reporting);; 174 | background-position: center center; 175 | background-repeat: no-repeat; 176 | background-size: contain; 177 | } 178 | 179 | .header, 180 | .footer { 181 | .grid(); 182 | } 183 | 184 | @media print { 185 | font-family: @font-family-print; 186 | } 187 | 188 | .preview .page { 189 | .page-size-a4(); 190 | } 191 | 192 | .schedule-element-separator { 193 | border-top: 1px solid @gray-lighter; 194 | } 195 | 196 | .icinga-controls .override-uploaded-file-hint { 197 | margin-left: 14em; 198 | } 199 | 200 | /* Form fallback styles, remove once <=2.9.5 support is dropped */ 201 | 202 | .icinga-controls { 203 | input[type="file"] { 204 | background-color: @low-sat-blue; 205 | } 206 | 207 | button[type="button"] { 208 | background-color: @low-sat-blue; 209 | } 210 | } 211 | 212 | form.icinga-form { 213 | input[type="file"] { 214 | flex: 1 1 auto; 215 | width: 0; 216 | } 217 | 218 | button[type="button"] { 219 | line-height: normal; 220 | } 221 | } 222 | 223 | /* Form fallback styles end */ 224 | -------------------------------------------------------------------------------- /public/css/system-report.css: -------------------------------------------------------------------------------- 1 | /* Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 */ 2 | 3 | .system-report { 4 | background-color: #fff; 5 | color: #222; 6 | font-family: sans-serif; 7 | 8 | width: 100%; 9 | } 10 | .system-report pre { 11 | margin: 0; 12 | font-family: monospace; 13 | } 14 | .system-report a:link { 15 | color: #009; 16 | text-decoration: none; 17 | background-color: #fff; 18 | } 19 | .system-report a:hover { 20 | text-decoration: underline; 21 | } 22 | .system-report table { 23 | border-collapse: collapse; 24 | border: 0; 25 | width: 934px; 26 | box-shadow: 1px 2px 3px #ccc; 27 | } 28 | .system-report .center { 29 | text-align: center; 30 | } 31 | .system-report .center table { 32 | margin: 1em auto; 33 | text-align: left; 34 | } 35 | .system-report .center th { 36 | text-align: center !important; 37 | } 38 | .system-report td, 39 | .system-report th { 40 | border: 1px solid #666; 41 | font-size: 75%; 42 | vertical-align: baseline; 43 | padding: 4px 5px; 44 | } 45 | .system-report h1 { 46 | font-size: 150%; 47 | } 48 | .system-report h2 { 49 | font-size: 125%; 50 | } 51 | .system-report .p { 52 | text-align: left; 53 | } 54 | .system-report .e { 55 | background-color: #ccf; 56 | width: 300px; 57 | font-weight: bold; 58 | } 59 | .system-report .h { 60 | background-color: #99c; 61 | font-weight: bold; 62 | } 63 | .system-report .v { 64 | background-color: #ddd; 65 | max-width: 300px; 66 | overflow-x: auto; 67 | word-wrap: break-word; 68 | } 69 | .system-report .v i { 70 | color: #999; 71 | } 72 | .system-report img { 73 | float: right; 74 | border: 0; 75 | } 76 | .system-report hr { 77 | width: 934px; 78 | background-color: #ccc; 79 | border: 0; 80 | height: 1px; 81 | } 82 | -------------------------------------------------------------------------------- /public/img/select-icon-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Icinga/icingaweb2-module-reporting/a0cc6c9c6d2c8e9885bc11c40efffa1ff7f6be5a/public/img/select-icon-2x.png -------------------------------------------------------------------------------- /public/img/select-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Icinga/icingaweb2-module-reporting/a0cc6c9c6d2c8e9885bc11c40efffa1ff7f6be5a/public/img/select-icon.png -------------------------------------------------------------------------------- /public/img/textarea-corner-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Icinga/icingaweb2-module-reporting/a0cc6c9c6d2c8e9885bc11c40efffa1ff7f6be5a/public/img/textarea-corner-2x.png -------------------------------------------------------------------------------- /public/img/textarea-corner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Icinga/icingaweb2-module-reporting/a0cc6c9c6d2c8e9885bc11c40efffa1ff7f6be5a/public/img/textarea-corner.png -------------------------------------------------------------------------------- /run.php: -------------------------------------------------------------------------------- 1 | provideHook('DbMigration', '\\Icinga\\Module\\Reporting\\ProvidedHook\\DbMigration'); 12 | 13 | $this->provideHook('reporting/Report', '\\Icinga\\Module\\Reporting\\Reports\\SystemReport'); 14 | 15 | $this->provideHook('reporting/Action', '\\Icinga\\Module\\Reporting\\Actions\\SendMail'); 16 | 17 | Icinga::app()->getLoader()->registerNamespace('reportingipl\Html', __DIR__ . '/library/vendor/ipl/Html/src'); 18 | } 19 | -------------------------------------------------------------------------------- /schema/mysql-upgrades/0.10.0.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE template ( 2 | id int(10) unsigned NOT NULL AUTO_INCREMENT, 3 | author varchar(255) NOT NULL COLLATE utf8mb4_unicode_ci, 4 | name varchar(128) NOT NULL COLLATE utf8mb4_unicode_ci, 5 | settings longblob NOT NULL, 6 | ctime bigint(20) unsigned NOT NULL, 7 | mtime bigint(20) unsigned NOT NULL, 8 | PRIMARY KEY(id) 9 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; 10 | 11 | ALTER TABLE report ADD COLUMN template_id int(10) unsigned NULL DEFAULT NULL AFTER timeframe_id; 12 | ALTER TABLE report ADD CONSTRAINT report_template FOREIGN KEY (template_id) REFERENCES template (id); 13 | -------------------------------------------------------------------------------- /schema/mysql-upgrades/0.9.1.sql: -------------------------------------------------------------------------------- 1 | UPDATE timeframe SET start = 'first day of January this year midnight' WHERE name = 'Current Year'; 2 | UPDATE timeframe SET start = 'first day of January last year midnight' WHERE name = 'Last Year'; 3 | UPDATE timeframe SET ctime = UNIX_TIMESTAMP() * 1000, mtime = UNIX_TIMESTAMP() * 1000; 4 | 5 | ALTER TABLE timeframe MODIFY COLUMN name varchar(128) NOT NULL COLLATE utf8mb4_unicode_ci; 6 | ALTER TABLE timeframe ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=default; 7 | 8 | ALTER TABLE report MODIFY COLUMN name varchar(128) NOT NULL COLLATE utf8mb4_unicode_ci; 9 | ALTER TABLE report ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=default; 10 | -------------------------------------------------------------------------------- /schema/mysql-upgrades/1.0.0.sql: -------------------------------------------------------------------------------- 1 | DROP PROCEDURE IF EXISTS migrate_schedule_config; 2 | DELIMITER // 3 | CREATE PROCEDURE migrate_schedule_config() 4 | BEGIN 5 | DECLARE session_time_zone text; 6 | 7 | DECLARE schedule_id int; 8 | DECLARE schedule_start bigint; 9 | DECLARE schedule_frequency enum('minutely', 'hourly', 'daily', 'weekly', 'monthly'); 10 | DECLARE schedule_config text; 11 | 12 | DECLARE frequency_json text; 13 | 14 | DECLARE done int DEFAULT 0; 15 | DECLARE schedule CURSOR FOR SELECT id, start, frequency, config FROM schedule; 16 | DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1; 17 | 18 | -- Determine the current session time zone name 19 | SELECT IF(@@session.TIME_ZONE = 'SYSTEM', @@system_time_zone, @@session.TIME_ZONE) INTO session_time_zone; 20 | 21 | IF session_time_zone NOT LIKE '+%:%' AND session_time_zone NOT LIKE '-%:%' AND CONVERT_TZ(FROM_UNIXTIME(1699903042), session_time_zone, '+00:00') IS NULL THEN 22 | SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'required named time zone information are not populated into mysql/mariadb'; 23 | END IF; 24 | 25 | OPEN schedule; 26 | read_loop: LOOP 27 | FETCH schedule INTO schedule_id, schedule_start, schedule_frequency, schedule_config; 28 | IF done THEN 29 | LEAVE read_loop; 30 | END IF; 31 | IF NOT INSTR(schedule_config, 'frequencyType') THEN 32 | SET frequency_json = CONCAT( 33 | ',"frequencyType":"\\\\ipl\\\\Scheduler\\\\Cron","frequency":"{', 34 | '\\"expression\\":\\"@', schedule_frequency, 35 | '\\",\\"start\\":\\"', DATE_FORMAT(CONVERT_TZ(FROM_UNIXTIME(schedule_start / 1000), session_time_zone, '+00:00'), '%Y-%m-%dT%H:%i:%s.%f UTC'), 36 | '\\"}"' 37 | ); 38 | UPDATE schedule SET config = INSERT(schedule_config, LENGTH(schedule_config), 0, frequency_json) WHERE id = schedule_id; 39 | END IF; 40 | END LOOP; 41 | CLOSE schedule; 42 | END // 43 | DELIMITER ; 44 | 45 | CALL migrate_schedule_config(); 46 | DROP PROCEDURE migrate_schedule_config; 47 | 48 | ALTER TABLE schedule 49 | DROP COLUMN start, 50 | DROP COLUMN frequency; 51 | 52 | CREATE TABLE reporting_schema ( 53 | id int unsigned NOT NULL AUTO_INCREMENT, 54 | version varchar(64) NOT NULL, 55 | timestamp bigint unsigned NOT NULL, 56 | success enum ('n', 'y') DEFAULT NULL, 57 | reason text DEFAULT NULL, 58 | 59 | PRIMARY KEY (id), 60 | CONSTRAINT idx_reporting_schema_version UNIQUE (version) 61 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC; 62 | 63 | INSERT INTO reporting_schema (version, timestamp, success, reason) 64 | VALUES ('1.0.0', UNIX_TIMESTAMP() * 1000, 'y', NULL); 65 | -------------------------------------------------------------------------------- /schema/mysql-upgrades/1.0.3.sql: -------------------------------------------------------------------------------- 1 | UPDATE timeframe SET end = 'now' WHERE name = 'Current Week'; 2 | 3 | INSERT INTO reporting_schema (version, timestamp, success, reason) 4 | VALUES ('1.0.3', unix_timestamp() * 1000, 'y', NULL); 5 | -------------------------------------------------------------------------------- /schema/mysql.schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE template ( 2 | id int(10) unsigned NOT NULL AUTO_INCREMENT, 3 | author varchar(255) NOT NULL COLLATE utf8mb4_unicode_ci, 4 | name varchar(128) NOT NULL COLLATE utf8mb4_unicode_ci, 5 | settings longblob NOT NULL, 6 | ctime bigint(20) unsigned NOT NULL, 7 | mtime bigint(20) unsigned NOT NULL, 8 | PRIMARY KEY(id) 9 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; 10 | 11 | CREATE TABLE timeframe ( 12 | id int(10) unsigned NOT NULL AUTO_INCREMENT, 13 | name varchar(128) NOT NULL COLLATE utf8mb4_unicode_ci, 14 | title varchar(255) NULL DEFAULT NULL COLLATE utf8mb4_unicode_ci, 15 | start varchar(255) NOT NULL COLLATE utf8mb4_unicode_ci, 16 | end varchar(255) NOT NULL COLLATE utf8mb4_unicode_ci, 17 | ctime bigint(20) unsigned NOT NULL, 18 | mtime bigint(20) unsigned NOT NULL, 19 | PRIMARY KEY(id), 20 | UNIQUE KEY timeframe (name) 21 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; 22 | 23 | INSERT INTO timeframe (name, title, start, end, ctime, mtime) VALUES 24 | ('4 Hours', null, '-4 hours', 'now', UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000), 25 | ('25 Hours', null, '-25 hours', 'now', UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000), 26 | ('One Week', null, '-1 week', 'now', UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000), 27 | ('One Month', null, '-1 month', 'now', UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000), 28 | ('One Year', null, '-1 year', 'now', UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000), 29 | ('Current Day', null, 'midnight', 'now', UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000), 30 | ('Last Day', null, 'yesterday midnight', 'yesterday 23:59:59', UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000), 31 | ('Current Week', null, 'monday this week midnight', 'now', UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000), 32 | ('Last Week', null, 'monday last week midnight', 'sunday last week 23:59:59', UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000), 33 | ('Current Month', null, 'first day of this month midnight', 'now', UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000), 34 | ('Last Month', null, 'first day of last month midnight', 'last day of last month 23:59:59', UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000), 35 | ('Current Year', null, 'first day of January this year midnight', 'now', UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000), 36 | ('Last Year', null, 'first day of January last year midnight', 'last day of December last year 23:59:59', UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000); 37 | 38 | CREATE TABLE report ( 39 | id int(10) unsigned NOT NULL AUTO_INCREMENT, 40 | timeframe_id int(10) unsigned NOT NULL, 41 | template_id int(10) unsigned NULL DEFAULT NULL, 42 | author varchar(255) NOT NULL COLLATE utf8mb4_unicode_ci, 43 | name varchar(128) NOT NULL COLLATE utf8mb4_unicode_ci, 44 | ctime bigint(20) unsigned NOT NULL, 45 | mtime bigint(20) unsigned NOT NULL, 46 | PRIMARY KEY(id), 47 | UNIQUE KEY report (name), 48 | CONSTRAINT report_timeframe FOREIGN KEY (timeframe_id) REFERENCES timeframe (id), 49 | CONSTRAINT report_template FOREIGN KEY (template_id) REFERENCES template (id) 50 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; 51 | 52 | CREATE TABLE reportlet ( 53 | id int(10) unsigned NOT NULL AUTO_INCREMENT, 54 | report_id int(10) unsigned NOT NULL, 55 | class varchar(255) NOT NULL, 56 | ctime bigint(20) unsigned NOT NULL, 57 | mtime bigint(20) unsigned NOT NULL, 58 | PRIMARY KEY(id), 59 | CONSTRAINT reportlet_report FOREIGN KEY (report_id) REFERENCES report (id) ON DELETE CASCADE ON UPDATE CASCADE 60 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; 61 | 62 | CREATE TABLE config ( 63 | id int(10) unsigned NOT NULL AUTO_INCREMENT, 64 | reportlet_id int(10) unsigned NOT NULL, 65 | name varchar(255) NOT NULL COLLATE utf8mb4_unicode_ci, 66 | value text NULL DEFAULT NULL, 67 | ctime bigint(20) unsigned NOT NULL, 68 | mtime bigint(20) unsigned NOT NULL, 69 | PRIMARY KEY(id), 70 | CONSTRAINT config_reportlet FOREIGN KEY (reportlet_id) REFERENCES reportlet (id) ON DELETE CASCADE ON UPDATE CASCADE 71 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; 72 | 73 | CREATE TABLE schedule ( 74 | id int(10) unsigned NOT NULL AUTO_INCREMENT, 75 | report_id int(10) unsigned NOT NULL, 76 | author varchar(255) NOT NULL COLLATE utf8mb4_unicode_ci, 77 | action varchar(255) NOT NULL, 78 | config text NULL DEFAULT NULL, 79 | ctime bigint(20) unsigned NOT NULL, 80 | mtime bigint(20) unsigned NOT NULL, 81 | PRIMARY KEY(id), 82 | CONSTRAINT schedule_report FOREIGN KEY (report_id) REFERENCES report (id) ON DELETE CASCADE ON UPDATE CASCADE 83 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; 84 | 85 | CREATE TABLE reporting_schema ( 86 | id int unsigned NOT NULL AUTO_INCREMENT, 87 | version varchar(64) NOT NULL, 88 | timestamp bigint unsigned NOT NULL, 89 | success enum ('n', 'y') DEFAULT NULL, 90 | reason text DEFAULT NULL, 91 | 92 | PRIMARY KEY (id), 93 | CONSTRAINT idx_reporting_schema_version UNIQUE (version) 94 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC; 95 | 96 | INSERT INTO reporting_schema (version, timestamp, success) 97 | VALUES ('1.0.3', UNIX_TIMESTAMP() * 1000, 'y'); 98 | 99 | -- CREATE TABLE share ( 100 | -- id int(10) unsigned NOT NULL AUTO_INCREMENT, 101 | -- report_id int(10) unsigned NOT NULL, 102 | -- username varchar(255) NOT NULL COLLATE utf8mb4_unicode_ci, 103 | -- restriction enum('none', 'owner', 'consumer'), 104 | -- ctime bigint(20) unsigned NOT NULL, 105 | -- mtime bigint(20) unsigned NOT NULL, 106 | -- PRIMARY KEY(id), 107 | -- CONSTRAINT share_report FOREIGN KEY (report_id) REFERENCES report (id) ON DELETE CASCADE ON UPDATE CASCADE 108 | -- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; 109 | -------------------------------------------------------------------------------- /schema/pgsql-upgrades/1.0.0.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE PROCEDURE migrate_schedule_config() 2 | LANGUAGE plpgsql 3 | AS $$ 4 | DECLARE 5 | row record; 6 | frequency_json text; 7 | BEGIN 8 | FOR row IN (SELECT id, start, frequency, config FROM schedule) 9 | LOOP 10 | IF NOT CAST(POSITION('frequencyType' IN row.config) AS bool) THEN 11 | frequency_json = CONCAT( 12 | ',"frequencyType":"\\ipl\\Scheduler\\Cron","frequency":"{', 13 | '\"expression\":\"@', row.frequency, 14 | '\",\"start\":\"', TO_CHAR(TO_TIMESTAMP(row.start / 1000) AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US UTC'), 15 | '\"}"' 16 | ); 17 | UPDATE schedule SET config = OVERLAY(row.config PLACING frequency_json FROM LENGTH(row.config) FOR 0) WHERE id = row.id; 18 | END IF; 19 | END LOOP; 20 | END; 21 | $$; 22 | 23 | CALL migrate_schedule_config(); 24 | DROP PROCEDURE migrate_schedule_config; 25 | 26 | ALTER TABLE schedule 27 | DROP COLUMN start, 28 | DROP COLUMN frequency; 29 | 30 | CREATE TYPE boolenum AS ENUM ('n', 'y'); 31 | 32 | CREATE TABLE reporting_schema ( 33 | id serial, 34 | version varchar(64) NOT NULL, 35 | timestamp bigint NOT NULL, 36 | success boolenum DEFAULT NULL, 37 | reason text DEFAULT NULL, 38 | 39 | CONSTRAINT pk_reporting_schema PRIMARY KEY (id), 40 | CONSTRAINT idx_reporting_schema_version UNIQUE (version) 41 | ); 42 | 43 | INSERT INTO reporting_schema (version, timestamp, success, reason) 44 | VALUES ('1.0.0', unix_timestamp() * 1000, 'y', NULL); 45 | -------------------------------------------------------------------------------- /schema/pgsql-upgrades/1.0.3.sql: -------------------------------------------------------------------------------- 1 | UPDATE timeframe SET "end" = 'now' WHERE name = 'Current Week'; 2 | 3 | INSERT INTO reporting_schema (version, timestamp, success, reason) 4 | VALUES ('1.0.3', unix_timestamp() * 1000, 'y', NULL) 5 | ON CONFLICT ON CONSTRAINT idx_reporting_schema_version DO UPDATE SET success = EXCLUDED.success, 6 | reason = EXCLUDED.reason, 7 | timestamp = EXCLUDED.timestamp; 8 | -------------------------------------------------------------------------------- /schema/pgsql.schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE boolenum AS ENUM ('n', 'y'); 2 | 3 | CREATE OR REPLACE FUNCTION unix_timestamp(timestamp with time zone DEFAULT NOW()) RETURNS bigint 4 | AS 'SELECT EXTRACT(EPOCH FROM $1)::bigint' 5 | LANGUAGE SQL; 6 | 7 | CREATE TABLE template ( 8 | id serial PRIMARY KEY, 9 | author varchar(255) NOT NULL, 10 | name varchar(128) NOT NULL, 11 | settings text NOT NULL, 12 | ctime bigint NOT NULL, 13 | mtime bigint NOT NULL 14 | ); 15 | 16 | CREATE TABLE timeframe ( 17 | id serial PRIMARY KEY, 18 | name varchar(128) NOT NULL UNIQUE, 19 | title varchar(255) DEFAULT NULL, 20 | start varchar(255) NOT NULL, 21 | "end" varchar(255) NOT NULL, 22 | ctime bigint NOT NULL DEFAULT unix_timestamp() * 1000, 23 | mtime bigint NOT NULL DEFAULT unix_timestamp() * 1000 24 | ); 25 | 26 | INSERT INTO timeframe (name, title, start, "end") VALUES 27 | ('4 Hours', null, '-4 hours', 'now'), 28 | ('25 Hours', null, '-25 hours', 'now'), 29 | ('One Week', null, '-1 week', 'now'), 30 | ('One Month', null, '-1 month', 'now'), 31 | ('One Year', null, '-1 year', 'now'), 32 | ('Current Day', null, 'midnight', 'now'), 33 | ('Last Day', null, 'yesterday midnight', 'yesterday 23:59:59'), 34 | ('Current Week', null, 'monday this week midnight', 'now'), 35 | ('Last Week', null, 'monday last week midnight', 'sunday last week 23:59:59'), 36 | ('Current Month', null, 'first day of this month midnight', 'now'), 37 | ('Last Month', null, 'first day of last month midnight', 'last day of last month 23:59:59'), 38 | ('Current Year', null, 'first day of January this year midnight', 'now'), 39 | ('Last Year', null, 'first day of January last year midnight', 'last day of December last year 23:59:59'); 40 | 41 | CREATE TABLE report ( 42 | id serial PRIMARY KEY, 43 | timeframe_id int NOT NULL, 44 | template_id int NULL DEFAULT NULL, 45 | author varchar(255) NOT NULL, 46 | name varchar(128) NOT NULL UNIQUE, 47 | ctime bigint NOT NULL DEFAULT unix_timestamp() * 1000, 48 | mtime bigint NOT NULL DEFAULT unix_timestamp() * 1000, 49 | CONSTRAINT report_timeframe FOREIGN KEY (timeframe_id) REFERENCES timeframe (id), 50 | CONSTRAINT report_template FOREIGN KEY (template_id) REFERENCES template (id) 51 | ); 52 | 53 | CREATE TABLE reportlet ( 54 | id serial PRIMARY KEY, 55 | report_id int NOT NULL, 56 | class varchar(255) NOT NULL, 57 | ctime bigint NOT NULL DEFAULT unix_timestamp() * 1000, 58 | mtime bigint NOT NULL DEFAULT unix_timestamp() * 1000, 59 | CONSTRAINT reportlet_report FOREIGN KEY (report_id) REFERENCES report (id) ON DELETE CASCADE ON UPDATE CASCADE 60 | ); 61 | 62 | CREATE TABLE config ( 63 | id serial PRIMARY KEY, 64 | reportlet_id int NOT NULL, 65 | name varchar(255) NOT NULL, 66 | value text DEFAULT NULL, 67 | ctime bigint NOT NULL DEFAULT unix_timestamp() * 1000, 68 | mtime bigint NOT NULL DEFAULT unix_timestamp() * 1000, 69 | CONSTRAINT config_reportlet FOREIGN KEY (reportlet_id) REFERENCES reportlet (id) ON DELETE CASCADE ON UPDATE CASCADE 70 | ); 71 | 72 | CREATE TABLE schedule ( 73 | id serial PRIMARY KEY, 74 | report_id int NOT NULL, 75 | author varchar(255) NOT NULL, 76 | action varchar(255) NOT NULL, 77 | config text DEFAULT NULL, 78 | ctime bigint NOT NULL DEFAULT unix_timestamp() * 1000, 79 | mtime bigint NOT NULL DEFAULT unix_timestamp() * 1000, 80 | CONSTRAINT schedule_report FOREIGN KEY (report_id) REFERENCES report (id) ON DELETE CASCADE ON UPDATE CASCADE 81 | ); 82 | 83 | CREATE TABLE reporting_schema ( 84 | id serial, 85 | version varchar(64) NOT NULL, 86 | timestamp bigint NOT NULL, 87 | success boolenum DEFAULT NULL, 88 | reason text DEFAULT NULL, 89 | 90 | CONSTRAINT pk_reporting_schema PRIMARY KEY (id), 91 | CONSTRAINT idx_reporting_schema_version UNIQUE (version) 92 | ); 93 | 94 | INSERT INTO reporting_schema (version, timestamp, success) 95 | VALUES ('1.0.3', UNIX_TIMESTAMP() * 1000, 'y'); 96 | --------------------------------------------------------------------------------