From c22a2c3ebc334fd7a891370e43a841d914893d47 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 14 Apr 2024 15:29:17 +0200 Subject: Merging upstream version 1.0.1. Signed-off-by: Daniel Baumann --- library/Reporting/Actions/SendMail.php | 40 +++- library/Reporting/Cli/Command.php | 4 +- library/Reporting/Database.php | 103 ++++++--- library/Reporting/Dimensions.php | 1 + library/Reporting/Hook/ActionHook.php | 8 +- library/Reporting/Hook/ReportHook.php | 13 +- library/Reporting/Mail.php | 19 +- library/Reporting/Model/Config.php | 47 ++++ library/Reporting/Model/Report.php | 71 ++++++ library/Reporting/Model/Reportlet.php | 48 ++++ library/Reporting/Model/Schedule.php | 48 ++++ library/Reporting/Model/Schema.php | 49 ++++ library/Reporting/Model/Template.php | 53 +++++ library/Reporting/Model/Timeframe.php | 53 +++++ library/Reporting/ProvidedActions.php | 1 + library/Reporting/ProvidedHook/DbMigration.php | 79 +++++++ library/Reporting/ProvidedReports.php | 1 + library/Reporting/Report.php | 249 ++++++++------------- library/Reporting/ReportData.php | 1 + library/Reporting/ReportRow.php | 1 + library/Reporting/Reportlet.php | 62 ++--- library/Reporting/Reports/SystemReport.php | 29 ++- library/Reporting/RetryConnection.php | 1 + library/Reporting/Schedule.php | 177 +++++++-------- library/Reporting/Scheduler.php | 176 --------------- library/Reporting/Str.php | 3 + library/Reporting/Timeframe.php | 97 +------- library/Reporting/Timerange.php | 1 + library/Reporting/Values.php | 1 + library/Reporting/Web/Controller.php | 11 +- library/Reporting/Web/Flatpickr.php | 77 ------- library/Reporting/Web/Forms/DecoratedElement.php | 17 -- .../Web/Forms/Decorator/CompatDecorator.php | 63 ------ library/Reporting/Web/Forms/ReportForm.php | 179 +++++++++++---- library/Reporting/Web/Forms/ScheduleForm.php | 195 ++++++++-------- library/Reporting/Web/Forms/SendForm.php | 8 +- library/Reporting/Web/Forms/TemplateForm.php | 190 +++++++++------- library/Reporting/Web/Forms/TimeframeForm.php | 195 ++++++++++++---- .../Web/ReportsTimeframesAndTemplatesTabs.php | 22 +- library/Reporting/Web/Widget/CompatDropdown.php | 1 + library/Reporting/Web/Widget/CoverPage.php | 18 +- library/Reporting/Web/Widget/HeaderOrFooter.php | 10 +- library/Reporting/Web/Widget/Template.php | 44 ++-- 43 files changed, 1373 insertions(+), 1093 deletions(-) create mode 100644 library/Reporting/Model/Config.php create mode 100644 library/Reporting/Model/Report.php create mode 100644 library/Reporting/Model/Reportlet.php create mode 100644 library/Reporting/Model/Schedule.php create mode 100644 library/Reporting/Model/Schema.php create mode 100644 library/Reporting/Model/Template.php create mode 100644 library/Reporting/Model/Timeframe.php create mode 100644 library/Reporting/ProvidedHook/DbMigration.php delete mode 100644 library/Reporting/Scheduler.php delete mode 100644 library/Reporting/Web/Flatpickr.php delete mode 100644 library/Reporting/Web/Forms/DecoratedElement.php delete mode 100644 library/Reporting/Web/Forms/Decorator/CompatDecorator.php (limited to 'library/Reporting') diff --git a/library/Reporting/Actions/SendMail.php b/library/Reporting/Actions/SendMail.php index 7c70bf5..a0dc42f 100644 --- a/library/Reporting/Actions/SendMail.php +++ b/library/Reporting/Actions/SendMail.php @@ -1,4 +1,5 @@ setFrom(Config::module('reporting')->get('mail', 'from', 'reporting@icinga')); + $mail->setFrom( + Config::module('reporting', 'config', true)->get('mail', 'from', 'reporting@icinga') + ); if (isset($config['subject'])) { $mail->setSubject($config['subject']); @@ -51,9 +57,10 @@ class SendMail extends ActionHook throw new \InvalidArgumentException(); } - $recipients = array_filter(preg_split('/[\s,]+/', $config['recipients'])); + /** @var array $recipients */ + $recipients = preg_split('/[\s,]+/', $config['recipients']); - $mail->send(null, $recipients); + $mail->send(null, array_filter($recipients)); } public function initConfigForm(Form $form, Report $report) @@ -66,19 +73,34 @@ class SendMail extends ActionHook } $form->addElement('select', 'type', [ - 'required' => true, - 'label' => t('Type'), - 'options' => $types + 'required' => true, + 'label' => t('Type'), + 'options' => $types ]); $form->addElement('text', 'subject', [ - 'label' => t('Subject'), - 'placeholder' => Mail::DEFAULT_SUBJECT + 'label' => t('Subject'), + 'placeholder' => Mail::DEFAULT_SUBJECT ]); $form->addElement('textarea', 'recipients', [ 'required' => true, - 'label' => t('Recipients') + 'label' => t('Recipients'), + 'validators' => [ + new CallbackValidator(function ($value, CallbackValidator $validator): bool { + $mailValidator = new EmailAddressValidator(); + $mails = Str::trimSplit($value); + foreach ($mails as $mail) { + if (! $mailValidator->isValid($mail)) { + $validator->addMessage(...$mailValidator->getMessages()); + + return false; + } + } + + return true; + }) + ] ]); } } diff --git a/library/Reporting/Cli/Command.php b/library/Reporting/Cli/Command.php index a89f77b..16ce8fa 100644 --- a/library/Reporting/Cli/Command.php +++ b/library/Reporting/Cli/Command.php @@ -1,16 +1,14 @@ 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'"; + /** + * Get the database connection + * + * @return RetryConnection + */ + public static function get(): RetryConnection + { + if (self::$instance === null) { + self::$instance = self::getDb(); } - $conn = new RetryConnection($config); - - return $conn; + return self::$instance; } - protected function listTimeframes() + private static function getDb(): RetryConnection { - $select = (new Sql\Select()) - ->from('timeframe') - ->columns(['id', 'name']); - - $timeframes = []; + $config = new Sql\Config( + ResourceFactory::getResourceConfig( + Config::module('reporting')->get('backend', 'resource', 'reporting') + ) + ); - foreach ($this->getDb()->select($select) as $row) { - $timeframes[$row->id] = $row->name; + $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'"; } - return $timeframes; + return new RetryConnection($config); } - protected function listTemplates() + /** + * List all reporting timeframes + * + * @return array + */ + public static function listTimeframes(): array { - $select = (new Sql\Select()) - ->from('template') - ->columns(['id', 'name']); + return self::list( + (new Sql\Select()) + ->from('timeframe') + ->columns(['id', 'name']) + ); + } - $templates = []; + /** + * List all reporting templates + * + * @return array + */ + public static function listTemplates(): array + { + return self::list( + (new Sql\Select()) + ->from('template') + ->columns(['id', 'name']) + ); + } + + /** + * Helper method for list templates and timeframes + * + * @param Sql\Select $select + * + * @return array + */ + private static function list(Sql\Select $select): array + { + $result = []; + /** @var stdClass $row */ + foreach (self::get()->select($select) as $row) { + /** @var int $id */ + $id = $row->id; + /** @var string $name */ + $name = $row->name; - foreach ($this->getDb()->select($select) as $row) { - $templates[$row->id] = $row->name; + $result[$id] = $name; } - return $templates; + return $result; } } diff --git a/library/Reporting/Dimensions.php b/library/Reporting/Dimensions.php index dfedbc8..09b23c9 100644 --- a/library/Reporting/Dimensions.php +++ b/library/Reporting/Dimensions.php @@ -1,4 +1,5 @@ from = 'icinga-reporting@' . $_SERVER[$key]; return $this->from; @@ -58,7 +59,7 @@ class Mail /** * Set the from part * - * @param string $from + * @param string $from * * @return $this */ @@ -82,7 +83,7 @@ class Mail /** * Set the subject * - * @param string $subject + * @param string $subject * * @return $this */ @@ -161,14 +162,14 @@ class Mail { $mail = new Zend_Mail('UTF-8'); - $mail->setFrom($this->getFrom()); + $mail->setFrom($this->getFrom(), ''); $mail->addTo($recipient); $mail->setSubject($this->getSubject()); - if (strlen($body) !== strlen(strip_tags($body))) { + if ($body && (strlen($body) !== strlen(strip_tags($body)))) { $mail->setBodyHtml($body); } else { - $mail->setBodyText($body); + $mail->setBodyText($body ?? ''); } foreach ($this->attachments as $attachment) { diff --git a/library/Reporting/Model/Config.php b/library/Reporting/Model/Config.php new file mode 100644 index 0000000..791a76d --- /dev/null +++ b/library/Reporting/Model/Config.php @@ -0,0 +1,47 @@ +add(new MillisecondTimestamp([ + 'ctime', + 'mtime' + ])); + } + + public function createRelations(Relations $relations) + { + $relations->belongsTo('reportlet', Reportlet::class); + } +} diff --git a/library/Reporting/Model/Report.php b/library/Reporting/Model/Report.php new file mode 100644 index 0000000..b466a60 --- /dev/null +++ b/library/Reporting/Model/Report.php @@ -0,0 +1,71 @@ +add(new MillisecondTimestamp([ + 'ctime', + 'mtime' + ])); + } + + public function createRelations(Relations $relations) + { + $relations->belongsTo('timeframe', Timeframe::class); + $relations->belongsTo('template', Template::class) + ->setJoinType('LEFT'); + + $relations->hasOne('schedule', Schedule::class) + ->setJoinType('LEFT'); + $relations->hasMany('reportlets', Reportlet::class); + } +} diff --git a/library/Reporting/Model/Reportlet.php b/library/Reporting/Model/Reportlet.php new file mode 100644 index 0000000..3552cf5 --- /dev/null +++ b/library/Reporting/Model/Reportlet.php @@ -0,0 +1,48 @@ +add(new MillisecondTimestamp([ + 'ctime', + 'mtime' + ])); + } + + public function createRelations(Relations $relations) + { + $relations->belongsTo('report', Report::class); + + $relations->hasMany('config', Config::class); + } +} diff --git a/library/Reporting/Model/Schedule.php b/library/Reporting/Model/Schedule.php new file mode 100644 index 0000000..6100d2a --- /dev/null +++ b/library/Reporting/Model/Schedule.php @@ -0,0 +1,48 @@ +add(new MillisecondTimestamp([ + 'ctime', + 'mtime' + ])); + } + + public function createRelations(Relations $relations) + { + $relations->belongsTo('report', Report::class); + } +} diff --git a/library/Reporting/Model/Schema.php b/library/Reporting/Model/Schema.php new file mode 100644 index 0000000..102a6eb --- /dev/null +++ b/library/Reporting/Model/Schema.php @@ -0,0 +1,49 @@ +add(new BoolCast(['success'])); + $behaviors->add(new MillisecondTimestamp(['timestamp'])); + } +} diff --git a/library/Reporting/Model/Template.php b/library/Reporting/Model/Template.php new file mode 100644 index 0000000..6695f37 --- /dev/null +++ b/library/Reporting/Model/Template.php @@ -0,0 +1,53 @@ +add(new MillisecondTimestamp([ + 'ctime', + 'mtime' + ])); + } + + public function createRelations(Relations $relations) + { + $relations->hasMany('report', Report::class) + ->setJoinType('LEFT'); + } +} diff --git a/library/Reporting/Model/Timeframe.php b/library/Reporting/Model/Timeframe.php new file mode 100644 index 0000000..9936a58 --- /dev/null +++ b/library/Reporting/Model/Timeframe.php @@ -0,0 +1,53 @@ +add(new MillisecondTimestamp([ + 'ctime', + 'mtime' + ])); + } + + public function createRelations(Relations $relations) + { + $relations->hasMany('report', Report::class); + } +} diff --git a/library/Reporting/ProvidedActions.php b/library/Reporting/ProvidedActions.php index 2590d1f..b3187c7 100644 --- a/library/Reporting/ProvidedActions.php +++ b/library/Reporting/ProvidedActions.php @@ -1,4 +1,5 @@ translate('Icinga Reporting'); + } + + public function providedDescriptions(): array + { + return [ + '0.9.1' => $this->translate( + 'Modifies all columns that uses current_timestamp to unix_timestamp and alters the database' + . ' engine of some tables.' + ), + '0.10.0' => $this->translate('Creates the template table and adjusts some column types'), + '1.0.0' => $this->translate('Migrates all your configured report schedules to the new config.') + ]; + } + + protected function getSchemaQuery(): Query + { + return Schema::on($this->getDb()); + } + + public function getDb(): Connection + { + return Database::get(); + } + + public function getVersion(): string + { + if ($this->version === null) { + $conn = $this->getDb(); + $schema = $this->getSchemaQuery() + ->columns(['version', 'success']) + ->orderBy('id', SORT_DESC) + ->limit(2); + + if (static::tableExists($conn, $schema->getModel()->getTableName())) { + /** @var Schema $version */ + foreach ($schema as $version) { + if ($version->success) { + $this->version = $version->version; + } + } + + if (! $this->version) { + // Schema version table exist, but the user has probably deleted the entry! + $this->version = '1.0.0'; + } + } elseif (static::tableExists($conn, 'template')) { + // We have added Postgres support and the template table with 0.10.0. + // So, use this as the last (migrated) version. + $this->version = '0.10.0'; + } elseif (static::getColumnType($conn, 'timeframe', 'name') === 'varchar(128)') { + // Upgrade script 0.9.1 alters the timeframe.name column from `varchar(255)` -> `varchar(128)`. + // Therefore, we can safely use this as the last migrated version. + $this->version = '0.9.1'; + } else { + // Use the initial version as the last migrated schema version! + $this->version = '0.9.0'; + } + } + + return $this->version; + } +} diff --git a/library/Reporting/ProvidedReports.php b/library/Reporting/ProvidedReports.php index edfc2ce..b672478 100644 --- a/library/Reporting/ProvidedReports.php +++ b/library/Reporting/ProvidedReports.php @@ -1,4 +1,5 @@ getDb(); + $report->id = $reportModel->id; + $report->name = $reportModel->name; + $report->author = $reportModel->author; + $report->timeframe = Timeframe::fromModel($reportModel->timeframe); - $select = (new Sql\Select()) - ->from('report') - ->columns('*') - ->where(['id = ?' => $id]); - - $row = $db->select($select)->fetch(); - - if ($row === false) { - throw new Exception('Report not found'); + $template = $reportModel->template->first(); + if ($template !== null) { + $report->template = Template::fromModel($template); } - $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.'); + $reportlets = []; + foreach ($reportModel->reportlets as $reportlet) { + $reportlet->report_name = $reportModel->name; + $reportlet->report_id = $reportModel->id; + $reportlets[] = Reportlet::fromModel($reportlet); } - $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; + if (empty($reportlets)) { + throw new Exception('No reportlets configured'); } - $reportlet->setConfig($config); - - $report->setReportlets([$reportlet]); - - $select = (new Sql\Select()) - ->from('schedule') - ->columns('*') - ->where(['report_id = ?' => $id]); - - $row = $db->select($select)->fetch(); + $report->reportlets = $reportlets; - 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); + $schedule = $reportModel->schedule->first(); + if ($schedule !== null) { + $report->schedule = Schedule::fromModel($schedule, $report); } return $report; @@ -131,18 +89,6 @@ class Report return $this->id; } - /** - * @param int $id - * - * @return $this - */ - public function setId($id) - { - $this->id = $id; - - return $this; - } - /** * @return string */ @@ -151,18 +97,6 @@ class Report return $this->name; } - /** - * @param string $name - * - * @return $this - */ - public function setName($name) - { - $this->name = $name; - - return $this; - } - /** * @return string */ @@ -171,18 +105,6 @@ class Report return $this->author; } - /** - * @param string $author - * - * @return $this - */ - public function setAuthor($author) - { - $this->author = $author; - - return $this; - } - /** * @return Timeframe */ @@ -191,18 +113,6 @@ class Report return $this->timeframe; } - /** - * @param Timeframe $timeframe - * - * @return $this - */ - public function setTimeframe(Timeframe $timeframe) - { - $this->timeframe = $timeframe; - - return $this; - } - /** * @return Reportlet[] */ @@ -211,18 +121,6 @@ class Report return $this->reportlets; } - /** - * @param Reportlet[] $reportlets - * - * @return $this - */ - public function setReportlets(array $reportlets) - { - $this->reportlets = $reportlets; - - return $this; - } - /** * @return Schedule */ @@ -231,18 +129,6 @@ class Report return $this->schedule; } - /** - * @param Schedule $schedule - * - * @return $this - */ - public function setSchedule(Schedule $schedule) - { - $this->schedule = $schedule; - - return $this; - } - /** * @return Template */ @@ -251,18 +137,6 @@ class Report 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) { @@ -300,6 +174,7 @@ class Report public function toCsv() { $timerange = $this->getTimeframe()->getTimerange(); + $convertFloats = version_compare(PHP_VERSION, '8.0.0', '<'); $csv = []; @@ -309,8 +184,41 @@ class Report if ($implementation->providesData()) { $data = $implementation->getData($timerange, $reportlet->getConfig()); $csv[] = array_merge($data->getDimensions(), $data->getValues()); + + $hosts = []; + $isServiceExport = false; + $config = $reportlet->getConfig(); + $exportTotalEnabled = isset($config['export_total']) && $config['export_total']; + if ($exportTotalEnabled) { + $isServiceExport = $reportlet->getClass() === ServiceSlaReport::class; + } + foreach ($data->getRows() as $row) { - $csv[] = array_merge($row->getDimensions(), $row->getValues()); + $values = $row->getValues(); + if ($convertFloats) { + foreach ($values as &$value) { + if (is_float($value)) { + $value = sprintf('%.4F', $value); + } + } + } + + if ($isServiceExport) { + $hosts[$row->getDimensions()[0]] = true; + } + + $csv[] = array_merge($row->getDimensions(), $values); + } + + if ($exportTotalEnabled) { + $precision = $config['sla_precision'] ?? SlaReport::DEFAULT_REPORT_PRECISION; + $total = [$isServiceExport ? count($hosts) : $data->count()]; + if ($isServiceExport) { + $total[] = $data->count(); + } + $total[] = round($data->getAverages()[0], $precision); + + $csv[] = $total; } break; @@ -336,9 +244,34 @@ class Report $data = $implementation->getData($timerange, $reportlet->getConfig()); $dimensions = $data->getDimensions(); $values = $data->getValues(); + + $hosts = []; + $isServiceExport = false; + $config = $reportlet->getConfig(); + $exportTotalEnabled = isset($config['export_total']) && $config['export_total']; + if ($exportTotalEnabled) { + $isServiceExport = $reportlet->getClass() === ServiceSlaReport::class; + } + foreach ($data->getRows() as $row) { - $json[] = \array_combine($dimensions, $row->getDimensions()) - + \array_combine($values, $row->getValues()); + $json[] = array_combine($dimensions, $row->getDimensions()) + + array_combine($values, $row->getValues()); + + if ($isServiceExport) { + $hosts[$row->getDimensions()[0]] = true; + } + } + + if ($exportTotalEnabled) { + $total = [t('Total Hosts') => $isServiceExport ? count($hosts) : $data->count()]; + if ($isServiceExport) { + $total[t('Total Services')] = $data->count(); + } + + $precision = $config['sla_precision'] ?? SlaReport::DEFAULT_REPORT_PRECISION; + $total[t('Total SLA Averages')] = round($data->getAverages()[0], $precision); + + $json[] = $total; } break; diff --git a/library/Reporting/ReportData.php b/library/Reporting/ReportData.php index 787f4db..addd6db 100644 --- a/library/Reporting/ReportData.php +++ b/library/Reporting/ReportData.php @@ -1,4 +1,5 @@ id; - } - - /** - * @param int $id + * Create reportlet from the given model * - * @return $this + * @param Model\Reportlet $reportletModel + * + * @return static */ - public function setId($id) + public static function fromModel(Model\Reportlet $reportletModel): self { - $this->id = $id; + $reportlet = new static(); + $reportlet->class = $reportletModel->class; + + $reportletConfig = [ + 'name' => $reportletModel->report_name, + 'id' => $reportletModel->report_id + ]; + + foreach ($reportletModel->config as $config) { + $reportletConfig[$config->name] = $config->value; + } - return $this; + $reportlet->config = $reportletConfig; + + return $reportlet; } /** @@ -42,18 +46,6 @@ class Reportlet return $this->class; } - /** - * @param string $class - * - * @return $this - */ - public function setClass($class) - { - $this->class = $class; - - return $this; - } - /** * @return array */ @@ -62,18 +54,6 @@ class Reportlet return $this->config; } - /** - * @param array $config - * - * @return $this - */ - public function setConfig($config) - { - $this->config = $config; - - return $this; - } - /** * @return \Icinga\Module\Reporting\Hook\ReportHook */ @@ -81,6 +61,6 @@ class Reportlet { $class = $this->getClass(); - return new $class; + return new $class(); } } diff --git a/library/Reporting/Reports/SystemReport.php b/library/Reporting/Reports/SystemReport.php index 8a3d8dd..5c9a544 100644 --- a/library/Reporting/Reports/SystemReport.php +++ b/library/Reporting/Reports/SystemReport.php @@ -1,8 +1,10 @@ loadHTML($html); + if (! Icinga::app()->isCli()) { + $doc = new \DOMDocument(); + @$doc->loadHTML($html); + + $style = $doc->getElementsByTagName('style')->item(0); + $style->parentNode->removeChild($style); - $style = $doc->getElementsByTagName('style')->item(0); - $style->parentNode->removeChild($style); + $title = $doc->getElementsByTagName('title')->item(0); + $title->parentNode->removeChild($title); - $title = $doc->getElementsByTagName('title')->item(0); - $title->parentNode->removeChild($title); + $meta = $doc->getElementsByTagName('meta')->item(0); + $meta->parentNode->removeChild($meta); - $meta = $doc->getElementsByTagName('meta')->item(0); - $meta->parentNode->removeChild($meta); + $doc->getElementsByTagName('div')->item(0)->setAttribute('class', 'system-report'); - $doc->getElementsByTagName('div')->item(0)->setAttribute('class', 'system-report'); + $html = $doc->saveHTML(); + } else { + $html = nl2br($html); + } - return new HtmlString($doc->saveHTML()); + return new HtmlString($html); } } diff --git a/library/Reporting/RetryConnection.php b/library/Reporting/RetryConnection.php index ebadfd2..5f7e125 100644 --- a/library/Reporting/RetryConnection.php +++ b/library/Reporting/RetryConnection.php @@ -1,4 +1,5 @@ id; + $this->action = $action; + $this->config = $config; + ksort($this->config); + + $this + ->setName($name) + ->setReport($report) + ->setUuid(Uuid::fromBytes($this->getChecksum())); } /** - * @param int $id + * Create schedule from the given model + * + * @param Model\Schedule $scheduleModel * - * @return $this + * @return static */ - public function setId($id) + + public static function fromModel(Model\Schedule $scheduleModel, Report $report): self { - $this->id = $id; + $config = Json::decode($scheduleModel->config ?? [], true); + $schedule = new static("Schedule{$scheduleModel->id}", $scheduleModel->action, $config, $report); + $schedule->id = $scheduleModel->id; - return $this; + return $schedule; } /** + * Get the id of this schedule + * * @return int */ - public function getReportId() + public function getId(): int { - return $this->reportId; + return $this->id; } /** - * @param int $id + * Get the action hook class of this schedule * - * @return $this - */ - public function setReportId($id) - { - $this->reportId = $id; - - return $this; - } - - /** - * @return \DateTime + * @return string */ - public function getStart() + public function getAction(): string { - return $this->start; + return $this->action; } /** - * @param \DateTime $start + * Get the config of this schedule * - * @return $this + * @return array */ - public function setStart(\DateTime $start) + public function getConfig(): array { - $this->start = $start; - - return $this; + return $this->config; } /** - * @return string + * Get the report this schedule belongs to + * + * @return Report */ - public function getFrequency() + public function getReport(): Report { - return $this->frequency; + return $this->report; } /** - * @param string $frequency + * Set the report this schedule belongs to * - * @return $this + * @param Report $report + * + * @return $this */ - public function setFrequency($frequency) + public function setReport(Report $report): self { - $this->frequency = $frequency; + $this->report = $report; return $this; } /** + * Get the checksum of this schedule + * * @return string */ - public function getAction() + public function getChecksum(): string { - return $this->action; + return md5( + $this->getName() . $this->getReport()->getName() . $this->getAction() . Json::encode($this->getConfig()), + true + ); } - /** - * @param string $action - * - * @return $this - */ - public function setAction($action) + public function run(): ExtendedPromiseInterface { - $this->action = $action; + $deferred = new Promise\Deferred(); + Loop::futureTick(function () use ($deferred) { + $action = $this->getAction(); + /** @var ActionHook $actionHook */ + $actionHook = new $action(); - return $this; - } + try { + $actionHook->execute($this->getReport(), $this->getConfig()); + } catch (Exception $err) { + $deferred->reject($err); - /** - * @return array - */ - public function getConfig() - { - return $this->config; - } + return; + } - /** - * @param array $config - * - * @return $this - */ - public function setConfig(array $config) - { - $this->config = $config; + $deferred->resolve(); + }); - return $this; - } + /** @var ExtendedPromiseInterface $promise */ + $promise = $deferred->promise(); - /** - * @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()) - ); + return $promise; } } diff --git a/library/Reporting/Scheduler.php b/library/Reporting/Scheduler.php deleted file mode 100644 index 1b8d9f6..0000000 --- a/library/Reporting/Scheduler.php +++ /dev/null @@ -1,176 +0,0 @@ -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 index d4c7355..77eb65e 100644 --- a/library/Reporting/Str.php +++ b/library/Reporting/Str.php @@ -1,4 +1,5 @@ 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); + $timeframe->id = $timeframeModel->id; + $timeframe->name = $timeframeModel->name; + $timeframe->title = $timeframeModel->title; + $timeframe->start = $timeframeModel->start; + $timeframe->end = $timeframeModel->end; return $timeframe; } @@ -66,18 +51,6 @@ class Timeframe return $this->id; } - /** - * @param int $id - * - * @return $this - */ - public function setId($id) - { - $this->id = $id; - - return $this; - } - /** * @return string */ @@ -86,18 +59,6 @@ class Timeframe return $this->name; } - /** - * @param string $name - * - * @return $this - */ - public function setName($name) - { - $this->name = $name; - - return $this; - } - /** * @return string */ @@ -106,18 +67,6 @@ class Timeframe return $this->title; } - /** - * @param string $title - * - * @return $this - */ - public function setTitle($title) - { - $this->title = $title; - - return $this; - } - /** * @return string */ @@ -126,18 +75,6 @@ class Timeframe return $this->start; } - /** - * @param string $start - * - * @return $this - */ - public function setStart($start) - { - $this->start = $start; - - return $this; - } - /** * @return string */ @@ -146,18 +83,6 @@ class Timeframe 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()); diff --git a/library/Reporting/Timerange.php b/library/Reporting/Timerange.php index 086bfb8..0ca0a2d 100644 --- a/library/Reporting/Timerange.php +++ b/library/Reporting/Timerange.php @@ -1,4 +1,5 @@ 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 deleted file mode 100644 index 5f6605d..0000000 --- a/library/Reporting/Web/Flatpickr.php +++ /dev/null @@ -1,77 +0,0 @@ -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 deleted file mode 100644 index 2578681..0000000 --- a/library/Reporting/Web/Forms/DecoratedElement.php +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index b2eb536..0000000 --- a/library/Reporting/Web/Forms/Decorator/CompatDecorator.php +++ /dev/null @@ -1,63 +0,0 @@ -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 index 6b1e692..40e376e 100644 --- a/library/Reporting/Web/Forms/ReportForm.php +++ b/library/Reporting/Web/Forms/ReportForm.php @@ -1,4 +1,5 @@ id = $id; + $form = new static(); + $form->id = $id; + + return $form; + } + + public function getId(): ?int + { + return $this->id; + } + + /** + * Set the label of the submit button + * + * @param string $label + * + * @return $this + */ + public function setSubmitButtonLabel(string $label): self + { + $this->submitButtonLabel = $label; return $this; } - protected function assemble() + /** + * Get the label of the submit button + * + * @return string + */ + public function getSubmitButtonLabel(): string + { + if ($this->submitButtonLabel !== null) { + return $this->submitButtonLabel; + } + + return $this->id === null ? $this->translate('Create Report') : $this->translate('Update Report'); + } + + /** + * Set whether the create and show submit button should be rendered + * + * @param bool $renderCreateAndShowButton + * + * @return $this + */ + public function setRenderCreateAndShowButton(bool $renderCreateAndShowButton): self { - $this->setDefaultElementDecorator(new CompatDecorator()); + $this->renderCreateAndShowButton = $renderCreateAndShowButton; + return $this; + } + + public function hasBeenSubmitted(): bool + { + return $this->hasBeenSent() && ( + $this->getPopulatedValue('submit') + || $this->getPopulatedValue('create_show') + || $this->getPopulatedValue('remove') + ); + } + + protected function assemble() + { $this->addElement('text', 'name', [ - 'required' => true, - 'label' => 'Name' + 'required' => true, + 'label' => $this->translate('Name'), + 'description' => $this->translate( + 'A unique name of this report. It is used when exporting to pdf, json or csv format' + . ' and also when listing the reports in the cli' + ), + 'validators' => [ + 'Callback' => function ($value, CallbackValidator $validator) { + if ($value !== null && strpos($value, '..') !== false) { + $validator->addMessage( + $this->translate('Double dots are not allowed in the report name') + ); + + return false; + } + + return true; + } + ] ]); $this->addElement('select', 'timeframe', [ - 'required' => true, - 'label' => 'Timeframe', - 'options' => [null => 'Please choose'] + $this->listTimeframes(), - 'class' => 'autosubmit' + 'required' => true, + 'class' => 'autosubmit', + 'label' => $this->translate('Timeframe'), + 'options' => [null => $this->translate('Please choose')] + Database::listTimeframes(), + 'description' => $this->translate( + 'Specifies the time frame in which this report is to be generated' + ) ]); $this->addElement('select', 'template', [ - 'label' => 'Template', - 'options' => [null => 'Please choose'] + $this->listTemplates() + 'label' => $this->translate('Template'), + 'options' => [null => $this->translate('Please choose')] + Database::listTemplates(), + 'description' => $this->translate( + 'Specifies the template to use when exporting this report to pdf. (Default Icinga template)' + ) ]); $this->addElement('select', 'reportlet', [ - 'required' => true, - 'label' => 'Report', - 'options' => [null => 'Please choose'] + $this->listReports(), - 'class' => 'autosubmit' + 'required' => true, + 'class' => 'autosubmit', + 'label' => $this->translate('Report'), + 'options' => [null => $this->translate('Please choose')] + $this->listReports(), + 'description' => $this->translate('Specifies the type of the reportlet to be generated') ]); $values = $this->getValues(); @@ -63,7 +153,7 @@ class ReportForm extends CompatForm // $config->populate($this->getValues()); /** @var \Icinga\Module\Reporting\Hook\ReportHook $reportlet */ - $reportlet = new $values['reportlet']; + $reportlet = new $values['reportlet'](); $reportlet->initConfigForm($config); @@ -73,40 +163,43 @@ class ReportForm extends CompatForm } $this->addElement('submit', 'submit', [ - 'label' => $this->id === null ? 'Create Report' : 'Update Report' + 'label' => $this->getSubmitButtonLabel() ]); if ($this->id !== null) { /** @var FormSubmitElement $removeButton */ $removeButton = $this->createElement('submit', 'remove', [ - 'label' => 'Remove Report', + 'label' => $this->translate('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; + /** @var HtmlDocument $wrapper */ + $wrapper = $this->getElement('submit')->getWrapper(); + $wrapper->prepend($removeButton); + } elseif ($this->renderCreateAndShowButton) { + $createAndShow = $this->createElement('submit', 'create_show', [ + 'label' => $this->translate('Create and Show'), + ]); + $this->registerElement($createAndShow); - return; - } + /** @var HtmlDocument $wrapper */ + $wrapper = $this->getElement('submit')->getWrapper(); + $wrapper->prepend($createAndShow); } } public function onSuccess() { - if ($this->callOnSuccess === false) { + $db = Database::get(); + + if ($this->getPopulatedValue('remove')) { + $db->delete('report', ['id = ?' => $this->id]); + return; } - $db = $this->getDb(); - $values = $this->getValues(); $now = time() * 1000; @@ -155,14 +248,16 @@ class ReportForm extends CompatForm foreach ($values as $name => $value) { $db->insert('config', [ - 'reportlet_id' => $reportletId, - 'name' => $name, - 'value' => $value, - 'ctime' => $now, - 'mtime' => $now + 'reportlet_id' => $reportletId, + 'name' => $name, + 'value' => $value, + 'ctime' => $now, + 'mtime' => $now ]); } $db->commitTransaction(); + + $this->id = $reportId; } } diff --git a/library/Reporting/Web/Forms/ScheduleForm.php b/library/Reporting/Web/Forms/ScheduleForm.php index 47f3ee3..72c4767 100644 --- a/library/Reporting/Web/Forms/ScheduleForm.php +++ b/library/Reporting/Web/Forms/ScheduleForm.php @@ -1,96 +1,98 @@ report = $report; + $this->scheduleElement = new ScheduleElement('schedule_element'); + /** @var Web $app */ + $app = Icinga::app(); + $this->scheduleElement->setIdProtector([$app->getRequest(), 'protectId']); + } - $schedule = $report->getSchedule(); + public function getPartUpdates(): array + { + return $this->scheduleElement->prepareMultipartUpdate($this->getRequest()); + } + + /** + * Create a new form instance with the given report + * + * @param Report $report + * + * @return static + */ + public static function fromReport(Report $report): self + { + $form = new static(); + $form->report = $report; + $schedule = $report->getSchedule(); if ($schedule !== null) { - $this->setId($schedule->getId()); + $config = $schedule->getConfig(); + $config['action'] = $schedule->getAction(); + + /** @var Frequency $type */ + $type = $config['frequencyType']; + $config['schedule_element'] = $type::fromJson($config['frequency']); - $values = [ - 'start' => $schedule->getStart()->format('Y-m-d\\TH:i:s'), - 'frequency' => $schedule->getFrequency(), - 'action' => $schedule->getAction() - ] + $schedule->getConfig(); + unset($config['frequency']); + unset($config['frequencyType']); - $this->populate($values); + $form->populate($config); } - return $this; + return $form; } - public function setId($id) + public function hasBeenSubmitted(): bool { - $this->id = $id; - - return $this; + return $this->hasBeenSent() && ( + $this->getPopulatedValue('submit') + || $this->getPopulatedValue('remove') + || $this->getPopulatedValue('send') + ); } 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' + 'required' => true, + 'class' => 'autosubmit', + 'options' => array_merge([null => $this->translate('Please choose')], $this->listActions()), + 'label' => $this->translate('Action'), + 'description' => $this->translate('Specifies an action to be triggered by the scheduler') ]); $values = $this->getValues(); @@ -99,8 +101,8 @@ class ScheduleForm extends CompatForm $config = new Form(); // $config->populate($this->getValues()); - /** @var \Icinga\Module\Reporting\Hook\ActionHook $action */ - $action = new $values['action']; + /** @var ActionHook $action */ + $action = new $values['action'](); $action->initConfigForm($config, $this->report); @@ -109,67 +111,80 @@ class ScheduleForm extends CompatForm } } + $this->addHtml(HtmlElement::create('div', ['class' => 'schedule-element-separator'])); + $this->addElement($this->scheduleElement); + + $schedule = $this->report->getSchedule(); $this->addElement('submit', 'submit', [ - 'label' => $this->id === null ? 'Create Schedule' : 'Update Schedule' + 'label' => $schedule === null ? $this->translate('Create Schedule') : $this->translate('Update Schedule') ]); - if ($this->id !== null) { + if ($schedule !== null) { + $sendButton = $this->createElement('submit', 'send', [ + 'label' => $this->translate('Send Report Now'), + 'formnovalidate' => true + ]); + $this->registerElement($sendButton); + + /** @var HtmlDocument $wrapper */ + $wrapper = $this->getElement('submit')->getWrapper(); + $wrapper->prepend($sendButton); + /** @var FormSubmitElement $removeButton */ $removeButton = $this->createElement('submit', 'remove', [ - 'label' => 'Remove Schedule', + 'label' => $this->translate('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; - } + $wrapper->prepend($removeButton); } } public function onSuccess() { - $db = $this->getDb(); + $db = Database::get(); + $schedule = $this->report->getSchedule(); + if ($this->getPopulatedValue('remove')) { + $db->delete('schedule', ['id = ?' => $schedule->getId()]); - $values = $this->getValues(); + return; + } - $now = time() * 1000; + $values = $this->getValues(); + if ($this->getPopulatedValue('send')) { + $action = new $values['action'](); + $action->execute($this->report, $values); - if (! $values['start'] instanceof DateTime) { - $values['start'] = DateTime::createFromFormat('Y-m-d H:i:s', $values['start']); + return; } - $data = [ - 'start' => $values['start']->getTimestamp() * 1000, - 'frequency' => $values['frequency'], - 'action' => $values['action'], - 'mtime' => $now - ]; - - unset($values['start']); - unset($values['frequency']); + $action = $values['action']; unset($values['action']); + unset($values['schedule_element']); - $data['config'] = json_encode($values); + $frequency = $this->scheduleElement->getValue(); + $values['frequency'] = Json::encode($frequency); + $values['frequencyType'] = get_php_type($frequency); + $config = Json::encode($values); $db->beginTransaction(); - if ($this->id === null) { - $db->insert('schedule', $data + [ + if ($schedule === null) { + $now = (new DateTime())->getTimestamp() * 1000; + $db->insert('schedule', [ 'author' => Auth::getInstance()->getUser()->getUsername(), 'report_id' => $this->report->getId(), - 'ctime' => $now + 'ctime' => $now, + 'mtime' => $now, + 'action' => $action, + 'config' => $config ]); } else { - $db->update('schedule', $data, ['id = ?' => $this->id]); + $db->update('schedule', [ + 'action' => $action, + 'config' => $config + ], ['id = ?' => $schedule->getId()]); } $db->commitTransaction(); diff --git a/library/Reporting/Web/Forms/SendForm.php b/library/Reporting/Web/Forms/SendForm.php index 03b691c..e3cf3ec 100644 --- a/library/Reporting/Web/Forms/SendForm.php +++ b/library/Reporting/Web/Forms/SendForm.php @@ -1,18 +1,16 @@ setDefaultElementDecorator(new CompatDecorator()); - (new SendMail())->initConfigForm($this, $this->report); $this->addElement('submit', 'submit', [ - 'label' => 'Send Report' + 'label' => $this->translate('Send Report') ]); } diff --git a/library/Reporting/Web/Forms/TemplateForm.php b/library/Reporting/Web/Forms/TemplateForm.php index bb062bb..4cd44a9 100644 --- a/library/Reporting/Web/Forms/TemplateForm.php +++ b/library/Reporting/Web/Forms/TemplateForm.php @@ -1,23 +1,21 @@ template; } - public function setTemplate($template) + /** + * Create a new form instance with the given report + * + * @param $template + * + * @return static + */ + public static function fromTemplate($template): self { - $this->template = $template; + $form = new static(); + + $template->settings = Json::decode($template->settings, true); + $form->template = $template; if ($template->settings) { - $this->populate(array_filter($template->settings, function ($value) { + /** @var array $settings */ + $settings = $template->settings; + $form->populate(array_filter($settings, function ($value) { // Don't populate files return ! is_array($value); })); } - return $this; + return $form; } - protected function assemble() + public function hasBeenSubmitted(): bool { - $this->setDefaultElementDecorator(new CompatDecorator()); + return $this->hasBeenSent() && ($this->getPopulatedValue('submit') || $this->getPopulatedValue('remove')); + } + protected function assemble() + { $this->setAttribute('enctype', 'multipart/form-data'); $this->add(Html::tag('h2', 'Template Settings')); $this->addElement('text', 'name', [ - 'label' => 'Name', - 'placeholder' => 'Template name', + 'label' => $this->translate('Name'), + 'placeholder' => $this->translate('Template name'), 'required' => true ]); - $this->add(Html::tag('h2', 'Cover Page Settings')); + $this->add(Html::tag('h2', $this->translate('Cover Page Settings'))); - $this->addElement(new FileElement('cover_page_background_image', [ - 'label' => 'Background Image', - 'accept' => 'image/png, image/jpeg' - ])); + $this->addElement('file', 'cover_page_background_image', [ + 'label' => $this->translate('Background Image'), + 'accept' => ['image/png', 'image/jpeg', 'image/jpg'], + 'destination' => sys_get_temp_dir() + ]); - if ($this->template !== null - && isset($this->template->settings['cover_page_background_image']) + 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' + ['class' => 'override-uploaded-file-hint'], + $this->translate('Upload a new background image to override the existing one') )); $this->addElement('checkbox', 'remove_cover_page_background_image', [ - 'label' => 'Remove background image' + 'label' => $this->translate('Remove background image') ]); } - $this->addElement(new FileElement('cover_page_logo', [ - 'label' => 'Logo', - 'accept' => 'image/png, image/jpeg' - ])); + $this->addElement('file', 'cover_page_logo', [ + 'label' => $this->translate('Logo'), + 'accept' => ['image/png', 'image/jpeg', 'image/jpg'], + 'destination' => sys_get_temp_dir() + ]); - if ($this->template !== null + 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' + ['class' => 'override-uploaded-file-hint'], + $this->translate('Upload a new logo to override the existing one') )); $this->addElement('checkbox', 'remove_cover_page_logo', [ - 'label' => 'Remove Logo' + 'label' => $this->translate('Remove Logo') ]); } $this->addElement('textarea', 'title', [ - 'label' => 'Title', - 'placeholder' => 'Report title' + 'label' => $this->translate('Title'), + 'placeholder' => $this->translate('Report title') ]); $this->addElement('text', 'color', [ - 'label' => 'Color', - 'placeholder' => 'CSS color code' + 'label' => $this->translate('Color'), + 'placeholder' => $this->translate('CSS color code') ]); - $this->add(Html::tag('h2', 'Header Settings')); + $this->add(Html::tag('h2', $this->translate('Header Settings'))); - $this->addColumnSettings('header_column1', 'Column 1'); - $this->addColumnSettings('header_column2', 'Column 2'); - $this->addColumnSettings('header_column3', 'Column 3'); + $this->addColumnSettings('header_column1', $this->translate('Column 1')); + $this->addColumnSettings('header_column2', $this->translate('Column 2')); + $this->addColumnSettings('header_column3', $this->translate('Column 3')); - $this->add(Html::tag('h2', 'Footer Settings')); + $this->add(Html::tag('h2', $this->translate('Footer Settings'))); - $this->addColumnSettings('footer_column1', 'Column 1'); - $this->addColumnSettings('footer_column2', 'Column 2'); - $this->addColumnSettings('footer_column3', 'Column 3'); + $this->addColumnSettings('footer_column1', $this->translate('Column 1')); + $this->addColumnSettings('footer_column2', $this->translate('Column 2')); + $this->addColumnSettings('footer_column3', $this->translate('Column 3')); $this->addElement('submit', 'submit', [ - 'label' => $this->template === null ? 'Create Template' : 'Update Template' + 'label' => $this->template === null + ? $this->translate('Create Template') + : $this->translate('Update Template') ]); if ($this->template !== null) { /** @var FormSubmitElement $removeButton */ $removeButton = $this->createElement('submit', 'remove', [ - 'label' => 'Remove Template', + 'label' => $this->translate('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; - } + /** @var HtmlDocument $wrapper */ + $wrapper = $this->getElement('submit')->getWrapper(); + $wrapper->prepend($removeButton); } } public function onSuccess() { - if ($this->callOnSuccess === false) { + if ($this->getPopulatedValue('remove')) { + Database::get()->delete('template', ['id = ?' => $this->template->id]); + return; } @@ -153,20 +166,17 @@ class TemplateForm extends CompatForm $settings = $this->getValues(); try { - /** @var $uploadedFile \GuzzleHttp\Psr7\UploadedFile */ - foreach ($this->getRequest()->getUploadedFiles() as $name => $uploadedFile) { - if ($uploadedFile->getError() === UPLOAD_ERR_NO_FILE) { - continue; + foreach ($settings as $name => $setting) { + if ($setting instanceof UploadedFile) { + $settings[$name] = [ + 'mime_type' => $setting->getClientMediaType(), + 'size' => $setting->getSize(), + 'content' => base64_encode((string) $setting->getStream()) + ]; } - - $settings[$name] = [ - 'mime_type' => $uploadedFile->getClientMediaType(), - 'size' => $uploadedFile->getSize(), - 'content' => base64_encode((string) $uploadedFile->getStream()) - ]; } - $db = $this->getDb(); + $db = Database::get(); $now = time() * 1000; @@ -179,19 +189,21 @@ class TemplateForm extends CompatForm 'mtime' => $now ]); } else { - if (isset($settings['remove_cover_page_background_image'])) { + if ($this->getValue('remove_cover_page_background_image', 'n') === 'y') { unset($settings['cover_page_background_image']); unset($settings['remove_cover_page_background_image']); - } elseif (! isset($settings['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'])) { + if ($this->getValue('remove_cover_page_logo', 'n') === 'y') { unset($settings['cover_page_logo']); unset($settings['remove_cover_page_logo']); - } elseif (! isset($settings['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']; @@ -204,7 +216,8 @@ class TemplateForm extends CompatForm if ($settings[$type] === 'image') { $value = "{$headerOrFooter}_column{$i}_value"; - if (! isset($settings[$value]) + if ( + ! isset($settings[$value]) && isset($this->template->settings[$value]) ) { $settings[$value] = $this->template->settings[$value]; @@ -219,7 +232,7 @@ class TemplateForm extends CompatForm 'mtime' => $now ], ['id = ?' => $this->template->id]); } - } catch (\Exception $e) { + } catch (Exception $e) { die($e->getMessage()); } } @@ -240,20 +253,31 @@ class TemplateForm extends CompatForm ] ]); + $valueType = $this->getValue($type, 'none'); + $populated = $this->getPopulatedValue($value); + if ( + ($valueType === 'image' && ! $populated instanceof UploadedFile) + || ($valueType !== 'image' && $populated instanceof UploadedFile) + ) { + $this->clearPopulatedValue($value); + } + switch ($this->getValue($type, 'none')) { case 'image': - $this->addElement(new FileElement($value, [ - 'label' => 'Image', - 'accept' => 'image/png, image/jpeg' - ])); + $this->addElement('file', $value, [ + 'label' => 'Image', + 'accept' => ['image/png', 'image/jpeg', 'image/jpg'], + 'destination' => sys_get_temp_dir() + ]); - if ($this->template !== null + if ( + $this->template !== null && $this->template->settings[$type] === 'image' && isset($this->template->settings[$value]) ) { $this->add(Html::tag( 'p', - ['style' => ['margin-left: 14em;']], + ['class' => 'override-uploaded-file-hint'], 'Upload a new image to override the existing one' )); } @@ -270,7 +294,7 @@ class TemplateForm extends CompatForm 'page_of' => 'Page Number + Total Number of Pages', 'date' => 'Date' ], - 'value' => 'report_title' + 'value' => 'report_title' ]); break; case 'text': diff --git a/library/Reporting/Web/Forms/TimeframeForm.php b/library/Reporting/Web/Forms/TimeframeForm.php index 3d78709..37ea34f 100644 --- a/library/Reporting/Web/Forms/TimeframeForm.php +++ b/library/Reporting/Web/Forms/TimeframeForm.php @@ -1,86 +1,203 @@ id = $id; + $form = new static(); + + $form->id = $id; - return $this; + return $form; } - protected function assemble() + public function hasBeenSubmitted(): bool { - $this->setDefaultElementDecorator(new CompatDecorator()); + return $this->hasBeenSent() && ($this->getPopulatedValue('submit') || $this->getPopulatedValue('remove')); + } + protected function assemble() + { $this->addElement('text', 'name', [ - 'required' => true, - 'label' => 'Name' + 'required' => true, + 'label' => $this->translate('Name'), + 'description' => $this->translate('A unique name of this timeframe') ]); - $flatpickr = new Flatpickr(); + $default = new DateTime('00:00:00'); + $start = $this->getPopulatedValue('start', $default); + if (! $start instanceof DateTime) { + $datetime = DateTime::createFromFormat(LocalDateTimeElement::FORMAT, $start); + if ($datetime) { + $start = $datetime; + } + } - $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' + $relativeStart = $this->getPopulatedValue('relative-start', $start instanceof DateTime ? 'n' : 'y'); + $this->addElement('checkbox', 'relative-start', [ + 'required' => false, + 'class' => 'autosubmit', + 'value' => $relativeStart, + 'label' => $this->translate('Relative Start') ]); - $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' - ]); + if ($relativeStart === 'n') { + if (! $start instanceof DateTime) { + $start = $default; + $this->clearPopulatedValue('start'); + } + + $this->addElement( + new LocalDateTimeElement('start', [ + 'required' => true, + 'value' => $start, + 'label' => $this->translate('Start'), + 'description' => $this->translate('Specifies the start time of this timeframe') + ]) + ); + } else { + $this->addElement('text', 'start', [ + 'required' => true, + 'label' => $this->translate('Start'), + 'placeholder' => $this->translate('First day of this month'), + 'description' => $this->translate('Specifies the start time of this timeframe'), + 'validators' => [ + new CallbackValidator(function ($value, CallbackValidator $validator) { + if ($value !== null) { + try { + new DateTime($value); + } catch (Exception $_) { + $validator->addMessage($this->translate('Invalid textual date time')); + + return false; + } + } + + return true; + }) + ] + ]); + } + + $default = new DateTime('23:59:59'); + $end = $this->getPopulatedValue('end', $default); + if (! $end instanceof DateTime) { + $datetime = DateTime::createFromFormat(LocalDateTimeElement::FORMAT, $end); + if ($datetime) { + $end = $datetime; + } + } + + $relativeEnd = $this->getPopulatedValue('relative-end', $end instanceof DateTime ? 'n' : 'y'); + if ($relativeStart === 'y') { + $this->addElement('checkbox', 'relative-end', [ + 'required' => false, + 'class' => 'autosubmit', + 'value' => $relativeEnd, + 'label' => $this->translate('Relative End') + ]); + } + + if ($relativeEnd === 'n' || $relativeStart === 'n') { + if (! $end instanceof DateTime) { + $end = $default; + $this->clearPopulatedValue('end'); + } + + $this->addElement( + new LocalDateTimeElement('end', [ + 'required' => true, + 'value' => $end, + 'label' => $this->translate('End'), + 'description' => $this->translate('Specifies the end time of this timeframe') + ]) + ); + } else { + $this->addElement('text', 'end', [ + 'required' => true, + 'label' => $this->translate('End'), + 'placeholder' => $this->translate('Last day of this month'), + 'description' => $this->translate('Specifies the end time of this timeframe'), + 'validators' => [ + new CallbackValidator(function ($value, CallbackValidator $validator) { + if ($value !== null) { + try { + new DateTime($value); + } catch (Exception $_) { + $validator->addMessage($this->translate('Invalid textual date time')); + + return false; + } + } + + return true; + }) + ] + ]); + } $this->addElement('submit', 'submit', [ - 'label' => $this->id === null ? 'Create Time Frame' : 'Update Time Frame' + 'label' => $this->id === null + ? $this->translate('Create Time Frame') + : $this->translate('Update Time Frame') ]); if ($this->id !== null) { /** @var FormSubmitElement $removeButton */ $removeButton = $this->createElement('submit', 'remove', [ - 'label' => 'Remove Time Frame', + 'label' => $this->translate('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; - } + /** @var HtmlDocument $wrapper */ + $wrapper = $this->getElement('submit')->getWrapper(); + $wrapper->prepend($removeButton); } } public function onSuccess() { - $db = $this->getDb(); + $db = Database::get(); + + if ($this->getPopulatedValue('remove')) { + $db->delete('timeframe', ['id = ?' => $this->id]); + + return; + } $values = $this->getValues(); + if ($values['start'] instanceof DateTime) { + $values['start'] = $values['start']->format(LocalDateTimeElement::FORMAT); + } + + if ($values['end'] instanceof DateTime) { + $values['end'] = $values['end']->format(LocalDateTimeElement::FORMAT); + } $now = time() * 1000; diff --git a/library/Reporting/Web/ReportsTimeframesAndTemplatesTabs.php b/library/Reporting/Web/ReportsTimeframesAndTemplatesTabs.php index afb8b14..fee6770 100644 --- a/library/Reporting/Web/ReportsTimeframesAndTemplatesTabs.php +++ b/library/Reporting/Web/ReportsTimeframesAndTemplatesTabs.php @@ -1,4 +1,5 @@ getTabs(); + $tabs->getAttributes()->set('data-base-target', '_main'); $tabs->add('reports', [ - 'title' => $this->translate('Show reports'), - 'label' => $this->translate('Reports'), - 'url' => 'reporting/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' + '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' + '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 index cdd7b40..f5d4b03 100644 --- a/library/Reporting/Web/Widget/CompatDropdown.php +++ b/library/Reporting/Web/Widget/CompatDropdown.php @@ -1,4 +1,5 @@ hasBackgroundImage()) { - $this - ->getAttributes() - ->add('style', "background-image: url('" . Template::getDataUrl($this->getBackgroundImage()) . "');"); + $coverPageBackground = (new StyleWithNonce()) + ->setModule('reporting') + ->addFor($this, [ + 'background-image' => sprintf("url('%s')", Template::getDataUrl($this->getBackgroundImage())) + ]); + + $this->addHtml($coverPageBackground); } $content = Html::tag('div', ['class' => 'cover-page-content']); - if ($this->hasColor()) { - $content->getAttributes()->add('style', "color: {$this->getColor()};"); + $coverPageLogo = (new StyleWithNonce()) + ->setModule('reporting') + ->addFor($content, ['color' => $this->getColor()]); + + $content->addHtml($coverPageLogo); } if ($this->hasLogo()) { diff --git a/library/Reporting/Web/Widget/HeaderOrFooter.php b/library/Reporting/Web/Widget/HeaderOrFooter.php index dcb37e7..3ec9a7f 100644 --- a/library/Reporting/Web/Widget/HeaderOrFooter.php +++ b/library/Reporting/Web/Widget/HeaderOrFooter.php @@ -10,9 +10,9 @@ class HeaderOrFooter extends HtmlDocument { use Macros; - const HEADER = 'header'; + public const HEADER = 'header'; - const FOOTER = 'footer'; + public const FOOTER = 'footer'; protected $type; @@ -64,9 +64,9 @@ class HeaderOrFooter extends HtmlDocument protected function createColumn(array $data, $key) { - $typeKey = "${key}_type"; - $valueKey = "${key}_value"; - $type = isset($data[$typeKey]) ? $data[$typeKey] : null; + $typeKey = "{$key}_type"; + $valueKey = "{$key}_value"; + $type = $data[$typeKey] ?? null; switch ($type) { case 'text': diff --git a/library/Reporting/Web/Widget/Template.php b/library/Reporting/Web/Widget/Template.php index e780a3d..0f07703 100644 --- a/library/Reporting/Web/Widget/Template.php +++ b/library/Reporting/Web/Widget/Template.php @@ -1,17 +1,15 @@ from('template') - ->columns('*') - ->where(['id = ?' => $id]); - - $row = $template->getDb()->select($select)->fetch(); - - if ($row === false) { - return null; - } - - $row->settings = json_decode($row->settings, true); + $templateModel->settings = json_decode($templateModel->settings, true); $coverPage = (new CoverPage()) - ->setColor($row->settings['color']) - ->setTitle($row->settings['title']); + ->setColor($templateModel->settings['color']) + ->setTitle($templateModel->settings['title']); - if (isset($row->settings['cover_page_background_image'])) { - $coverPage->setBackgroundImage($row->settings['cover_page_background_image']); + if (isset($templateModel->settings['cover_page_background_image'])) { + $coverPage->setBackgroundImage($templateModel->settings['cover_page_background_image']); } - if (isset($row->settings['cover_page_logo'])) { - $coverPage->setLogo($row->settings['cover_page_logo']); + if (isset($templateModel->settings['cover_page_logo'])) { + $coverPage->setLogo($templateModel->settings['cover_page_logo']); } $template ->setCoverPage($coverPage) - ->setHeader(new HeaderOrFooter(HeaderOrFooter::HEADER, $row->settings)) - ->setFooter(new HeaderOrFooter(HeaderOrFooter::FOOTER, $row->settings)); + ->setHeader(new HeaderOrFooter(HeaderOrFooter::HEADER, $templateModel->settings)) + ->setFooter(new HeaderOrFooter(HeaderOrFooter::FOOTER, $templateModel->settings)); return $template; } -- cgit v1.2.3