summaryrefslogtreecommitdiffstats
path: root/library/Reporting
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 12:46:47 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 12:46:47 +0000
commit4ada86876033fa171e2896d7e3d3c5645d8062db (patch)
treef0d1fee61877df200ccfb1c0af58a39cd551fb46 /library/Reporting
parentInitial commit. (diff)
downloadicingaweb2-module-reporting-upstream/0.10.0.tar.xz
icingaweb2-module-reporting-upstream/0.10.0.zip
Adding upstream version 0.10.0.upstream/0.10.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'library/Reporting')
-rw-r--r--library/Reporting/Actions/SendMail.php84
-rw-r--r--library/Reporting/Cli/Command.php23
-rw-r--r--library/Reporting/Common/Macros.php49
-rw-r--r--library/Reporting/Database.php58
-rw-r--r--library/Reporting/Dimensions.php21
-rw-r--r--library/Reporting/Hook/ActionHook.php37
-rw-r--r--library/Reporting/Hook/ReportHook.php116
-rw-r--r--library/Reporting/Mail.php180
-rw-r--r--library/Reporting/ProvidedActions.php20
-rw-r--r--library/Reporting/ProvidedReports.php20
-rw-r--r--library/Reporting/Report.php382
-rw-r--r--library/Reporting/ReportData.php71
-rw-r--r--library/Reporting/ReportRow.php10
-rw-r--r--library/Reporting/Reportlet.php86
-rw-r--r--library/Reporting/Reports/SystemReport.php39
-rw-r--r--library/Reporting/RetryConnection.php66
-rw-r--r--library/Reporting/Schedule.php160
-rw-r--r--library/Reporting/Scheduler.php176
-rw-r--r--library/Reporting/Str.php37
-rw-r--r--library/Reporting/Timeframe.php168
-rw-r--r--library/Reporting/Timerange.php35
-rw-r--r--library/Reporting/Values.php21
-rw-r--r--library/Reporting/Web/Controller.php20
-rw-r--r--library/Reporting/Web/Flatpickr.php77
-rw-r--r--library/Reporting/Web/Forms/DecoratedElement.php17
-rw-r--r--library/Reporting/Web/Forms/Decorator/CompatDecorator.php63
-rw-r--r--library/Reporting/Web/Forms/ReportForm.php168
-rw-r--r--library/Reporting/Web/Forms/ScheduleForm.php177
-rw-r--r--library/Reporting/Web/Forms/SendForm.php47
-rw-r--r--library/Reporting/Web/Forms/TemplateForm.php284
-rw-r--r--library/Reporting/Web/Forms/TimeframeForm.php106
-rw-r--r--library/Reporting/Web/ReportsTimeframesAndTemplatesTabs.php37
-rw-r--r--library/Reporting/Web/Widget/CompatDropdown.php22
-rw-r--r--library/Reporting/Web/Widget/CoverPage.php181
-rw-r--r--library/Reporting/Web/Widget/HeaderOrFooter.php95
-rw-r--r--library/Reporting/Web/Widget/Template.php183
36 files changed, 3336 insertions, 0 deletions
diff --git a/library/Reporting/Actions/SendMail.php b/library/Reporting/Actions/SendMail.php
new file mode 100644
index 0000000..7c70bf5
--- /dev/null
+++ b/library/Reporting/Actions/SendMail.php
@@ -0,0 +1,84 @@
+<?php
+// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting\Actions;
+
+use Icinga\Application\Config;
+use Icinga\Module\Pdfexport\ProvidedHook\Pdfexport;
+use Icinga\Module\Reporting\Hook\ActionHook;
+use Icinga\Module\Reporting\Mail;
+use Icinga\Module\Reporting\Report;
+use ipl\Html\Form;
+
+class SendMail extends ActionHook
+{
+ public function getName()
+ {
+ return 'Send Mail';
+ }
+
+ public function execute(Report $report, array $config)
+ {
+ $name = sprintf(
+ '%s (%s) %s',
+ $report->getName(),
+ $report->getTimeframe()->getName(),
+ date('Y-m-d H:i')
+ );
+
+ $mail = new Mail();
+
+ $mail->setFrom(Config::module('reporting')->get('mail', 'from', 'reporting@icinga'));
+
+ if (isset($config['subject'])) {
+ $mail->setSubject($config['subject']);
+ }
+
+ switch ($config['type']) {
+ case 'pdf':
+ $mail->attachPdf(Pdfexport::first()->htmlToPdf($report->toPdf()), $name);
+
+ break;
+ case 'csv':
+ $mail->attachCsv($report->toCsv(), $name);
+
+ break;
+ case 'json':
+ $mail->attachJson($report->toJson(), $name);
+
+ break;
+ default:
+ throw new \InvalidArgumentException();
+ }
+
+ $recipients = array_filter(preg_split('/[\s,]+/', $config['recipients']));
+
+ $mail->send(null, $recipients);
+ }
+
+ public function initConfigForm(Form $form, Report $report)
+ {
+ $types = ['pdf' => 'PDF'];
+
+ if ($report->providesData()) {
+ $types['csv'] = 'CSV';
+ $types['json'] = 'JSON';
+ }
+
+ $form->addElement('select', 'type', [
+ 'required' => true,
+ 'label' => t('Type'),
+ 'options' => $types
+ ]);
+
+ $form->addElement('text', 'subject', [
+ 'label' => t('Subject'),
+ 'placeholder' => Mail::DEFAULT_SUBJECT
+ ]);
+
+ $form->addElement('textarea', 'recipients', [
+ 'required' => true,
+ 'label' => t('Recipients')
+ ]);
+ }
+}
diff --git a/library/Reporting/Cli/Command.php b/library/Reporting/Cli/Command.php
new file mode 100644
index 0000000..a89f77b
--- /dev/null
+++ b/library/Reporting/Cli/Command.php
@@ -0,0 +1,23 @@
+<?php
+// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting\Cli;
+
+use Icinga\Application\Icinga;
+use Icinga\Application\Version;
+use Icinga\Module\Reporting\Database;
+
+class Command extends \Icinga\Cli\Command
+{
+ use Database;
+
+ // Fix Web 2 issue where $configs is not properly initialized
+ protected $configs = [];
+
+ public function init()
+ {
+ if (version_compare(Version::VERSION, '2.7.0', '<')) {
+ Icinga::app()->getModuleManager()->loadEnabledModules();
+ }
+ }
+}
diff --git a/library/Reporting/Common/Macros.php b/library/Reporting/Common/Macros.php
new file mode 100644
index 0000000..052cdd2
--- /dev/null
+++ b/library/Reporting/Common/Macros.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Icinga\Module\Reporting\Common;
+
+trait Macros
+{
+ protected $macros;
+
+ /**
+ * @param string $name
+ *
+ * @return mixed
+ */
+ public function getMacro($name)
+ {
+ return $this->macros[$name] ?: null;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getMacros()
+ {
+ return $this->macros;
+ }
+
+ /**
+ * @param mixed $macros
+ *
+ * @return $this
+ */
+ public function setMacros($macros)
+ {
+ $this->macros = $macros;
+
+ return $this;
+ }
+
+ public function resolveMacros($subject)
+ {
+ $macros = [];
+
+ foreach ((array) $this->macros as $key => $value) {
+ $macros['${' . $key . '}'] = $value;
+ }
+
+ return str_replace(array_keys($macros), array_values($macros), $subject);
+ }
+}
diff --git a/library/Reporting/Database.php b/library/Reporting/Database.php
new file mode 100644
index 0000000..3dabe17
--- /dev/null
+++ b/library/Reporting/Database.php
@@ -0,0 +1,58 @@
+<?php
+// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting;
+
+use Icinga\Application\Config;
+use Icinga\Data\ResourceFactory;
+use ipl\Sql;
+
+trait Database
+{
+ protected function getDb($resource = null)
+ {
+ $config = new Sql\Config(ResourceFactory::getResourceConfig(
+ $resource ?: Config::module('reporting')->get('backend', 'resource', 'reporting')
+ ));
+
+ $config->options = [\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_OBJ];
+ if ($config->db === 'mysql') {
+ $config->options[\PDO::MYSQL_ATTR_INIT_COMMAND] = "SET SESSION SQL_MODE='STRICT_TRANS_TABLES"
+ . ",NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'";
+ }
+
+ $conn = new RetryConnection($config);
+
+ return $conn;
+ }
+
+ protected function listTimeframes()
+ {
+ $select = (new Sql\Select())
+ ->from('timeframe')
+ ->columns(['id', 'name']);
+
+ $timeframes = [];
+
+ foreach ($this->getDb()->select($select) as $row) {
+ $timeframes[$row->id] = $row->name;
+ }
+
+ return $timeframes;
+ }
+
+ protected function listTemplates()
+ {
+ $select = (new Sql\Select())
+ ->from('template')
+ ->columns(['id', 'name']);
+
+ $templates = [];
+
+ foreach ($this->getDb()->select($select) as $row) {
+ $templates[$row->id] = $row->name;
+ }
+
+ return $templates;
+ }
+}
diff --git a/library/Reporting/Dimensions.php b/library/Reporting/Dimensions.php
new file mode 100644
index 0000000..dfedbc8
--- /dev/null
+++ b/library/Reporting/Dimensions.php
@@ -0,0 +1,21 @@
+<?php
+// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting;
+
+trait Dimensions
+{
+ protected $dimensions;
+
+ public function getDimensions()
+ {
+ return $this->dimensions;
+ }
+
+ public function setDimensions(array $dimensions)
+ {
+ $this->dimensions = $dimensions;
+
+ return $this;
+ }
+}
diff --git a/library/Reporting/Hook/ActionHook.php b/library/Reporting/Hook/ActionHook.php
new file mode 100644
index 0000000..ef550ee
--- /dev/null
+++ b/library/Reporting/Hook/ActionHook.php
@@ -0,0 +1,37 @@
+<?php
+// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting\Hook;
+
+use Icinga\Application\Hook;
+use Icinga\Module\Reporting\Report;
+use ipl\Html\Form;
+
+abstract class ActionHook
+{
+ /**
+ * @return string
+ */
+ abstract public function getName();
+
+ /**
+ * @param Report $report
+ * @param array $config
+ */
+ abstract public function execute(Report $report, array $config);
+
+ /**
+ * @param Form $form
+ */
+ public function initConfigForm(Form $form, Report $report)
+ {
+ }
+
+ /**
+ * @return ActionHook[]
+ */
+ final public static function getActions()
+ {
+ return Hook::all('reporting/Action');
+ }
+}
diff --git a/library/Reporting/Hook/ReportHook.php b/library/Reporting/Hook/ReportHook.php
new file mode 100644
index 0000000..13cc01e
--- /dev/null
+++ b/library/Reporting/Hook/ReportHook.php
@@ -0,0 +1,116 @@
+<?php
+// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting\Hook;
+
+use Icinga\Application\ClassLoader;
+use Icinga\Application\Hook;
+use Icinga\Module\Reporting\ReportData;
+use Icinga\Module\Reporting\Timerange;
+use ipl\Html\Form;
+use ipl\Html\ValidHtml;
+
+abstract class ReportHook
+{
+ /**
+ * Get the name of the report
+ *
+ * @return string
+ */
+ abstract public function getName();
+
+ /**
+ * @param Timerange $timerange
+ * @param array $config
+ *
+ * @return ReportData|null
+ */
+ public function getData(Timerange $timerange, array $config = null)
+ {
+ return null;
+ }
+
+ /**
+ * Get the HTML of the report
+ *
+ * @param Timerange $timerange
+ * @param array $config
+ *
+ * @return ValidHtml|null
+ */
+ public function getHtml(Timerange $timerange, array $config = null)
+ {
+ return null;
+ }
+
+ /**
+ * Initialize the report's configuration form
+ *
+ * @param Form $form
+ */
+ public function initConfigForm(Form $form)
+ {
+ }
+
+ /**
+ * Get the description of the report
+ *
+ * @return string
+ */
+ public function getDescription()
+ {
+ return null;
+ }
+
+ /**
+ * Get whether the report provides reporting data
+ *
+ * @return bool
+ */
+ public function providesData()
+ {
+ try {
+ $method = new \ReflectionMethod($this, 'getData');
+ } catch (\ReflectionException $e) {
+ return false;
+ }
+
+ return $method->getDeclaringClass()->getName() !== self::class;
+ }
+
+ /**
+ * Get whether the report provides HTML
+ *
+ * @return bool
+ */
+ public function providesHtml()
+ {
+ try {
+ $method = new \ReflectionMethod($this, 'getHtml');
+ } catch (\ReflectionException $e) {
+ return false;
+ }
+
+ return $method->getDeclaringClass()->getName() !== self::class;
+ }
+
+ /**
+ * Get the module name of the report
+ *
+ * @return string
+ */
+ final public function getModuleName()
+ {
+ return ClassLoader::extractModuleName(get_class($this));
+ }
+
+ /**
+ * Get all provided reports
+ *
+ * @return ReportHook[]
+ */
+ final public static function getReports()
+ {
+ return Hook::all('reporting/Report');
+ }
+}
diff --git a/library/Reporting/Mail.php b/library/Reporting/Mail.php
new file mode 100644
index 0000000..7581f45
--- /dev/null
+++ b/library/Reporting/Mail.php
@@ -0,0 +1,180 @@
+<?php
+// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting;
+
+use Zend_Mail;
+use Zend_Mail_Transport_Sendmail;
+use Zend_Mime;
+use Zend_Mime_Part;
+
+class Mail
+{
+ /** @var string */
+ const DEFAULT_SUBJECT = 'Icinga Reporting';
+
+ /** @var string */
+ protected $from;
+
+ /** @var string */
+ protected $subject = self::DEFAULT_SUBJECT;
+
+ /** @var Zend_Mail_Transport_Sendmail */
+ protected $transport;
+
+ /** @var array */
+ protected $attachments = [];
+
+ /**
+ * Get the from part
+ *
+ * @return string
+ */
+ public function getFrom()
+ {
+ if (isset($this->from)) {
+ return $this->from;
+ }
+
+ if (isset($_SERVER['SERVER_ADMIN'])) {
+ $this->from = $_SERVER['SERVER_ADMIN'];
+
+ return $this->from;
+ }
+
+ foreach (['HTTP_HOST', 'SERVER_NAME', 'HOSTNAME'] as $key) {
+ if (isset($_SEVER[$key])) {
+ $this->from = 'icinga-reporting@' . $_SERVER[$key];
+
+ return $this->from;
+ }
+ }
+
+ $this->from = 'icinga-reporting@localhost';
+
+ return $this->from;
+ }
+
+ /**
+ * Set the from part
+ *
+ * @param string $from
+ *
+ * @return $this
+ */
+ public function setFrom($from)
+ {
+ $this->from = $from;
+
+ return $this;
+ }
+
+ /**
+ * Get the subject
+ *
+ * @return string
+ */
+ public function getSubject()
+ {
+ return $this->subject;
+ }
+
+ /**
+ * Set the subject
+ *
+ * @param string $subject
+ *
+ * @return $this
+ */
+ public function setSubject($subject)
+ {
+ $this->subject = $subject;
+
+ return $this;
+ }
+
+ /**
+ * Get the mail transport
+ *
+ * @return Zend_Mail_Transport_Sendmail
+ */
+ public function getTransport()
+ {
+ if (! isset($this->transport)) {
+ $this->transport = new Zend_Mail_Transport_Sendmail('-f ' . escapeshellarg($this->getFrom()));
+ }
+
+ return $this->transport;
+ }
+
+ public function attachCsv($csv, $filename)
+ {
+ if (is_array($csv)) {
+ $csv = Str::putcsv($csv);
+ }
+
+ $attachment = new Zend_Mime_Part($csv);
+
+ $attachment->type = 'text/csv';
+ $attachment->disposition = Zend_Mime::DISPOSITION_ATTACHMENT;
+ $attachment->encoding = Zend_Mime::ENCODING_BASE64;
+ $attachment->filename = basename($filename, '.csv') . '.csv';
+
+ $this->attachments[] = $attachment;
+
+ return $this;
+ }
+
+ public function attachJson($json, $filename)
+ {
+ if (is_array($json)) {
+ $json = json_encode($json);
+ }
+
+ $attachment = new Zend_Mime_Part($json);
+
+ $attachment->type = 'application/json';
+ $attachment->disposition = Zend_Mime::DISPOSITION_ATTACHMENT;
+ $attachment->encoding = Zend_Mime::ENCODING_BASE64;
+ $attachment->filename = basename($filename, '.json') . '.json';
+
+ $this->attachments[] = $attachment;
+
+ return $this;
+ }
+
+ public function attachPdf($pdf, $filename)
+ {
+ $attachment = new Zend_Mime_Part($pdf);
+
+ $attachment->type = 'application/pdf';
+ $attachment->disposition = Zend_Mime::DISPOSITION_ATTACHMENT;
+ $attachment->encoding = Zend_Mime::ENCODING_BASE64;
+ $attachment->filename = basename($filename, '.pdf') . '.pdf';
+
+ $this->attachments[] = $attachment;
+
+ return $this;
+ }
+
+ public function send($body, $recipient)
+ {
+ $mail = new Zend_Mail('UTF-8');
+
+ $mail->setFrom($this->getFrom());
+ $mail->addTo($recipient);
+ $mail->setSubject($this->getSubject());
+
+ if (strlen($body) !== strlen(strip_tags($body))) {
+ $mail->setBodyHtml($body);
+ } else {
+ $mail->setBodyText($body);
+ }
+
+ foreach ($this->attachments as $attachment) {
+ $mail->addAttachment($attachment);
+ }
+
+ $mail->send($this->getTransport());
+ }
+}
diff --git a/library/Reporting/ProvidedActions.php b/library/Reporting/ProvidedActions.php
new file mode 100644
index 0000000..2590d1f
--- /dev/null
+++ b/library/Reporting/ProvidedActions.php
@@ -0,0 +1,20 @@
+<?php
+// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting;
+
+use Icinga\Module\Reporting\Hook\ActionHook;
+
+trait ProvidedActions
+{
+ public function listActions()
+ {
+ $actions = [];
+
+ foreach (ActionHook::getActions() as $class => $action) {
+ $actions[$class] = $action->getName();
+ }
+
+ return $actions;
+ }
+}
diff --git a/library/Reporting/ProvidedReports.php b/library/Reporting/ProvidedReports.php
new file mode 100644
index 0000000..edfc2ce
--- /dev/null
+++ b/library/Reporting/ProvidedReports.php
@@ -0,0 +1,20 @@
+<?php
+// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting;
+
+use Icinga\Module\Reporting\Hook\ReportHook;
+
+trait ProvidedReports
+{
+ public function listReports()
+ {
+ $reports = [];
+
+ foreach (ReportHook::getReports() as $class => $report) {
+ $reports[$class] = $report->getName();
+ }
+
+ return $reports;
+ }
+}
diff --git a/library/Reporting/Report.php b/library/Reporting/Report.php
new file mode 100644
index 0000000..7f2eee3
--- /dev/null
+++ b/library/Reporting/Report.php
@@ -0,0 +1,382 @@
+<?php
+// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting;
+
+use DateTime;
+use Exception;
+use Icinga\Module\Pdfexport\PrintableHtmlDocument;
+use Icinga\Module\Reporting\Web\Widget\Template;
+use ipl\Html\HtmlDocument;
+use ipl\Sql;
+
+class Report
+{
+ use Database;
+
+ /** @var int */
+ protected $id;
+
+ /** @var string */
+ protected $name;
+
+ /** @var string */
+ protected $author;
+
+ /** @var Timeframe */
+ protected $timeframe;
+
+ /** @var Reportlet[] */
+ protected $reportlets;
+
+ /** @var Schedule */
+ protected $schedule;
+
+ /** @var Template */
+ protected $template;
+
+ /**
+ * @param int $id
+ *
+ * @return static
+ *
+ * @throws Exception
+ */
+ public static function fromDb($id)
+ {
+ $report = new static();
+
+ $db = $report->getDb();
+
+ $select = (new Sql\Select())
+ ->from('report')
+ ->columns('*')
+ ->where(['id = ?' => $id]);
+
+ $row = $db->select($select)->fetch();
+
+ if ($row === false) {
+ throw new Exception('Report not found');
+ }
+
+ $report
+ ->setId($row->id)
+ ->setName($row->name)
+ ->setAuthor($row->author)
+ ->setTimeframe(Timeframe::fromDb($row->timeframe_id))
+ ->setTemplate(Template::fromDb($row->template_id));
+
+ $select = (new Sql\Select())
+ ->from('reportlet')
+ ->columns('*')
+ ->where(['report_id = ?' => $id]);
+
+ $row = $db->select($select)->fetch();
+
+ if ($row === false) {
+ throw new Exception('No reportlets configured.');
+ }
+
+ $reportlet = new Reportlet();
+
+ $reportlet
+ ->setId($row->id)
+ ->setClass($row->class);
+
+ $select = (new Sql\Select())
+ ->from('config')
+ ->columns('*')
+ ->where(['reportlet_id = ?' => $row->id]);
+
+ $rows = $db->select($select)->fetchAll();
+
+ $config = [];
+
+ foreach ($rows as $row) {
+ $config[$row->name] = $row->value;
+ }
+
+ $reportlet->setConfig($config);
+
+ $report->setReportlets([$reportlet]);
+
+ $select = (new Sql\Select())
+ ->from('schedule')
+ ->columns('*')
+ ->where(['report_id = ?' => $id]);
+
+ $row = $db->select($select)->fetch();
+
+ if ($row !== false) {
+ $schedule = new Schedule();
+
+ $schedule
+ ->setId($row->id)
+ ->setStart((new \DateTime())->setTimestamp((int) $row->start / 1000))
+ ->setFrequency($row->frequency)
+ ->setAction($row->action)
+ ->setConfig(json_decode($row->config, true));
+
+ $report->setSchedule($schedule);
+ }
+
+ return $report;
+ }
+
+ /**
+ * @return int
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * @param int $id
+ *
+ * @return $this
+ */
+ public function setId($id)
+ {
+ $this->id = $id;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setName($name)
+ {
+ $this->name = $name;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getAuthor()
+ {
+ return $this->author;
+ }
+
+ /**
+ * @param string $author
+ *
+ * @return $this
+ */
+ public function setAuthor($author)
+ {
+ $this->author = $author;
+
+ return $this;
+ }
+
+ /**
+ * @return Timeframe
+ */
+ public function getTimeframe()
+ {
+ return $this->timeframe;
+ }
+
+ /**
+ * @param Timeframe $timeframe
+ *
+ * @return $this
+ */
+ public function setTimeframe(Timeframe $timeframe)
+ {
+ $this->timeframe = $timeframe;
+
+ return $this;
+ }
+
+ /**
+ * @return Reportlet[]
+ */
+ public function getReportlets()
+ {
+ return $this->reportlets;
+ }
+
+ /**
+ * @param Reportlet[] $reportlets
+ *
+ * @return $this
+ */
+ public function setReportlets(array $reportlets)
+ {
+ $this->reportlets = $reportlets;
+
+ return $this;
+ }
+
+ /**
+ * @return Schedule
+ */
+ public function getSchedule()
+ {
+ return $this->schedule;
+ }
+
+ /**
+ * @param Schedule $schedule
+ *
+ * @return $this
+ */
+ public function setSchedule(Schedule $schedule)
+ {
+ $this->schedule = $schedule;
+
+ return $this;
+ }
+
+ /**
+ * @return Template
+ */
+ public function getTemplate()
+ {
+ return $this->template;
+ }
+
+ /**
+ * @param Template $template
+ *
+ * @return $this
+ */
+ public function setTemplate($template)
+ {
+ $this->template = $template;
+
+ return $this;
+ }
+
+ public function providesData()
+ {
+ foreach ($this->getReportlets() as $reportlet) {
+ $implementation = $reportlet->getImplementation();
+
+ if ($implementation->providesData()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @return HtmlDocument
+ */
+ public function toHtml()
+ {
+ $timerange = $this->getTimeframe()->getTimerange();
+
+ $html = new HtmlDocument();
+
+ foreach ($this->getReportlets() as $reportlet) {
+ $implementation = $reportlet->getImplementation();
+
+ $html->add($implementation->getHtml($timerange, $reportlet->getConfig()));
+ }
+
+ return $html;
+ }
+
+ /**
+ * @return string
+ */
+ public function toCsv()
+ {
+ $timerange = $this->getTimeframe()->getTimerange();
+
+ $csv = [];
+
+ foreach ($this->getReportlets() as $reportlet) {
+ $implementation = $reportlet->getImplementation();
+
+ if ($implementation->providesData()) {
+ $data = $implementation->getData($timerange, $reportlet->getConfig());
+ $csv[] = array_merge($data->getDimensions(), $data->getValues());
+ foreach ($data->getRows() as $row) {
+ $csv[] = array_merge($row->getDimensions(), $row->getValues());
+ }
+
+ break;
+ }
+ }
+
+ return Str::putcsv($csv);
+ }
+
+ /**
+ * @return string
+ */
+ public function toJson()
+ {
+ $timerange = $this->getTimeframe()->getTimerange();
+
+ $json = [];
+
+ foreach ($this->getReportlets() as $reportlet) {
+ $implementation = $reportlet->getImplementation();
+
+ if ($implementation->providesData()) {
+ $data = $implementation->getData($timerange, $reportlet->getConfig());
+ $dimensions = $data->getDimensions();
+ $values = $data->getValues();
+ foreach ($data->getRows() as $row) {
+ $json[] = \array_combine($dimensions, $row->getDimensions())
+ + \array_combine($values, $row->getValues());
+ }
+
+ break;
+ }
+ }
+
+ return json_encode($json);
+ }
+
+ /**
+ * @return PrintableHtmlDocument
+ *
+ * @throws Exception
+ */
+ public function toPdf()
+ {
+ $html = (new PrintableHtmlDocument())
+ ->setTitle($this->getName())
+ ->addAttributes(['class' => 'icinga-module module-reporting'])
+ ->addHtml($this->toHtml());
+
+ if ($this->template !== null) {
+ $this->template->setMacros([
+ 'title' => $this->name,
+ 'date' => (new DateTime())->format('jS M, Y'),
+ 'time_frame' => $this->timeframe->getName(),
+ 'time_frame_absolute' => sprintf(
+ 'From %s to %s',
+ $this->timeframe->getTimerange()->getStart()->format('r'),
+ $this->timeframe->getTimerange()->getEnd()->format('r')
+ )
+ ]);
+
+ $html->setCoverPage($this->template->getCoverPage()->setMacros($this->template->getMacros()));
+ $html->setHeader($this->template->getHeader()->setMacros($this->template->getMacros()));
+ $html->setFooter($this->template->getFooter()->setMacros($this->template->getMacros()));
+ }
+
+ return $html;
+ }
+}
diff --git a/library/Reporting/ReportData.php b/library/Reporting/ReportData.php
new file mode 100644
index 0000000..787f4db
--- /dev/null
+++ b/library/Reporting/ReportData.php
@@ -0,0 +1,71 @@
+<?php
+// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting;
+
+class ReportData implements \Countable
+{
+ use Dimensions;
+ use Values;
+
+ /** @var ReportRow[]|null */
+ protected $rows;
+
+ public function getRows()
+ {
+ return $this->rows;
+ }
+
+ public function setRows(array $rows)
+ {
+ $this->rows = $rows;
+
+ return $this;
+ }
+
+ public function getAverages()
+ {
+ $totals = $this->getTotals();
+ $averages = [];
+ $count = \count($this);
+
+ foreach ($totals as $total) {
+ $averages[] = $total / $count;
+ }
+
+ return $averages;
+ }
+
+// public function getMaximums()
+// {
+// }
+
+// public function getMinimums()
+// {
+// }
+
+ public function getTotals()
+ {
+ $totals = [];
+
+ foreach ((array) $this->getRows() as $row) {
+ $i = 0;
+ foreach ((array) $row->getValues() as $value) {
+ if (! isset($totals[$i])) {
+ $totals[$i] = $value;
+ } else {
+ $totals[$i] += $value;
+ }
+
+ ++$i;
+ }
+ }
+
+ return $totals;
+ }
+
+ public function count(): int
+ {
+ return count((array) $this->getRows());
+ }
+}
diff --git a/library/Reporting/ReportRow.php b/library/Reporting/ReportRow.php
new file mode 100644
index 0000000..1536488
--- /dev/null
+++ b/library/Reporting/ReportRow.php
@@ -0,0 +1,10 @@
+<?php
+// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting;
+
+class ReportRow
+{
+ use Dimensions;
+ use Values;
+}
diff --git a/library/Reporting/Reportlet.php b/library/Reporting/Reportlet.php
new file mode 100644
index 0000000..2876a00
--- /dev/null
+++ b/library/Reporting/Reportlet.php
@@ -0,0 +1,86 @@
+<?php
+// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting;
+
+class Reportlet
+{
+ /** @var int */
+ protected $id;
+
+ /** @var string */
+ protected $class;
+
+ /** @var array */
+ protected $config;
+
+ /**
+ * @return int
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * @param int $id
+ *
+ * @return $this
+ */
+ public function setId($id)
+ {
+ $this->id = $id;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getClass()
+ {
+ return $this->class;
+ }
+
+ /**
+ * @param string $class
+ *
+ * @return $this
+ */
+ public function setClass($class)
+ {
+ $this->class = $class;
+
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getConfig()
+ {
+ return $this->config;
+ }
+
+ /**
+ * @param array $config
+ *
+ * @return $this
+ */
+ public function setConfig($config)
+ {
+ $this->config = $config;
+
+ return $this;
+ }
+
+ /**
+ * @return \Icinga\Module\Reporting\Hook\ReportHook
+ */
+ public function getImplementation()
+ {
+ $class = $this->getClass();
+
+ return new $class;
+ }
+}
diff --git a/library/Reporting/Reports/SystemReport.php b/library/Reporting/Reports/SystemReport.php
new file mode 100644
index 0000000..8a3d8dd
--- /dev/null
+++ b/library/Reporting/Reports/SystemReport.php
@@ -0,0 +1,39 @@
+<?php
+// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting\Reports;
+
+use Icinga\Module\Reporting\Hook\ReportHook;
+use Icinga\Module\Reporting\Timerange;
+use ipl\Html\HtmlString;
+
+class SystemReport extends ReportHook
+{
+ public function getName()
+ {
+ return 'System';
+ }
+
+ public function getHtml(Timerange $timerange, array $config = null)
+ {
+ ob_start();
+ phpinfo();
+ $html = ob_get_clean();
+
+ $doc = new \DOMDocument();
+ @$doc->loadHTML($html);
+
+ $style = $doc->getElementsByTagName('style')->item(0);
+ $style->parentNode->removeChild($style);
+
+ $title = $doc->getElementsByTagName('title')->item(0);
+ $title->parentNode->removeChild($title);
+
+ $meta = $doc->getElementsByTagName('meta')->item(0);
+ $meta->parentNode->removeChild($meta);
+
+ $doc->getElementsByTagName('div')->item(0)->setAttribute('class', 'system-report');
+
+ return new HtmlString($doc->saveHTML());
+ }
+}
diff --git a/library/Reporting/RetryConnection.php b/library/Reporting/RetryConnection.php
new file mode 100644
index 0000000..ebadfd2
--- /dev/null
+++ b/library/Reporting/RetryConnection.php
@@ -0,0 +1,66 @@
+<?php
+// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting;
+
+use ipl\Sql\Connection;
+
+class RetryConnection extends Connection
+{
+ public function prepexec($stmt, $values = null)
+ {
+ try {
+ $sth = parent::prepexec($stmt, $values);
+ } catch (\Exception $e) {
+ $lostConnection = Str::contains($e->getMessage(), [
+ 'server has gone away',
+ 'no connection to the server',
+ 'Lost connection',
+ 'Error while sending',
+ 'is dead or not enabled',
+ 'decryption failed or bad record mac',
+ 'server closed the connection unexpectedly',
+ 'SSL connection has been closed unexpectedly',
+ 'Error writing data to the connection',
+ 'Resource deadlock avoided',
+ 'Transaction() on null',
+ 'child connection forced to terminate due to client_idle_limit',
+ 'query_wait_timeout',
+ 'reset by peer',
+ 'Physical connection is not usable',
+ 'TCP Provider: Error code 0x68',
+ 'ORA-03114',
+ 'Packets out of order. Expected',
+ 'Adaptive Server connection failed',
+ 'Communication link failure',
+ ]);
+
+ if (! $lostConnection) {
+ throw $e;
+ }
+
+ $this->disconnect();
+
+ try {
+ $this->connect();
+ } catch (\Exception $e) {
+ $noConnection = Str::contains($e->getMessage(), [
+ 'No such file or directory',
+ 'Connection refused'
+ ]);
+
+ if (! $noConnection) {
+ throw $e;
+ }
+
+ \sleep(10);
+
+ $this->connect();
+ }
+
+ $sth = parent::prepexec($stmt, $values);
+ }
+
+ return $sth;
+ }
+}
diff --git a/library/Reporting/Schedule.php b/library/Reporting/Schedule.php
new file mode 100644
index 0000000..e0ffa9f
--- /dev/null
+++ b/library/Reporting/Schedule.php
@@ -0,0 +1,160 @@
+<?php
+// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting;
+
+class Schedule
+{
+ /** @var int */
+ protected $id;
+
+ /** @var int */
+ protected $reportId;
+
+ /** @var \DateTime */
+ protected $start;
+
+ /** @var string */
+ protected $frequency;
+
+ /** @var string */
+ protected $action;
+
+ /** @var array */
+ protected $config;
+
+ /**
+ * @return int
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * @param int $id
+ *
+ * @return $this
+ */
+ public function setId($id)
+ {
+ $this->id = $id;
+
+ return $this;
+ }
+
+ /**
+ * @return int
+ */
+ public function getReportId()
+ {
+ return $this->reportId;
+ }
+
+ /**
+ * @param int $id
+ *
+ * @return $this
+ */
+ public function setReportId($id)
+ {
+ $this->reportId = $id;
+
+ return $this;
+ }
+
+ /**
+ * @return \DateTime
+ */
+ public function getStart()
+ {
+ return $this->start;
+ }
+
+ /**
+ * @param \DateTime $start
+ *
+ * @return $this
+ */
+ public function setStart(\DateTime $start)
+ {
+ $this->start = $start;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getFrequency()
+ {
+ return $this->frequency;
+ }
+
+ /**
+ * @param string $frequency
+ *
+ * @return $this
+ */
+ public function setFrequency($frequency)
+ {
+ $this->frequency = $frequency;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getAction()
+ {
+ return $this->action;
+ }
+
+ /**
+ * @param string $action
+ *
+ * @return $this
+ */
+ public function setAction($action)
+ {
+ $this->action = $action;
+
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getConfig()
+ {
+ return $this->config;
+ }
+
+ /**
+ * @param array $config
+ *
+ * @return $this
+ */
+ public function setConfig(array $config)
+ {
+ $this->config = $config;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getChecksum()
+ {
+ return \md5(
+ $this->getId()
+ . $this->getReportId()
+ . $this->getStart()->format('Y-m-d H:i:s')
+ . $this->getAction()
+ . $this->getFrequency()
+ . \json_encode($this->getConfig())
+ );
+ }
+}
diff --git a/library/Reporting/Scheduler.php b/library/Reporting/Scheduler.php
new file mode 100644
index 0000000..1b8d9f6
--- /dev/null
+++ b/library/Reporting/Scheduler.php
@@ -0,0 +1,176 @@
+<?php
+// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting;
+
+use Cron\CronExpression;
+use ipl\Sql\Connection;
+use ipl\Sql\Select;
+use React\EventLoop\Factory as Loop;
+
+function datetime_get_time_of_day(\DateTime $dateTime)
+{
+ $midnight = clone $dateTime;
+ $midnight->modify('midnight');
+
+ $diff = $midnight->diff($dateTime);
+
+ return $diff->h * 60 * 60 + $diff->i * 60 + $diff->s;
+}
+
+class Scheduler
+{
+ protected $db;
+
+ protected $loop;
+
+ /** @var array */
+ protected $schedules = [];
+
+ /** @var array */
+ protected $timers = [];
+
+ public function __construct(Connection $db)
+ {
+ $this->db = $db;
+ $this->loop = Loop::create();
+ }
+
+ public function run()
+ {
+ $updateTimers = function () use (&$updateTimers) {
+ $this->updateTimers();
+
+ $this->loop->addTimer(60, $updateTimers);
+ };
+
+ $this->loop->futureTick($updateTimers);
+
+ $this->loop->run();
+ }
+
+ protected function fetchSchedules()
+ {
+ $schedules = [];
+
+ $select = (new Select())
+ ->from('schedule')
+ ->columns('*');
+
+ foreach ($this->db->select($select) as $row) {
+ $schedule = (new Schedule())
+ ->setId((int) $row->id)
+ ->setReportId((int) $row->report_id)
+ ->setAction($row->action)
+ ->setConfig(\json_decode($row->config, true))
+ ->setStart((new \DateTime())->setTimestamp((int) $row->start / 1000))
+ ->setFrequency($row->frequency);
+
+ $schedules[$schedule->getChecksum()] = $schedule;
+ }
+
+ return $schedules;
+ }
+
+ protected function updateTimers()
+ {
+ $schedules = $this->fetchSchedules();
+
+ $remove = \array_diff_key($this->schedules, $schedules);
+
+ foreach ($remove as $schedule) {
+ printf("Removing job %s.\n", "Schedule {$schedule->getId()}");
+
+ $checksum = $schedule->getChecksum();
+
+ if (isset($this->timers[$checksum])) {
+ $this->loop->cancelTimer($this->timers[$checksum]);
+ unset($this->timers[$checksum]);
+ } else {
+ printf("Can't find timer for job %s.\n", $checksum);
+ }
+ }
+
+ $add = \array_diff_key($schedules, $this->schedules);
+
+ foreach ($add as $schedule) {
+ $this->add($schedule);
+ }
+
+ $this->schedules = $schedules;
+ }
+
+
+ protected function add(Schedule $schedule)
+ {
+ $name = "Schedule {$schedule->getId()}";
+ $frequency = $schedule->getFrequency();
+ $start = clone $schedule->getStart();
+ $callback = function () use ($schedule) {
+ $actionClass = $schedule->getAction();
+ /** @var ActionHook $action */
+ $action = new $actionClass;
+
+ $action->execute(
+ Report::fromDb($schedule->getReportId()),
+ $schedule->getConfig()
+ );
+ };
+
+ switch ($frequency) {
+ case 'minutely':
+ $modify = '+1 minute';
+ break;
+ case 'hourly':
+ $modify = '+1 hour';
+ break;
+ case 'daily':
+ $modify = '+1 day';
+ break;
+ case 'weekly':
+ $modify = '+1 week';
+ break;
+ case 'monthly':
+ $modify = '+1 month';
+ break;
+ default:
+ throw new \InvalidArgumentException('Invalid frequency.');
+ }
+
+ $now = new \DateTime();
+
+ if ($start < $now) {
+// printf("Scheduling job %s to run immediately.\n", $name);
+// $this->loop->futureTick($callback);
+
+ while ($start < $now) {
+ $start->modify($modify);
+ }
+ }
+
+ $next = clone $start;
+ $next->modify($modify);
+ $interval = $next->getTimestamp() - $start->getTimestamp();
+
+ $current = $start->getTimestamp() - $now->getTimestamp();
+
+ printf("Scheduling job %s to run at %s.\n", $name, $start->format('Y-m-d H:i:s'));
+
+ $loop = function () use (&$loop, $name, $callback, $interval, $schedule) {
+ $callback();
+
+ $nextRun = (new \DateTime())
+ ->add(new \DateInterval("PT{$interval}S"));
+
+ printf("Scheduling job %s to run at %s.\n", $name, $nextRun->format('Y-m-d H:i:s'));
+
+ $timer = $this->loop->addTimer($interval, $loop);
+
+ $this->timers[$schedule->getChecksum()] = $timer;
+ };
+
+ $timer = $this->loop->addTimer($current, $loop);
+
+ $this->timers[$schedule->getChecksum()] = $timer;
+ }
+}
diff --git a/library/Reporting/Str.php b/library/Reporting/Str.php
new file mode 100644
index 0000000..d4c7355
--- /dev/null
+++ b/library/Reporting/Str.php
@@ -0,0 +1,37 @@
+<?php
+// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting;
+
+class Str
+{
+ public static function putcsv(array $data, $delimiter = ',', $enclosure = '"', $escape = '\\')
+ {
+ $fp = fopen('php://temp', 'r+b');
+
+ foreach ($data as $row) {
+ fputcsv($fp, $row, $delimiter, $enclosure, $escape);
+ }
+
+ rewind($fp);
+
+ $csv = stream_get_contents($fp);
+
+ fclose($fp);
+
+ $csv = rtrim($csv, "\n"); // fputcsv adds a newline
+
+ return $csv;
+ }
+
+ public static function contains($haystack, $needle)
+ {
+ foreach ((array) $needle as $n) {
+ if (\strpos($haystack, $n) !== false) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/library/Reporting/Timeframe.php b/library/Reporting/Timeframe.php
new file mode 100644
index 0000000..f295779
--- /dev/null
+++ b/library/Reporting/Timeframe.php
@@ -0,0 +1,168 @@
+<?php
+// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting;
+
+use ipl\Sql\Select;
+
+class Timeframe
+{
+ use Database;
+
+ /** @var int */
+ protected $id;
+
+ /** @var string */
+ protected $name;
+
+ /** @var string */
+ protected $title;
+
+ /** @var string */
+ protected $start;
+
+ /** @var string */
+ protected $end;
+
+ /**
+ * @param int $id
+ *
+ * @return static
+ *
+ * @throws \Exception
+ */
+ public static function fromDb($id)
+ {
+ $timeframe = new static();
+
+ $db = $timeframe->getDb();
+
+ $select = (new Select())
+ ->from('timeframe')
+ ->columns('*')
+ ->where(['id = ?' => $id]);
+
+ $row = $db->select($select)->fetch();
+
+ if ($row === false) {
+ throw new \Exception('Timeframe not found');
+ }
+
+ $timeframe
+ ->setId($row->id)
+ ->setName($row->name)
+ ->setTitle($row->title)
+ ->setStart($row->start)
+ ->setEnd($row->end);
+
+ return $timeframe;
+ }
+
+ /**
+ * @return int
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * @param int $id
+ *
+ * @return $this
+ */
+ public function setId($id)
+ {
+ $this->id = $id;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setName($name)
+ {
+ $this->name = $name;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTitle()
+ {
+ return $this->title;
+ }
+
+ /**
+ * @param string $title
+ *
+ * @return $this
+ */
+ public function setTitle($title)
+ {
+ $this->title = $title;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getStart()
+ {
+ return $this->start;
+ }
+
+ /**
+ * @param string $start
+ *
+ * @return $this
+ */
+ public function setStart($start)
+ {
+ $this->start = $start;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getEnd()
+ {
+ return $this->end;
+ }
+
+ /**
+ * @param string $end
+ *
+ * @return $this
+ */
+ public function setEnd($end)
+ {
+ $this->end = $end;
+
+ return $this;
+ }
+
+ public function getTimerange()
+ {
+ $start = new \DateTime($this->getStart());
+ $end = new \DateTime($this->getEnd());
+
+ return new Timerange($start, $end);
+ }
+}
diff --git a/library/Reporting/Timerange.php b/library/Reporting/Timerange.php
new file mode 100644
index 0000000..086bfb8
--- /dev/null
+++ b/library/Reporting/Timerange.php
@@ -0,0 +1,35 @@
+<?php
+// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting;
+
+class Timerange
+{
+ /** @var \DateTime */
+ protected $start;
+
+ /** @var \DateTime */
+ protected $end;
+
+ public function __construct(\DateTime $start, \DateTime $end)
+ {
+ $this->start = $start;
+ $this->end = $end;
+ }
+
+ /**
+ * @return \DateTime
+ */
+ public function getStart()
+ {
+ return $this->start;
+ }
+
+ /**
+ * @return \DateTime
+ */
+ public function getEnd()
+ {
+ return $this->end;
+ }
+}
diff --git a/library/Reporting/Values.php b/library/Reporting/Values.php
new file mode 100644
index 0000000..3aa9b24
--- /dev/null
+++ b/library/Reporting/Values.php
@@ -0,0 +1,21 @@
+<?php
+// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting;
+
+trait Values
+{
+ protected $values;
+
+ public function getValues()
+ {
+ return $this->values;
+ }
+
+ public function setValues(array $values)
+ {
+ $this->values = $values;
+
+ return $this;
+ }
+}
diff --git a/library/Reporting/Web/Controller.php b/library/Reporting/Web/Controller.php
new file mode 100644
index 0000000..5040183
--- /dev/null
+++ b/library/Reporting/Web/Controller.php
@@ -0,0 +1,20 @@
+<?php
+// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting\Web;
+
+use ipl\Html\Form;
+use ipl\Web\Compat\CompatController;
+
+class Controller extends CompatController
+{
+ protected function redirectForm(Form $form, $url)
+ {
+ if ($form->hasBeenSubmitted()
+ && ((isset($form->valid) && $form->valid === true)
+ || $form->isValid())
+ ) {
+ $this->redirectNow($url);
+ }
+ }
+}
diff --git a/library/Reporting/Web/Flatpickr.php b/library/Reporting/Web/Flatpickr.php
new file mode 100644
index 0000000..5f6605d
--- /dev/null
+++ b/library/Reporting/Web/Flatpickr.php
@@ -0,0 +1,77 @@
+<?php
+// Icinga Reporting | (c) 2019 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting\Web;
+
+use Icinga\Application\Version;
+use ipl\Html\Html;
+use ipl\Web\Compat\CompatDecorator;
+
+class Flatpickr extends CompatDecorator
+{
+ protected $allowInput = true;
+
+ /**
+ * Set whether to allow manual input
+ *
+ * @param bool $state
+ *
+ * @return $this
+ */
+ public function setAllowInput(bool $state): self
+ {
+ $this->allowInput = $state;
+
+ return $this;
+ }
+
+ protected function assembleElement()
+ {
+ if (version_compare(Version::VERSION, '2.9.0', '>=')) {
+ $element = parent::assembleElement();
+ } else {
+ $element = $this->formElement;
+ }
+
+ if (version_compare(Version::VERSION, '2.10.0', '<')) {
+ $element->getAttributes()->set('data-use-flatpickr-fallback', true);
+ } else {
+ $element->getAttributes()->set('data-use-datetime-picker', true);
+ }
+
+ if (! $this->allowInput) {
+ return $element;
+ }
+
+ $element->getAttributes()
+ ->set('data-input', true)
+ ->set('data-flatpickr-wrap', true)
+ ->set('data-flatpickr-allow-input', true)
+ ->set('data-flatpickr-click-opens', 'false');
+
+ return [
+ $element,
+ Html::tag('button', ['type' => 'button', 'class' => 'icon-calendar', 'data-toggle' => true]),
+ Html::tag('button', ['type' => 'button', 'class' => 'icon-cancel', 'data-clear' => true])
+ ];
+ }
+
+ protected function assemble()
+ {
+ if (version_compare(Version::VERSION, '2.9.0', '>=')) {
+ parent::assemble();
+ return;
+ }
+
+ if ($this->formElement->hasBeenValidated() && ! $this->formElement->isValid()) {
+ $this->getAttributes()->add('class', 'has-error');
+ }
+
+ $this->add(array_filter([
+ $this->assembleLabel(),
+ $this->assembleElement(),
+ $this->assembleDescription(),
+ $this->assembleErrors()
+ ]));
+ }
+}
diff --git a/library/Reporting/Web/Forms/DecoratedElement.php b/library/Reporting/Web/Forms/DecoratedElement.php
new file mode 100644
index 0000000..2578681
--- /dev/null
+++ b/library/Reporting/Web/Forms/DecoratedElement.php
@@ -0,0 +1,17 @@
+<?php
+// Icinga Reporting | (c) 2019 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting\Web\Forms;
+
+use ipl\Html\Contract\FormElementDecorator;
+
+trait DecoratedElement
+{
+ protected function addDecoratedElement(FormElementDecorator $decorator, $type, $name, array $attributes)
+ {
+ $element = $this->createElement($type, $name, $attributes);
+ $decorator->decorate($element);
+ $this->registerElement($element);
+ $this->add($element);
+ }
+}
diff --git a/library/Reporting/Web/Forms/Decorator/CompatDecorator.php b/library/Reporting/Web/Forms/Decorator/CompatDecorator.php
new file mode 100644
index 0000000..b2eb536
--- /dev/null
+++ b/library/Reporting/Web/Forms/Decorator/CompatDecorator.php
@@ -0,0 +1,63 @@
+<?php
+// Icinga Reporting | (c) 2021 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting\Web\Forms\Decorator;
+
+use Icinga\Application\Version;
+use ipl\Html\Attributes;
+use ipl\Html\FormElement\CheckboxElement;
+use ipl\Html\HtmlElement;
+
+class CompatDecorator extends \ipl\Web\Compat\CompatDecorator
+{
+ protected function createCheckboxCompat(CheckboxElement $checkbox)
+ {
+ if (! $checkbox->getAttributes()->has('id')) {
+ $checkbox->setAttribute('id', base64_encode(random_bytes(8)));
+ }
+
+ $checkbox->getAttributes()->add('class', 'sr-only');
+
+ $classes = ['toggle-switch'];
+ if ($checkbox->getAttributes()->get('disabled')->getValue()) {
+ $classes[] = 'disabled';
+ }
+
+ return [
+ $checkbox,
+ new HtmlElement('label', Attributes::create([
+ 'class' => $classes,
+ 'aria-hidden' => 'true',
+ 'for' => $checkbox->getAttributes()->get('id')->getValue()
+ ]), new HtmlElement('span', Attributes::create(['class' => 'toggle-slider'])))
+ ];
+ }
+
+ protected function assembleElementCompat()
+ {
+ if ($this->formElement instanceof CheckboxElement) {
+ return $this->createCheckboxCompat($this->formElement);
+ }
+
+ return $this->formElement;
+ }
+
+ protected function assemble()
+ {
+ if (version_compare(Version::VERSION, '2.9.0', '>=')) {
+ parent::assemble();
+ return;
+ }
+
+ if ($this->formElement->hasBeenValidated() && ! $this->formElement->isValid()) {
+ $this->getAttributes()->add('class', 'has-error');
+ }
+
+ $this->add(array_filter([
+ $this->assembleLabel(),
+ $this->assembleElementCompat(),
+ $this->assembleDescription(),
+ $this->assembleErrors()
+ ]));
+ }
+}
diff --git a/library/Reporting/Web/Forms/ReportForm.php b/library/Reporting/Web/Forms/ReportForm.php
new file mode 100644
index 0000000..6b1e692
--- /dev/null
+++ b/library/Reporting/Web/Forms/ReportForm.php
@@ -0,0 +1,168 @@
+<?php
+// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting\Web\Forms;
+
+use Icinga\Authentication\Auth;
+use Icinga\Module\Reporting\Database;
+use Icinga\Module\Reporting\ProvidedReports;
+use Icinga\Module\Reporting\Web\Forms\Decorator\CompatDecorator;
+use ipl\Html\Contract\FormSubmitElement;
+use ipl\Html\Form;
+use ipl\Web\Compat\CompatForm;
+
+class ReportForm extends CompatForm
+{
+ use Database;
+ use ProvidedReports;
+
+ /** @var bool Hack to disable the {@link onSuccess()} code upon deletion of the report */
+ protected $callOnSuccess;
+
+ protected $id;
+
+ public function setId($id)
+ {
+ $this->id = $id;
+
+ return $this;
+ }
+
+ protected function assemble()
+ {
+ $this->setDefaultElementDecorator(new CompatDecorator());
+
+ $this->addElement('text', 'name', [
+ 'required' => true,
+ 'label' => 'Name'
+ ]);
+
+ $this->addElement('select', 'timeframe', [
+ 'required' => true,
+ 'label' => 'Timeframe',
+ 'options' => [null => 'Please choose'] + $this->listTimeframes(),
+ 'class' => 'autosubmit'
+ ]);
+
+ $this->addElement('select', 'template', [
+ 'label' => 'Template',
+ 'options' => [null => 'Please choose'] + $this->listTemplates()
+ ]);
+
+ $this->addElement('select', 'reportlet', [
+ 'required' => true,
+ 'label' => 'Report',
+ 'options' => [null => 'Please choose'] + $this->listReports(),
+ 'class' => 'autosubmit'
+ ]);
+
+ $values = $this->getValues();
+
+ if (isset($values['reportlet'])) {
+ $config = new Form();
+// $config->populate($this->getValues());
+
+ /** @var \Icinga\Module\Reporting\Hook\ReportHook $reportlet */
+ $reportlet = new $values['reportlet'];
+
+ $reportlet->initConfigForm($config);
+
+ foreach ($config->getElements() as $element) {
+ $this->addElement($element);
+ }
+ }
+
+ $this->addElement('submit', 'submit', [
+ 'label' => $this->id === null ? 'Create Report' : 'Update Report'
+ ]);
+
+ if ($this->id !== null) {
+ /** @var FormSubmitElement $removeButton */
+ $removeButton = $this->createElement('submit', 'remove', [
+ 'label' => 'Remove Report',
+ 'class' => 'btn-remove',
+ 'formnovalidate' => true
+ ]);
+ $this->registerElement($removeButton);
+ $this->getElement('submit')->getWrapper()->prepend($removeButton);
+
+ if ($removeButton->hasBeenPressed()) {
+ $this->getDb()->delete('report', ['id = ?' => $this->id]);
+
+ // Stupid cheat because ipl/html is not capable of multiple submit buttons
+ $this->getSubmitButton()->setValue($this->getSubmitButton()->getButtonLabel());
+ $this->callOnSuccess = false;
+ $this->valid = true;
+
+ return;
+ }
+ }
+ }
+
+ public function onSuccess()
+ {
+ if ($this->callOnSuccess === false) {
+ return;
+ }
+
+ $db = $this->getDb();
+
+ $values = $this->getValues();
+
+ $now = time() * 1000;
+
+ $db->beginTransaction();
+
+ if ($this->id === null) {
+ $db->insert('report', [
+ 'name' => $values['name'],
+ 'author' => Auth::getInstance()->getUser()->getUsername(),
+ 'timeframe_id' => $values['timeframe'],
+ 'template_id' => $values['template'],
+ 'ctime' => $now,
+ 'mtime' => $now
+ ]);
+
+ $reportId = $db->lastInsertId();
+ } else {
+ $db->update('report', [
+ 'name' => $values['name'],
+ 'timeframe_id' => $values['timeframe'],
+ 'template_id' => $values['template'],
+ 'mtime' => $now
+ ], ['id = ?' => $this->id]);
+
+ $reportId = $this->id;
+ }
+
+ unset($values['name']);
+ unset($values['timeframe']);
+
+ if ($this->id !== null) {
+ $db->delete('reportlet', ['report_id = ?' => $reportId]);
+ }
+
+ $db->insert('reportlet', [
+ 'report_id' => $reportId,
+ 'class' => $values['reportlet'],
+ 'ctime' => $now,
+ 'mtime' => $now
+ ]);
+
+ $reportletId = $db->lastInsertId();
+
+ unset($values['reportlet']);
+
+ foreach ($values as $name => $value) {
+ $db->insert('config', [
+ 'reportlet_id' => $reportletId,
+ 'name' => $name,
+ 'value' => $value,
+ 'ctime' => $now,
+ 'mtime' => $now
+ ]);
+ }
+
+ $db->commitTransaction();
+ }
+}
diff --git a/library/Reporting/Web/Forms/ScheduleForm.php b/library/Reporting/Web/Forms/ScheduleForm.php
new file mode 100644
index 0000000..47f3ee3
--- /dev/null
+++ b/library/Reporting/Web/Forms/ScheduleForm.php
@@ -0,0 +1,177 @@
+<?php
+// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting\Web\Forms;
+
+use DateTime;
+use Icinga\Application\Version;
+use Icinga\Authentication\Auth;
+use Icinga\Module\Reporting\Database;
+use Icinga\Module\Reporting\ProvidedActions;
+use Icinga\Module\Reporting\Report;
+use Icinga\Module\Reporting\Web\Flatpickr;
+use Icinga\Module\Reporting\Web\Forms\Decorator\CompatDecorator;
+use ipl\Html\Contract\FormSubmitElement;
+use ipl\Html\Form;
+use ipl\Web\Compat\CompatForm;
+
+class ScheduleForm extends CompatForm
+{
+ use Database;
+ use DecoratedElement;
+ use ProvidedActions;
+
+ /** @var Report */
+ protected $report;
+
+ protected $id;
+
+ public function setReport(Report $report)
+ {
+ $this->report = $report;
+
+ $schedule = $report->getSchedule();
+
+ if ($schedule !== null) {
+ $this->setId($schedule->getId());
+
+ $values = [
+ 'start' => $schedule->getStart()->format('Y-m-d\\TH:i:s'),
+ 'frequency' => $schedule->getFrequency(),
+ 'action' => $schedule->getAction()
+ ] + $schedule->getConfig();
+
+ $this->populate($values);
+ }
+
+ return $this;
+ }
+
+ public function setId($id)
+ {
+ $this->id = $id;
+
+ return $this;
+ }
+
+ protected function assemble()
+ {
+ $this->setDefaultElementDecorator(new CompatDecorator());
+
+ $frequency = [
+ 'minutely' => 'Minutely',
+ 'hourly' => 'Hourly',
+ 'daily' => 'Daily',
+ 'weekly' => 'Weekly',
+ 'monthly' => 'Monthly'
+ ];
+
+ if (version_compare(Version::VERSION, '2.9.0', '>=')) {
+ $this->addElement('localDateTime', 'start', [
+ 'required' => true,
+ 'label' => t('Start'),
+ 'placeholder' => t('Choose date and time')
+ ]);
+ } else {
+ $this->addDecoratedElement((new Flatpickr())->setAllowInput(false), 'text', 'start', [
+ 'required' => true,
+ 'label' => t('Start'),
+ 'placeholder' => t('Choose date and time')
+ ]);
+ }
+
+ $this->addElement('select', 'frequency', [
+ 'required' => true,
+ 'label' => 'Frequency',
+ 'options' => [null => 'Please choose'] + $frequency,
+ ]);
+
+ $this->addElement('select', 'action', [
+ 'required' => true,
+ 'label' => 'Action',
+ 'options' => [null => 'Please choose'] + $this->listActions(),
+ 'class' => 'autosubmit'
+ ]);
+
+ $values = $this->getValues();
+
+ if (isset($values['action'])) {
+ $config = new Form();
+// $config->populate($this->getValues());
+
+ /** @var \Icinga\Module\Reporting\Hook\ActionHook $action */
+ $action = new $values['action'];
+
+ $action->initConfigForm($config, $this->report);
+
+ foreach ($config->getElements() as $element) {
+ $this->addElement($element);
+ }
+ }
+
+ $this->addElement('submit', 'submit', [
+ 'label' => $this->id === null ? 'Create Schedule' : 'Update Schedule'
+ ]);
+
+ if ($this->id !== null) {
+ /** @var FormSubmitElement $removeButton */
+ $removeButton = $this->createElement('submit', 'remove', [
+ 'label' => 'Remove Schedule',
+ 'class' => 'btn-remove',
+ 'formnovalidate' => true
+ ]);
+ $this->registerElement($removeButton);
+ $this->getElement('submit')->getWrapper()->prepend($removeButton);
+
+ if ($removeButton->hasBeenPressed()) {
+ $this->getDb()->delete('schedule', ['id = ?' => $this->id]);
+
+ // Stupid cheat because ipl/html is not capable of multiple submit buttons
+ $this->getSubmitButton()->setValue($this->getSubmitButton()->getButtonLabel());
+ $this->valid = true;
+
+ return;
+ }
+ }
+ }
+
+ public function onSuccess()
+ {
+ $db = $this->getDb();
+
+ $values = $this->getValues();
+
+ $now = time() * 1000;
+
+ if (! $values['start'] instanceof DateTime) {
+ $values['start'] = DateTime::createFromFormat('Y-m-d H:i:s', $values['start']);
+ }
+
+ $data = [
+ 'start' => $values['start']->getTimestamp() * 1000,
+ 'frequency' => $values['frequency'],
+ 'action' => $values['action'],
+ 'mtime' => $now
+ ];
+
+ unset($values['start']);
+ unset($values['frequency']);
+ unset($values['action']);
+
+ $data['config'] = json_encode($values);
+
+ $db->beginTransaction();
+
+ if ($this->id === null) {
+ $db->insert('schedule', $data + [
+ 'author' => Auth::getInstance()->getUser()->getUsername(),
+ 'report_id' => $this->report->getId(),
+ 'ctime' => $now
+ ]);
+ } else {
+ $db->update('schedule', $data, ['id = ?' => $this->id]);
+ }
+
+ $db->commitTransaction();
+ }
+}
diff --git a/library/Reporting/Web/Forms/SendForm.php b/library/Reporting/Web/Forms/SendForm.php
new file mode 100644
index 0000000..03b691c
--- /dev/null
+++ b/library/Reporting/Web/Forms/SendForm.php
@@ -0,0 +1,47 @@
+<?php
+// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting\Web\Forms;
+
+use Icinga\Module\Reporting\Actions\SendMail;
+use Icinga\Module\Reporting\Database;
+use Icinga\Module\Reporting\ProvidedReports;
+use Icinga\Module\Reporting\Report;
+use Icinga\Module\Reporting\Web\Forms\Decorator\CompatDecorator;
+use ipl\Web\Compat\CompatForm;
+
+class SendForm extends CompatForm
+{
+ use Database;
+ use ProvidedReports;
+
+ /** @var Report */
+ protected $report;
+
+ public function setReport(Report $report)
+ {
+ $this->report = $report;
+
+ return $this;
+ }
+
+ protected function assemble()
+ {
+ $this->setDefaultElementDecorator(new CompatDecorator());
+
+ (new SendMail())->initConfigForm($this, $this->report);
+
+ $this->addElement('submit', 'submit', [
+ 'label' => 'Send Report'
+ ]);
+ }
+
+ public function onSuccess()
+ {
+ $values = $this->getValues();
+
+ $sendMail = new SendMail();
+
+ $sendMail->execute($this->report, $values);
+ }
+}
diff --git a/library/Reporting/Web/Forms/TemplateForm.php b/library/Reporting/Web/Forms/TemplateForm.php
new file mode 100644
index 0000000..bb062bb
--- /dev/null
+++ b/library/Reporting/Web/Forms/TemplateForm.php
@@ -0,0 +1,284 @@
+<?php
+// Icinga Reporting | (c) 2019 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting\Web\Forms;
+
+use Icinga\Authentication\Auth;
+use Icinga\Module\Reporting\Database;
+use Icinga\Module\Reporting\Web\Forms\Decorator\CompatDecorator;
+use ipl\Html\Contract\FormSubmitElement;
+use ipl\Html\Html;
+use ipl\Web\Compat\CompatForm;
+use reportingipl\Html\FormElement\FileElement;
+
+class TemplateForm extends CompatForm
+{
+ use Database;
+
+ /** @var bool Hack to disable the {@link onSuccess()} code upon deletion of the template */
+ protected $callOnSuccess;
+
+ protected $template;
+
+ public function getTemplate()
+ {
+ return $this->template;
+ }
+
+ public function setTemplate($template)
+ {
+ $this->template = $template;
+
+ if ($template->settings) {
+ $this->populate(array_filter($template->settings, function ($value) {
+ // Don't populate files
+ return ! is_array($value);
+ }));
+ }
+
+ return $this;
+ }
+
+ protected function assemble()
+ {
+ $this->setDefaultElementDecorator(new CompatDecorator());
+
+ $this->setAttribute('enctype', 'multipart/form-data');
+
+ $this->add(Html::tag('h2', 'Template Settings'));
+
+ $this->addElement('text', 'name', [
+ 'label' => 'Name',
+ 'placeholder' => 'Template name',
+ 'required' => true
+ ]);
+
+ $this->add(Html::tag('h2', 'Cover Page Settings'));
+
+ $this->addElement(new FileElement('cover_page_background_image', [
+ 'label' => 'Background Image',
+ 'accept' => 'image/png, image/jpeg'
+ ]));
+
+ if ($this->template !== null
+ && isset($this->template->settings['cover_page_background_image'])
+ ) {
+ $this->add(Html::tag(
+ 'p',
+ ['style' => ['margin-left: 14em;']],
+ 'Upload a new background image to override the existing one'
+ ));
+
+ $this->addElement('checkbox', 'remove_cover_page_background_image', [
+ 'label' => 'Remove background image'
+ ]);
+ }
+
+ $this->addElement(new FileElement('cover_page_logo', [
+ 'label' => 'Logo',
+ 'accept' => 'image/png, image/jpeg'
+ ]));
+
+ if ($this->template !== null
+ && isset($this->template->settings['cover_page_logo'])
+ ) {
+ $this->add(Html::tag(
+ 'p',
+ ['style' => ['margin-left: 14em;']],
+ 'Upload a new logo to override the existing one'
+ ));
+
+ $this->addElement('checkbox', 'remove_cover_page_logo', [
+ 'label' => 'Remove Logo'
+ ]);
+ }
+
+ $this->addElement('textarea', 'title', [
+ 'label' => 'Title',
+ 'placeholder' => 'Report title'
+ ]);
+
+ $this->addElement('text', 'color', [
+ 'label' => 'Color',
+ 'placeholder' => 'CSS color code'
+ ]);
+
+ $this->add(Html::tag('h2', 'Header Settings'));
+
+ $this->addColumnSettings('header_column1', 'Column 1');
+ $this->addColumnSettings('header_column2', 'Column 2');
+ $this->addColumnSettings('header_column3', 'Column 3');
+
+ $this->add(Html::tag('h2', 'Footer Settings'));
+
+ $this->addColumnSettings('footer_column1', 'Column 1');
+ $this->addColumnSettings('footer_column2', 'Column 2');
+ $this->addColumnSettings('footer_column3', 'Column 3');
+
+ $this->addElement('submit', 'submit', [
+ 'label' => $this->template === null ? 'Create Template' : 'Update Template'
+ ]);
+
+ if ($this->template !== null) {
+ /** @var FormSubmitElement $removeButton */
+ $removeButton = $this->createElement('submit', 'remove', [
+ 'label' => 'Remove Template',
+ 'class' => 'btn-remove',
+ 'formnovalidate' => true
+ ]);
+ $this->registerElement($removeButton);
+ $this->getElement('submit')->getWrapper()->prepend($removeButton);
+
+ if ($removeButton->hasBeenPressed()) {
+ $this->getDb()->delete('template', ['id = ?' => $this->template->id]);
+
+ // Stupid cheat because ipl/html is not capable of multiple submit buttons
+ $this->getSubmitButton()->setValue($this->getSubmitButton()->getButtonLabel());
+ $this->callOnSuccess = false;
+ $this->valid = true;
+
+ return;
+ }
+ }
+ }
+
+ public function onSuccess()
+ {
+ if ($this->callOnSuccess === false) {
+ return;
+ }
+
+ ini_set('upload_max_filesize', '10M');
+
+ $settings = $this->getValues();
+
+ try {
+ /** @var $uploadedFile \GuzzleHttp\Psr7\UploadedFile */
+ foreach ($this->getRequest()->getUploadedFiles() as $name => $uploadedFile) {
+ if ($uploadedFile->getError() === UPLOAD_ERR_NO_FILE) {
+ continue;
+ }
+
+ $settings[$name] = [
+ 'mime_type' => $uploadedFile->getClientMediaType(),
+ 'size' => $uploadedFile->getSize(),
+ 'content' => base64_encode((string) $uploadedFile->getStream())
+ ];
+ }
+
+ $db = $this->getDb();
+
+ $now = time() * 1000;
+
+ if ($this->template === null) {
+ $db->insert('template', [
+ 'name' => $settings['name'],
+ 'author' => Auth::getInstance()->getUser()->getUsername(),
+ 'settings' => json_encode($settings),
+ 'ctime' => $now,
+ 'mtime' => $now
+ ]);
+ } else {
+ if (isset($settings['remove_cover_page_background_image'])) {
+ unset($settings['cover_page_background_image']);
+ unset($settings['remove_cover_page_background_image']);
+ } elseif (! isset($settings['cover_page_background_image'])
+ && isset($this->template->settings['cover_page_background_image'])
+ ) {
+ $settings['cover_page_background_image'] = $this->template->settings['cover_page_background_image'];
+ }
+
+ if (isset($settings['remove_cover_page_logo'])) {
+ unset($settings['cover_page_logo']);
+ unset($settings['remove_cover_page_logo']);
+ } elseif (! isset($settings['cover_page_logo'])
+ && isset($this->template->settings['cover_page_logo'])
+ ) {
+ $settings['cover_page_logo'] = $this->template->settings['cover_page_logo'];
+ }
+
+ foreach (['header', 'footer'] as $headerOrFooter) {
+ for ($i = 1; $i <= 3; ++$i) {
+ $type = "{$headerOrFooter}_column{$i}_type";
+
+ if ($settings[$type] === 'image') {
+ $value = "{$headerOrFooter}_column{$i}_value";
+
+ if (! isset($settings[$value])
+ && isset($this->template->settings[$value])
+ ) {
+ $settings[$value] = $this->template->settings[$value];
+ }
+ }
+ }
+ }
+
+ $db->update('template', [
+ 'name' => $settings['name'],
+ 'settings' => json_encode($settings),
+ 'mtime' => $now
+ ], ['id = ?' => $this->template->id]);
+ }
+ } catch (\Exception $e) {
+ die($e->getMessage());
+ }
+ }
+
+ protected function addColumnSettings($name, $label)
+ {
+ $type = "{$name}_type";
+ $value = "{$name}_value";
+
+ $this->addElement('select', $type, [
+ 'class' => 'autosubmit',
+ 'label' => $label,
+ 'options' => [
+ null => 'None',
+ 'text' => 'Text',
+ 'image' => 'Image',
+ 'variable' => 'Variable'
+ ]
+ ]);
+
+ switch ($this->getValue($type, 'none')) {
+ case 'image':
+ $this->addElement(new FileElement($value, [
+ 'label' => 'Image',
+ 'accept' => 'image/png, image/jpeg'
+ ]));
+
+ if ($this->template !== null
+ && $this->template->settings[$type] === 'image'
+ && isset($this->template->settings[$value])
+ ) {
+ $this->add(Html::tag(
+ 'p',
+ ['style' => ['margin-left: 14em;']],
+ 'Upload a new image to override the existing one'
+ ));
+ }
+ break;
+ case 'variable':
+ $this->addElement('select', $value, [
+ 'label' => 'Variable',
+ 'options' => [
+ 'report_title' => 'Report Title',
+ 'time_frame' => 'Time Frame',
+ 'time_frame_absolute' => 'Time Frame (absolute)',
+ 'page_number' => 'Page Number',
+ 'total_number_of_pages' => 'Total Number of Pages',
+ 'page_of' => 'Page Number + Total Number of Pages',
+ 'date' => 'Date'
+ ],
+ 'value' => 'report_title'
+ ]);
+ break;
+ case 'text':
+ $this->addElement('text', $value, [
+ 'label' => 'Text',
+ 'placeholder' => 'Column text'
+ ]);
+ break;
+ }
+ }
+}
diff --git a/library/Reporting/Web/Forms/TimeframeForm.php b/library/Reporting/Web/Forms/TimeframeForm.php
new file mode 100644
index 0000000..3d78709
--- /dev/null
+++ b/library/Reporting/Web/Forms/TimeframeForm.php
@@ -0,0 +1,106 @@
+<?php
+// Icinga Reporting | (c) 2019 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting\Web\Forms;
+
+use Icinga\Module\Reporting\Database;
+use Icinga\Module\Reporting\Web\Flatpickr;
+use Icinga\Module\Reporting\Web\Forms\Decorator\CompatDecorator;
+use ipl\Html\Contract\FormSubmitElement;
+use ipl\Web\Compat\CompatForm;
+
+class TimeframeForm extends CompatForm
+{
+ use Database;
+ use DecoratedElement;
+
+ protected $id;
+
+ public function setId($id)
+ {
+ $this->id = $id;
+
+ return $this;
+ }
+
+ protected function assemble()
+ {
+ $this->setDefaultElementDecorator(new CompatDecorator());
+
+ $this->addElement('text', 'name', [
+ 'required' => true,
+ 'label' => 'Name'
+ ]);
+
+ $flatpickr = new Flatpickr();
+
+ $this->addDecoratedElement($flatpickr, 'text', 'start', [
+ 'required' => true,
+ 'label' => 'Start',
+ 'placeholder' => 'Select a start date or provide a textual datetime description',
+ 'data-flatpickr-default-hour' => '00'
+ ]);
+
+ $this->addDecoratedElement($flatpickr, 'text', 'end', [
+ 'required' => true,
+ 'label' => 'End',
+ 'placeholder' => 'Select a end date or provide a textual datetime description',
+ 'data-flatpickrDefaultHour' => '23',
+ 'data-flatpickrDefaultMinute' => '59',
+ 'data-flatpickrDefaultSeconds' => '59'
+ ]);
+
+ $this->addElement('submit', 'submit', [
+ 'label' => $this->id === null ? 'Create Time Frame' : 'Update Time Frame'
+ ]);
+
+ if ($this->id !== null) {
+ /** @var FormSubmitElement $removeButton */
+ $removeButton = $this->createElement('submit', 'remove', [
+ 'label' => 'Remove Time Frame',
+ 'class' => 'btn-remove',
+ 'formnovalidate' => true
+ ]);
+ $this->registerElement($removeButton);
+ $this->getElement('submit')->getWrapper()->prepend($removeButton);
+
+ if ($removeButton->hasBeenPressed()) {
+ $this->getDb()->delete('timeframe', ['id = ?' => $this->id]);
+
+ // Stupid cheat because ipl/html is not capable of multiple submit buttons
+ $this->getSubmitButton()->setValue($this->getSubmitButton()->getButtonLabel());
+ $this->valid = true;
+
+ return;
+ }
+ }
+ }
+
+ public function onSuccess()
+ {
+ $db = $this->getDb();
+
+ $values = $this->getValues();
+
+ $now = time() * 1000;
+
+ $end = $db->quoteIdentifier('end');
+
+ if ($this->id === null) {
+ $db->insert('timeframe', [
+ 'name' => $values['name'],
+ 'start' => $values['start'],
+ $end => $values['end'],
+ 'ctime' => $now,
+ 'mtime' => $now
+ ]);
+ } else {
+ $db->update('timeframe', [
+ 'name' => $values['name'],
+ 'start' => $values['start'],
+ $end => $values['end'],
+ 'mtime' => $now
+ ], ['id = ?' => $this->id]);
+ }
+ }
+}
diff --git a/library/Reporting/Web/ReportsTimeframesAndTemplatesTabs.php b/library/Reporting/Web/ReportsTimeframesAndTemplatesTabs.php
new file mode 100644
index 0000000..afb8b14
--- /dev/null
+++ b/library/Reporting/Web/ReportsTimeframesAndTemplatesTabs.php
@@ -0,0 +1,37 @@
+<?php
+// Icinga Reporting | (c) 2019 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting\Web;
+
+trait ReportsTimeframesAndTemplatesTabs
+{
+ /**
+ * Create tabs
+ *
+ * @return \Icinga\Web\Widget\Tabs
+ */
+ protected function createTabs()
+ {
+ $tabs = $this->getTabs();
+
+ $tabs->add('reports', [
+ 'title' => $this->translate('Show reports'),
+ 'label' => $this->translate('Reports'),
+ 'url' => 'reporting/reports'
+ ]);
+
+ $tabs->add('timeframes', [
+ 'title' => $this->translate('Show time frames'),
+ 'label' => $this->translate('Time Frames'),
+ 'url' => 'reporting/timeframes'
+ ]);
+
+ $tabs->add('templates', [
+ 'title' => $this->translate('Show templates'),
+ 'label' => $this->translate('Templates'),
+ 'url' => 'reporting/templates'
+ ]);
+
+ return $tabs;
+ }
+}
diff --git a/library/Reporting/Web/Widget/CompatDropdown.php b/library/Reporting/Web/Widget/CompatDropdown.php
new file mode 100644
index 0000000..cdd7b40
--- /dev/null
+++ b/library/Reporting/Web/Widget/CompatDropdown.php
@@ -0,0 +1,22 @@
+<?php
+// Icinga Reporting | (c) 2021 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting\Web\Widget;
+
+use ipl\Web\Widget\ActionLink;
+use ipl\Web\Widget\Dropdown;
+
+class CompatDropdown extends Dropdown
+{
+ public function addLink($content, $url, $icon = null, array $attributes = null)
+ {
+ $link = new ActionLink($content, $url, $icon, ['class' => 'dropdown-item']);
+ if (! empty($attributes)) {
+ $link->addAttributes($attributes);
+ }
+
+ $this->links[] = $link;
+
+ return $this;
+ }
+}
diff --git a/library/Reporting/Web/Widget/CoverPage.php b/library/Reporting/Web/Widget/CoverPage.php
new file mode 100644
index 0000000..545ef6a
--- /dev/null
+++ b/library/Reporting/Web/Widget/CoverPage.php
@@ -0,0 +1,181 @@
+<?php
+
+namespace Icinga\Module\Reporting\Web\Widget;
+
+use Icinga\Module\Reporting\Common\Macros;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+
+class CoverPage extends BaseHtmlElement
+{
+ use Macros;
+
+ /** @var array */
+ protected $backgroundImage;
+
+ /** @var string */
+ protected $color;
+
+ /** @var array */
+ protected $logo;
+
+ /** @var string */
+ protected $title;
+
+ protected $tag = 'div';
+
+ protected $defaultAttributes = ['class' => 'cover-page page'];
+
+ /**
+ * @return bool
+ */
+ public function hasBackgroundImage()
+ {
+ return $this->backgroundImage !== null;
+ }
+
+ /**
+ * @return array
+ */
+ public function getBackgroundImage()
+ {
+ return $this->backgroundImage;
+ }
+
+ /**
+ * @param array $backgroundImage
+ *
+ * @return $this
+ */
+ public function setBackgroundImage($backgroundImage)
+ {
+ $this->backgroundImage = $backgroundImage;
+
+ return $this;
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasColor()
+ {
+ return $this->color !== null;
+ }
+
+ /**
+ * @return string
+ */
+ public function getColor()
+ {
+ return $this->color;
+ }
+
+ /**
+ * @param string $color
+ *
+ * @return $this
+ */
+ public function setColor($color)
+ {
+ $this->color = $color;
+
+ return $this;
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasLogo()
+ {
+ return $this->logo !== null;
+ }
+
+ /**
+ * @return array
+ */
+ public function getLogo()
+ {
+ return $this->logo;
+ }
+
+ /**
+ * @param array $logo
+ *
+ * @return $this
+ */
+ public function setLogo($logo)
+ {
+ $this->logo = $logo;
+
+ return $this;
+ }
+
+ public function hasTitle()
+ {
+ return $this->title !== null;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTitle()
+ {
+ return $this->title;
+ }
+
+ /**
+ * @param string $title
+ *
+ * @return $this
+ */
+ public function setTitle($title)
+ {
+ $this->title = $title;
+
+ return $this;
+ }
+
+ protected function assemble()
+ {
+ if ($this->hasBackgroundImage()) {
+ $this
+ ->getAttributes()
+ ->add('style', "background-image: url('" . Template::getDataUrl($this->getBackgroundImage()) . "');");
+ }
+
+ $content = Html::tag('div', ['class' => 'cover-page-content']);
+
+ if ($this->hasColor()) {
+ $content->getAttributes()->add('style', "color: {$this->getColor()};");
+ }
+
+ if ($this->hasLogo()) {
+ $content->add(Html::tag(
+ 'img',
+ [
+ 'class' => 'logo',
+ 'src' => Template::getDataUrl($this->getLogo())
+ ]
+ ));
+ }
+
+ if ($this->hasTitle()) {
+ $title = array_map(function ($part) {
+ $part = trim($part);
+
+ if (! $part) {
+ return Html::tag('br');
+ } else {
+ return Html::tag('div', null, $part);
+ }
+ }, explode("\n", $this->resolveMacros($this->getTitle())));
+
+ $content->add(Html::tag(
+ 'h2',
+ $title
+ ));
+ }
+
+ $this->add($content);
+ }
+}
diff --git a/library/Reporting/Web/Widget/HeaderOrFooter.php b/library/Reporting/Web/Widget/HeaderOrFooter.php
new file mode 100644
index 0000000..dcb37e7
--- /dev/null
+++ b/library/Reporting/Web/Widget/HeaderOrFooter.php
@@ -0,0 +1,95 @@
+<?php
+
+namespace Icinga\Module\Reporting\Web\Widget;
+
+use Icinga\Module\Reporting\Common\Macros;
+use ipl\Html\Html;
+use ipl\Html\HtmlDocument;
+
+class HeaderOrFooter extends HtmlDocument
+{
+ use Macros;
+
+ const HEADER = 'header';
+
+ const FOOTER = 'footer';
+
+ protected $type;
+
+ protected $data;
+
+ protected $tag = 'div';
+
+ public function __construct($type, array $data)
+ {
+ $this->type = $type;
+ $this->data = $data;
+ }
+
+ protected function resolveVariable($variable)
+ {
+ switch ($variable) {
+ case 'report_title':
+ $resolved = Html::tag('span', ['class' => 'title']);
+ break;
+ case 'time_frame':
+ $resolved = Html::tag('p', $this->getMacro('time_frame'));
+ break;
+ case 'time_frame_absolute':
+ $resolved = Html::tag('p', $this->getMacro('time_frame_absolute'));
+ break;
+ case 'page_number':
+ $resolved = Html::tag('span', ['class' => 'pageNumber']);
+ break;
+ case 'total_number_of_pages':
+ $resolved = Html::tag('span', ['class' => 'totalPages']);
+ break;
+ case 'page_of':
+ $resolved = Html::tag('p', Html::sprintf(
+ '%s / %s',
+ Html::tag('span', ['class' => 'pageNumber']),
+ Html::tag('span', ['class' => 'totalPages'])
+ ));
+ break;
+ case 'date':
+ $resolved = Html::tag('span', ['class' => 'date']);
+ break;
+ default:
+ $resolved = $variable;
+ break;
+ }
+
+ return $resolved;
+ }
+
+ protected function createColumn(array $data, $key)
+ {
+ $typeKey = "${key}_type";
+ $valueKey = "${key}_value";
+ $type = isset($data[$typeKey]) ? $data[$typeKey] : null;
+
+ switch ($type) {
+ case 'text':
+ $column = Html::tag('p', $data[$valueKey]);
+ break;
+ case 'image':
+ $column = Html::tag('img', ['height' => 13, 'src' => Template::getDataUrl($data[$valueKey])]);
+ break;
+ case 'variable':
+ $column = $this->resolveVariable($data[$valueKey]);
+ break;
+ default:
+ $column = Html::tag('div');
+ break;
+ }
+
+ return $column;
+ }
+
+ protected function assemble()
+ {
+ for ($i = 1; $i <= 3; ++$i) {
+ $this->add($this->createColumn($this->data, "{$this->type}_column{$i}"));
+ }
+ }
+}
diff --git a/library/Reporting/Web/Widget/Template.php b/library/Reporting/Web/Widget/Template.php
new file mode 100644
index 0000000..e780a3d
--- /dev/null
+++ b/library/Reporting/Web/Widget/Template.php
@@ -0,0 +1,183 @@
+<?php
+// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\Reporting\Web\Widget;
+
+use Icinga\Module\Reporting\Common\Macros;
+use Icinga\Module\Reporting\Database;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\Html;
+use ipl\Sql\Select;
+
+class Template extends BaseHtmlElement
+{
+ use Database;
+ use Macros;
+
+ protected $tag = 'div';
+
+ protected $defaultAttributes = ['class' => 'template'];
+
+ /** @var CoverPage */
+ protected $coverPage;
+
+ /** @var HeaderOrFooter */
+ protected $header;
+
+ /** @var HeaderOrFooter */
+ protected $footer;
+
+ protected $preview;
+
+ public static function getDataUrl(array $image = null)
+ {
+ if (empty($image)) {
+ return 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
+ }
+
+ return sprintf('data:%s;base64,%s', $image['mime_type'], $image['content']);
+ }
+
+ public static function fromDb($id)
+ {
+ $template = new static();
+
+ $select = (new Select())
+ ->from('template')
+ ->columns('*')
+ ->where(['id = ?' => $id]);
+
+ $row = $template->getDb()->select($select)->fetch();
+
+ if ($row === false) {
+ return null;
+ }
+
+ $row->settings = json_decode($row->settings, true);
+
+ $coverPage = (new CoverPage())
+ ->setColor($row->settings['color'])
+ ->setTitle($row->settings['title']);
+
+ if (isset($row->settings['cover_page_background_image'])) {
+ $coverPage->setBackgroundImage($row->settings['cover_page_background_image']);
+ }
+
+ if (isset($row->settings['cover_page_logo'])) {
+ $coverPage->setLogo($row->settings['cover_page_logo']);
+ }
+
+ $template
+ ->setCoverPage($coverPage)
+ ->setHeader(new HeaderOrFooter(HeaderOrFooter::HEADER, $row->settings))
+ ->setFooter(new HeaderOrFooter(HeaderOrFooter::FOOTER, $row->settings));
+
+ return $template;
+ }
+
+ /**
+ * @return CoverPage
+ */
+ public function getCoverPage()
+ {
+ return $this->coverPage;
+ }
+
+ /**
+ * @param CoverPage $coverPage
+ *
+ * @return $this
+ */
+ public function setCoverPage(CoverPage $coverPage)
+ {
+ $this->coverPage = $coverPage;
+
+ return $this;
+ }
+
+ /**
+ * @return HeaderOrFooter
+ */
+ public function getHeader()
+ {
+ return $this->header;
+ }
+
+ /**
+ * @param HeaderOrFooter $header
+ *
+ * @return $this
+ */
+ public function setHeader($header)
+ {
+ $this->header = $header;
+
+ return $this;
+ }
+
+ /**
+ * @return HeaderOrFooter
+ */
+ public function getFooter()
+ {
+ return $this->footer;
+ }
+
+ /**
+ * @param HeaderOrFooter $footer
+ *
+ * @return $this
+ */
+ public function setFooter($footer)
+ {
+ $this->footer = $footer;
+
+ return $this;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getPreview()
+ {
+ return $this->preview;
+ }
+
+ /**
+ * @param mixed $preview
+ *
+ * @return $this
+ */
+ public function setPreview($preview)
+ {
+ $this->preview = $preview;
+
+ return $this;
+ }
+
+ protected function assemble()
+ {
+ if ($this->preview) {
+ $this->getAttributes()->add('class', 'preview');
+ }
+
+ $this->add($this->getCoverPage()->setMacros($this->macros));
+
+// $page = Html::tag(
+// 'div',
+// ['class' => 'main'],
+// Html::tag('div', ['class' => 'page-content'], [
+// $this->header->setMacros($this->macros),
+// Html::tag(
+// 'div',
+// [
+// 'class' => 'main'
+// ]
+// ),
+// $this->footer->setMacros($this->macros)
+// ])
+// );
+//
+// $this->add($page);
+ }
+}