summaryrefslogtreecommitdiffstats
path: root/library/Reporting/Web
diff options
context:
space:
mode:
Diffstat (limited to 'library/Reporting/Web')
-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
14 files changed, 1477 insertions, 0 deletions
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);
+ }
+}