├── public
├── img
│ ├── select-icon.png
│ ├── select-icon-2x.png
│ ├── textarea-corner.png
│ └── textarea-corner-2x.png
└── css
│ ├── system-report.css
│ └── module.less
├── module.info
├── schema
├── mysql-upgrades
│ ├── 1.0.3.sql
│ ├── 0.10.0.sql
│ ├── 0.9.1.sql
│ └── 1.0.0.sql
├── pgsql-upgrades
│ ├── 1.0.3.sql
│ └── 1.0.0.sql
├── pgsql.schema.sql
└── mysql.schema.sql
├── library
└── Reporting
│ ├── ReportRow.php
│ ├── Web
│ ├── Controller.php
│ ├── Widget
│ │ ├── CompatDropdown.php
│ │ ├── HeaderOrFooter.php
│ │ ├── Template.php
│ │ └── CoverPage.php
│ ├── Forms
│ │ ├── SendForm.php
│ │ ├── ScheduleForm.php
│ │ ├── TimeframeForm.php
│ │ ├── ReportForm.php
│ │ └── TemplateForm.php
│ └── ReportsTimeframesAndTemplatesTabs.php
│ ├── Values.php
│ ├── Dimensions.php
│ ├── ProvidedActions.php
│ ├── ProvidedReports.php
│ ├── Cli
│ └── Command.php
│ ├── Timerange.php
│ ├── Hook
│ ├── ActionHook.php
│ └── ReportHook.php
│ ├── Str.php
│ ├── Common
│ └── Macros.php
│ ├── Model
│ ├── Config.php
│ ├── Schedule.php
│ ├── Reportlet.php
│ ├── Timeframe.php
│ ├── Template.php
│ ├── Schema.php
│ └── Report.php
│ ├── Reports
│ └── SystemReport.php
│ ├── Reportlet.php
│ ├── ReportData.php
│ ├── Timeframe.php
│ ├── RetryConnection.php
│ ├── Database.php
│ ├── ProvidedHook
│ └── DbMigration.php
│ ├── Actions
│ └── SendMail.php
│ ├── Schedule.php
│ ├── Mail.php
│ └── Report.php
├── config
└── systemd
│ └── icinga-reporting.service
├── application
├── views
│ └── scripts
│ │ └── config
│ │ ├── backend.phtml
│ │ └── mail.phtml
├── forms
│ ├── ConfigureMailForm.php
│ └── SelectBackendForm.php
├── controllers
│ ├── ConfigController.php
│ ├── TimeframeController.php
│ ├── TemplateController.php
│ ├── TemplatesController.php
│ ├── TimeframesController.php
│ ├── ReportsController.php
│ └── ReportController.php
└── clicommands
│ ├── DownloadCommand.php
│ ├── ListCommand.php
│ └── ScheduleCommand.php
├── run.php
├── phpcs.xml
├── AUTHORS
├── .mailmap
├── phpstan.neon
├── README.md
├── configuration.php
├── doc
├── 03-Configuration.md
├── 02-Installation.md
└── 80-Upgrading.md
└── LICENSE
/public/img/select-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Icinga/icingaweb2-module-reporting/HEAD/public/img/select-icon.png
--------------------------------------------------------------------------------
/public/img/select-icon-2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Icinga/icingaweb2-module-reporting/HEAD/public/img/select-icon-2x.png
--------------------------------------------------------------------------------
/public/img/textarea-corner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Icinga/icingaweb2-module-reporting/HEAD/public/img/textarea-corner.png
--------------------------------------------------------------------------------
/public/img/textarea-corner-2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Icinga/icingaweb2-module-reporting/HEAD/public/img/textarea-corner-2x.png
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/library/Reporting/ReportRow.php:
--------------------------------------------------------------------------------
1 |
2 | = /** @var \Icinga\Web\Widget\Tabs $tabs */ $tabs ?>
3 |
4 |
5 | = /** @var \Icinga\Module\Reporting\Forms\SelectBackendForm $form */ $form ?>
6 |
7 |
--------------------------------------------------------------------------------
/application/views/scripts/config/mail.phtml:
--------------------------------------------------------------------------------
1 |
2 | = /** @var \Icinga\Web\Widget\Tabs $tabs */ $tabs ?>
3 |
4 |
5 | = /** @var \Icinga\Module\Reporting\Forms\ConfigureMailForm $form */ $form ?>
6 |
7 |
--------------------------------------------------------------------------------
/library/Reporting/Web/Controller.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/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/ProvidedActions.php:
--------------------------------------------------------------------------------
1 | $action) {
16 | $actions[$class] = $action->getName();
17 | }
18 |
19 | return $actions;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/library/Reporting/ProvidedReports.php:
--------------------------------------------------------------------------------
1 | $report) {
16 | $reports[$class] = $report->getName();
17 | }
18 |
19 | return $reports;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/library/Reporting/Cli/Command.php:
--------------------------------------------------------------------------------
1 | getModuleManager()->loadEnabledModules();
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/library/Reporting/Hook/ActionHook.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Icinga Reporting
2 |
3 | [](https://php.net/)
4 | [](https://github.com/Icinga/icingaweb2-module-reporting/actions/workflows/php.yml)
5 | [](https://github.com/Icinga/icingaweb2-module-reporting/releases/latest)
6 |
7 | 
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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/library/Reporting/Model/Schema.php:
--------------------------------------------------------------------------------
1 | add(new BoolCast(['success']));
47 | $behaviors->add(new MillisecondTimestamp(['timestamp']));
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/library/Reporting/Reportlet.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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/library/Reporting/Timeframe.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/Hook/ReportHook.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/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/Forms/ReportForm.php:
--------------------------------------------------------------------------------
1 | db = $db;
38 | }
39 |
40 | /**
41 | * Create a new form instance with the given report id
42 | *
43 | * @param int $id
44 | * @param RetryConnection $db
45 | *
46 | * @return static
47 | */
48 | public static function fromId(int $id, RetryConnection $db): self
49 | {
50 | $form = new static($db);
51 | $form->id = $id;
52 |
53 | return $form;
54 | }
55 |
56 | public function getId(): ?int
57 | {
58 | return $this->id;
59 | }
60 |
61 | /**
62 | * Set the label of the submit button
63 | *
64 | * @param string $label
65 | *
66 | * @return $this
67 | */
68 | public function setSubmitButtonLabel(string $label): self
69 | {
70 | $this->submitButtonLabel = $label;
71 |
72 | return $this;
73 | }
74 |
75 | /**
76 | * Get the label of the submit button
77 | *
78 | * @return string
79 | */
80 | public function getSubmitButtonLabel(): string
81 | {
82 | if ($this->submitButtonLabel !== null) {
83 | return $this->submitButtonLabel;
84 | }
85 |
86 | return $this->id === null ? $this->translate('Create Report') : $this->translate('Update Report');
87 | }
88 |
89 | /**
90 | * Set whether the create and show submit button should be rendered
91 | *
92 | * @param bool $renderCreateAndShowButton
93 | *
94 | * @return $this
95 | */
96 | public function setRenderCreateAndShowButton(bool $renderCreateAndShowButton): self
97 | {
98 | $this->renderCreateAndShowButton = $renderCreateAndShowButton;
99 |
100 | return $this;
101 | }
102 |
103 | public function hasBeenSubmitted(): bool
104 | {
105 | return $this->hasBeenSent() && (
106 | $this->getPopulatedValue('submit')
107 | || $this->getPopulatedValue('create_show')
108 | || $this->getPopulatedValue('remove')
109 | );
110 | }
111 |
112 | protected function assemble(): void
113 | {
114 | $this->addElement('text', 'name', [
115 | 'required' => true,
116 | 'label' => $this->translate('Name'),
117 | 'description' => $this->translate(
118 | 'A unique name of this report. It is used when exporting to pdf, json or csv format'
119 | . ' and also when listing the reports in the cli'
120 | ),
121 | 'validators' => [
122 | 'Callback' => function ($value, CallbackValidator $validator) {
123 | if (strpos($value, '..') !== false) {
124 | $validator->addMessage(
125 | $this->translate('Double dots are not allowed in the report name')
126 | );
127 |
128 | return false;
129 | }
130 |
131 | $filter = Filter::all(Filter::equal('name', $value));
132 | if ($this->id) {
133 | $filter->add(Filter::unequal('id', $this->id));
134 | }
135 |
136 | $report = Report::on($this->db)
137 | ->columns([new Expression('1')])
138 | ->filter($filter)
139 | ->first();
140 |
141 | if ($report !== null) {
142 | $validator->addMessage(
143 | $this->translate('A report with this name already exists')
144 | );
145 |
146 | return false;
147 | }
148 |
149 | return true;
150 | }
151 | ]
152 | ]);
153 |
154 | $this->addElement('select', 'timeframe', [
155 | 'required' => true,
156 | 'class' => 'autosubmit',
157 | 'label' => $this->translate('Timeframe'),
158 | 'options' => [null => $this->translate('Please choose')] + Database::listTimeframes(),
159 | 'description' => $this->translate(
160 | 'Specifies the time frame in which this report is to be generated'
161 | )
162 | ]);
163 |
164 | $this->addElement('select', 'template', [
165 | 'label' => $this->translate('Template'),
166 | 'options' => [null => $this->translate('Please choose')] + Database::listTemplates(),
167 | 'description' => $this->translate(
168 | 'Specifies the template to use when exporting this report to pdf. (Default Icinga template)'
169 | )
170 | ]);
171 |
172 | $this->addElement('select', 'reportlet', [
173 | 'required' => true,
174 | 'class' => 'autosubmit',
175 | 'label' => $this->translate('Report'),
176 | 'options' => [null => $this->translate('Please choose')] + $this->listReports(),
177 | 'description' => $this->translate('Specifies the type of the reportlet to be generated')
178 | ]);
179 |
180 | $values = $this->getValues();
181 |
182 | if (isset($values['reportlet'])) {
183 | $config = new Form();
184 |
185 | /** @var \Icinga\Module\Reporting\Hook\ReportHook $reportlet */
186 | $reportlet = new $values['reportlet']();
187 |
188 | $reportlet->initConfigForm($config);
189 |
190 | foreach ($config->getElements() as $element) {
191 | $this->addElement($element);
192 | }
193 | }
194 |
195 | $this->addElement('submit', 'submit', [
196 | 'label' => $this->getSubmitButtonLabel()
197 | ]);
198 |
199 | if ($this->id !== null) {
200 | $removeButton = $this->createElement('submit', 'remove', [
201 | 'label' => $this->translate('Remove Report'),
202 | 'class' => 'btn-remove',
203 | 'formnovalidate' => true
204 | ]);
205 | $this->registerElement($removeButton);
206 |
207 | /** @var HtmlDocument $wrapper */
208 | $wrapper = $this->getElement('submit')->getWrapper();
209 | $wrapper->prepend($removeButton);
210 | } elseif ($this->renderCreateAndShowButton) {
211 | $createAndShow = $this->createElement('submit', 'create_show', [
212 | 'label' => $this->translate('Create and Show'),
213 | ]);
214 | $this->registerElement($createAndShow);
215 |
216 | /** @var HtmlDocument $wrapper */
217 | $wrapper = $this->getElement('submit')->getWrapper();
218 | $wrapper->prepend($createAndShow);
219 | }
220 | }
221 |
222 | public function onSuccess(): void
223 | {
224 | $db = Database::get();
225 |
226 | if ($this->getPopulatedValue('remove')) {
227 | $db->delete('report', ['id = ?' => $this->id]);
228 |
229 | return;
230 | }
231 |
232 | $values = $this->getValues();
233 |
234 | $now = time() * 1000;
235 |
236 | $db->beginTransaction();
237 |
238 | if ($this->id === null) {
239 | $db->insert('report', [
240 | 'name' => $values['name'],
241 | 'author' => Auth::getInstance()->getUser()->getUsername(),
242 | 'timeframe_id' => $values['timeframe'],
243 | 'template_id' => $values['template'],
244 | 'ctime' => $now,
245 | 'mtime' => $now
246 | ]);
247 |
248 | $reportId = $db->lastInsertId();
249 | } else {
250 | $db->update('report', [
251 | 'name' => $values['name'],
252 | 'timeframe_id' => $values['timeframe'],
253 | 'template_id' => $values['template'],
254 | 'mtime' => $now
255 | ], ['id = ?' => $this->id]);
256 |
257 | $reportId = $this->id;
258 | }
259 |
260 | unset($values['name']);
261 | unset($values['timeframe']);
262 |
263 | if ($this->id !== null) {
264 | $db->delete('reportlet', ['report_id = ?' => $reportId]);
265 | }
266 |
267 | $db->insert('reportlet', [
268 | 'report_id' => $reportId,
269 | 'class' => $values['reportlet'],
270 | 'ctime' => $now,
271 | 'mtime' => $now
272 | ]);
273 |
274 | $reportletId = $db->lastInsertId();
275 |
276 | unset($values['reportlet']);
277 |
278 | foreach ($values as $name => $value) {
279 | $db->insert('config', [
280 | 'reportlet_id' => $reportletId,
281 | 'name' => $name,
282 | 'value' => $value,
283 | 'ctime' => $now,
284 | 'mtime' => $now
285 | ]);
286 | }
287 |
288 | $db->commitTransaction();
289 |
290 | $this->id = $reportId;
291 | }
292 | }
293 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------