From 978a1651bac3faf5e91ddba327bf39aeb6a4cb63 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 14 Apr 2024 15:28:59 +0200 Subject: Adding upstream version 0.10.0. Signed-off-by: Daniel Baumann --- library/Reporting/Actions/SendMail.php | 84 +++++ library/Reporting/Cli/Command.php | 23 ++ library/Reporting/Common/Macros.php | 49 +++ library/Reporting/Database.php | 58 ++++ library/Reporting/Dimensions.php | 21 ++ library/Reporting/Hook/ActionHook.php | 37 ++ library/Reporting/Hook/ReportHook.php | 116 +++++++ library/Reporting/Mail.php | 180 ++++++++++ library/Reporting/ProvidedActions.php | 20 ++ library/Reporting/ProvidedReports.php | 20 ++ library/Reporting/Report.php | 382 +++++++++++++++++++++ library/Reporting/ReportData.php | 71 ++++ library/Reporting/ReportRow.php | 10 + library/Reporting/Reportlet.php | 86 +++++ library/Reporting/Reports/SystemReport.php | 39 +++ library/Reporting/RetryConnection.php | 66 ++++ library/Reporting/Schedule.php | 160 +++++++++ library/Reporting/Scheduler.php | 176 ++++++++++ library/Reporting/Str.php | 37 ++ library/Reporting/Timeframe.php | 168 +++++++++ library/Reporting/Timerange.php | 35 ++ library/Reporting/Values.php | 21 ++ library/Reporting/Web/Controller.php | 20 ++ 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 | 168 +++++++++ library/Reporting/Web/Forms/ScheduleForm.php | 177 ++++++++++ library/Reporting/Web/Forms/SendForm.php | 47 +++ library/Reporting/Web/Forms/TemplateForm.php | 284 +++++++++++++++ library/Reporting/Web/Forms/TimeframeForm.php | 106 ++++++ .../Web/ReportsTimeframesAndTemplatesTabs.php | 37 ++ library/Reporting/Web/Widget/CompatDropdown.php | 22 ++ library/Reporting/Web/Widget/CoverPage.php | 181 ++++++++++ library/Reporting/Web/Widget/HeaderOrFooter.php | 95 +++++ library/Reporting/Web/Widget/Template.php | 183 ++++++++++ .../ipl/Html/src/FormElement/FileElement.php | 15 + 37 files changed, 3351 insertions(+) create mode 100644 library/Reporting/Actions/SendMail.php create mode 100644 library/Reporting/Cli/Command.php create mode 100644 library/Reporting/Common/Macros.php create mode 100644 library/Reporting/Database.php create mode 100644 library/Reporting/Dimensions.php create mode 100644 library/Reporting/Hook/ActionHook.php create mode 100644 library/Reporting/Hook/ReportHook.php create mode 100644 library/Reporting/Mail.php create mode 100644 library/Reporting/ProvidedActions.php create mode 100644 library/Reporting/ProvidedReports.php create mode 100644 library/Reporting/Report.php create mode 100644 library/Reporting/ReportData.php create mode 100644 library/Reporting/ReportRow.php create mode 100644 library/Reporting/Reportlet.php create mode 100644 library/Reporting/Reports/SystemReport.php create mode 100644 library/Reporting/RetryConnection.php create mode 100644 library/Reporting/Schedule.php create mode 100644 library/Reporting/Scheduler.php create mode 100644 library/Reporting/Str.php create mode 100644 library/Reporting/Timeframe.php create mode 100644 library/Reporting/Timerange.php create mode 100644 library/Reporting/Values.php create mode 100644 library/Reporting/Web/Controller.php create mode 100644 library/Reporting/Web/Flatpickr.php create mode 100644 library/Reporting/Web/Forms/DecoratedElement.php create mode 100644 library/Reporting/Web/Forms/Decorator/CompatDecorator.php create mode 100644 library/Reporting/Web/Forms/ReportForm.php create mode 100644 library/Reporting/Web/Forms/ScheduleForm.php create mode 100644 library/Reporting/Web/Forms/SendForm.php create mode 100644 library/Reporting/Web/Forms/TemplateForm.php create mode 100644 library/Reporting/Web/Forms/TimeframeForm.php create mode 100644 library/Reporting/Web/ReportsTimeframesAndTemplatesTabs.php create mode 100644 library/Reporting/Web/Widget/CompatDropdown.php create mode 100644 library/Reporting/Web/Widget/CoverPage.php create mode 100644 library/Reporting/Web/Widget/HeaderOrFooter.php create mode 100644 library/Reporting/Web/Widget/Template.php create mode 100644 library/vendor/ipl/Html/src/FormElement/FileElement.php (limited to 'library') diff --git a/library/Reporting/Actions/SendMail.php b/library/Reporting/Actions/SendMail.php new file mode 100644 index 0000000..7c70bf5 --- /dev/null +++ b/library/Reporting/Actions/SendMail.php @@ -0,0 +1,84 @@ +getName(), + $report->getTimeframe()->getName(), + date('Y-m-d H:i') + ); + + $mail = new Mail(); + + $mail->setFrom(Config::module('reporting')->get('mail', 'from', 'reporting@icinga')); + + if (isset($config['subject'])) { + $mail->setSubject($config['subject']); + } + + switch ($config['type']) { + case 'pdf': + $mail->attachPdf(Pdfexport::first()->htmlToPdf($report->toPdf()), $name); + + break; + case 'csv': + $mail->attachCsv($report->toCsv(), $name); + + break; + case 'json': + $mail->attachJson($report->toJson(), $name); + + break; + default: + throw new \InvalidArgumentException(); + } + + $recipients = array_filter(preg_split('/[\s,]+/', $config['recipients'])); + + $mail->send(null, $recipients); + } + + public function initConfigForm(Form $form, Report $report) + { + $types = ['pdf' => 'PDF']; + + if ($report->providesData()) { + $types['csv'] = 'CSV'; + $types['json'] = 'JSON'; + } + + $form->addElement('select', 'type', [ + 'required' => true, + 'label' => t('Type'), + 'options' => $types + ]); + + $form->addElement('text', 'subject', [ + 'label' => t('Subject'), + 'placeholder' => Mail::DEFAULT_SUBJECT + ]); + + $form->addElement('textarea', 'recipients', [ + 'required' => true, + 'label' => t('Recipients') + ]); + } +} diff --git a/library/Reporting/Cli/Command.php b/library/Reporting/Cli/Command.php new file mode 100644 index 0000000..a89f77b --- /dev/null +++ b/library/Reporting/Cli/Command.php @@ -0,0 +1,23 @@ +getModuleManager()->loadEnabledModules(); + } + } +} diff --git a/library/Reporting/Common/Macros.php b/library/Reporting/Common/Macros.php new file mode 100644 index 0000000..052cdd2 --- /dev/null +++ b/library/Reporting/Common/Macros.php @@ -0,0 +1,49 @@ +macros[$name] ?: null; + } + + /** + * @return mixed + */ + public function getMacros() + { + return $this->macros; + } + + /** + * @param mixed $macros + * + * @return $this + */ + public function setMacros($macros) + { + $this->macros = $macros; + + return $this; + } + + public function resolveMacros($subject) + { + $macros = []; + + foreach ((array) $this->macros as $key => $value) { + $macros['${' . $key . '}'] = $value; + } + + return str_replace(array_keys($macros), array_values($macros), $subject); + } +} diff --git a/library/Reporting/Database.php b/library/Reporting/Database.php new file mode 100644 index 0000000..3dabe17 --- /dev/null +++ b/library/Reporting/Database.php @@ -0,0 +1,58 @@ +get('backend', 'resource', 'reporting') + )); + + $config->options = [\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_OBJ]; + if ($config->db === 'mysql') { + $config->options[\PDO::MYSQL_ATTR_INIT_COMMAND] = "SET SESSION SQL_MODE='STRICT_TRANS_TABLES" + . ",NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'"; + } + + $conn = new RetryConnection($config); + + return $conn; + } + + protected function listTimeframes() + { + $select = (new Sql\Select()) + ->from('timeframe') + ->columns(['id', 'name']); + + $timeframes = []; + + foreach ($this->getDb()->select($select) as $row) { + $timeframes[$row->id] = $row->name; + } + + return $timeframes; + } + + protected function listTemplates() + { + $select = (new Sql\Select()) + ->from('template') + ->columns(['id', 'name']); + + $templates = []; + + foreach ($this->getDb()->select($select) as $row) { + $templates[$row->id] = $row->name; + } + + return $templates; + } +} diff --git a/library/Reporting/Dimensions.php b/library/Reporting/Dimensions.php new file mode 100644 index 0000000..dfedbc8 --- /dev/null +++ b/library/Reporting/Dimensions.php @@ -0,0 +1,21 @@ +dimensions; + } + + public function setDimensions(array $dimensions) + { + $this->dimensions = $dimensions; + + return $this; + } +} diff --git a/library/Reporting/Hook/ActionHook.php b/library/Reporting/Hook/ActionHook.php new file mode 100644 index 0000000..ef550ee --- /dev/null +++ b/library/Reporting/Hook/ActionHook.php @@ -0,0 +1,37 @@ +getDeclaringClass()->getName() !== self::class; + } + + /** + * Get whether the report provides HTML + * + * @return bool + */ + public function providesHtml() + { + try { + $method = new \ReflectionMethod($this, 'getHtml'); + } catch (\ReflectionException $e) { + return false; + } + + return $method->getDeclaringClass()->getName() !== self::class; + } + + /** + * Get the module name of the report + * + * @return string + */ + final public function getModuleName() + { + return ClassLoader::extractModuleName(get_class($this)); + } + + /** + * Get all provided reports + * + * @return ReportHook[] + */ + final public static function getReports() + { + return Hook::all('reporting/Report'); + } +} diff --git a/library/Reporting/Mail.php b/library/Reporting/Mail.php new file mode 100644 index 0000000..7581f45 --- /dev/null +++ b/library/Reporting/Mail.php @@ -0,0 +1,180 @@ +from)) { + return $this->from; + } + + if (isset($_SERVER['SERVER_ADMIN'])) { + $this->from = $_SERVER['SERVER_ADMIN']; + + return $this->from; + } + + foreach (['HTTP_HOST', 'SERVER_NAME', 'HOSTNAME'] as $key) { + if (isset($_SEVER[$key])) { + $this->from = 'icinga-reporting@' . $_SERVER[$key]; + + return $this->from; + } + } + + $this->from = 'icinga-reporting@localhost'; + + return $this->from; + } + + /** + * Set the from part + * + * @param string $from + * + * @return $this + */ + public function setFrom($from) + { + $this->from = $from; + + return $this; + } + + /** + * Get the subject + * + * @return string + */ + public function getSubject() + { + return $this->subject; + } + + /** + * Set the subject + * + * @param string $subject + * + * @return $this + */ + public function setSubject($subject) + { + $this->subject = $subject; + + return $this; + } + + /** + * Get the mail transport + * + * @return Zend_Mail_Transport_Sendmail + */ + public function getTransport() + { + if (! isset($this->transport)) { + $this->transport = new Zend_Mail_Transport_Sendmail('-f ' . escapeshellarg($this->getFrom())); + } + + return $this->transport; + } + + public function attachCsv($csv, $filename) + { + if (is_array($csv)) { + $csv = Str::putcsv($csv); + } + + $attachment = new Zend_Mime_Part($csv); + + $attachment->type = 'text/csv'; + $attachment->disposition = Zend_Mime::DISPOSITION_ATTACHMENT; + $attachment->encoding = Zend_Mime::ENCODING_BASE64; + $attachment->filename = basename($filename, '.csv') . '.csv'; + + $this->attachments[] = $attachment; + + return $this; + } + + public function attachJson($json, $filename) + { + if (is_array($json)) { + $json = json_encode($json); + } + + $attachment = new Zend_Mime_Part($json); + + $attachment->type = 'application/json'; + $attachment->disposition = Zend_Mime::DISPOSITION_ATTACHMENT; + $attachment->encoding = Zend_Mime::ENCODING_BASE64; + $attachment->filename = basename($filename, '.json') . '.json'; + + $this->attachments[] = $attachment; + + return $this; + } + + public function attachPdf($pdf, $filename) + { + $attachment = new Zend_Mime_Part($pdf); + + $attachment->type = 'application/pdf'; + $attachment->disposition = Zend_Mime::DISPOSITION_ATTACHMENT; + $attachment->encoding = Zend_Mime::ENCODING_BASE64; + $attachment->filename = basename($filename, '.pdf') . '.pdf'; + + $this->attachments[] = $attachment; + + return $this; + } + + public function send($body, $recipient) + { + $mail = new Zend_Mail('UTF-8'); + + $mail->setFrom($this->getFrom()); + $mail->addTo($recipient); + $mail->setSubject($this->getSubject()); + + if (strlen($body) !== strlen(strip_tags($body))) { + $mail->setBodyHtml($body); + } else { + $mail->setBodyText($body); + } + + foreach ($this->attachments as $attachment) { + $mail->addAttachment($attachment); + } + + $mail->send($this->getTransport()); + } +} diff --git a/library/Reporting/ProvidedActions.php b/library/Reporting/ProvidedActions.php new file mode 100644 index 0000000..2590d1f --- /dev/null +++ b/library/Reporting/ProvidedActions.php @@ -0,0 +1,20 @@ + $action) { + $actions[$class] = $action->getName(); + } + + return $actions; + } +} diff --git a/library/Reporting/ProvidedReports.php b/library/Reporting/ProvidedReports.php new file mode 100644 index 0000000..edfc2ce --- /dev/null +++ b/library/Reporting/ProvidedReports.php @@ -0,0 +1,20 @@ + $report) { + $reports[$class] = $report->getName(); + } + + return $reports; + } +} diff --git a/library/Reporting/Report.php b/library/Reporting/Report.php new file mode 100644 index 0000000..7f2eee3 --- /dev/null +++ b/library/Reporting/Report.php @@ -0,0 +1,382 @@ +getDb(); + + $select = (new Sql\Select()) + ->from('report') + ->columns('*') + ->where(['id = ?' => $id]); + + $row = $db->select($select)->fetch(); + + if ($row === false) { + throw new Exception('Report not found'); + } + + $report + ->setId($row->id) + ->setName($row->name) + ->setAuthor($row->author) + ->setTimeframe(Timeframe::fromDb($row->timeframe_id)) + ->setTemplate(Template::fromDb($row->template_id)); + + $select = (new Sql\Select()) + ->from('reportlet') + ->columns('*') + ->where(['report_id = ?' => $id]); + + $row = $db->select($select)->fetch(); + + if ($row === false) { + throw new Exception('No reportlets configured.'); + } + + $reportlet = new Reportlet(); + + $reportlet + ->setId($row->id) + ->setClass($row->class); + + $select = (new Sql\Select()) + ->from('config') + ->columns('*') + ->where(['reportlet_id = ?' => $row->id]); + + $rows = $db->select($select)->fetchAll(); + + $config = []; + + foreach ($rows as $row) { + $config[$row->name] = $row->value; + } + + $reportlet->setConfig($config); + + $report->setReportlets([$reportlet]); + + $select = (new Sql\Select()) + ->from('schedule') + ->columns('*') + ->where(['report_id = ?' => $id]); + + $row = $db->select($select)->fetch(); + + if ($row !== false) { + $schedule = new Schedule(); + + $schedule + ->setId($row->id) + ->setStart((new \DateTime())->setTimestamp((int) $row->start / 1000)) + ->setFrequency($row->frequency) + ->setAction($row->action) + ->setConfig(json_decode($row->config, true)); + + $report->setSchedule($schedule); + } + + return $report; + } + + /** + * @return int + */ + public function getId() + { + return $this->id; + } + + /** + * @param int $id + * + * @return $this + */ + public function setId($id) + { + $this->id = $id; + + return $this; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @param string $name + * + * @return $this + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * @return string + */ + public function getAuthor() + { + return $this->author; + } + + /** + * @param string $author + * + * @return $this + */ + public function setAuthor($author) + { + $this->author = $author; + + return $this; + } + + /** + * @return Timeframe + */ + public function getTimeframe() + { + return $this->timeframe; + } + + /** + * @param Timeframe $timeframe + * + * @return $this + */ + public function setTimeframe(Timeframe $timeframe) + { + $this->timeframe = $timeframe; + + return $this; + } + + /** + * @return Reportlet[] + */ + public function getReportlets() + { + return $this->reportlets; + } + + /** + * @param Reportlet[] $reportlets + * + * @return $this + */ + public function setReportlets(array $reportlets) + { + $this->reportlets = $reportlets; + + return $this; + } + + /** + * @return Schedule + */ + public function getSchedule() + { + return $this->schedule; + } + + /** + * @param Schedule $schedule + * + * @return $this + */ + public function setSchedule(Schedule $schedule) + { + $this->schedule = $schedule; + + return $this; + } + + /** + * @return Template + */ + public function getTemplate() + { + return $this->template; + } + + /** + * @param Template $template + * + * @return $this + */ + public function setTemplate($template) + { + $this->template = $template; + + return $this; + } + + public function providesData() + { + foreach ($this->getReportlets() as $reportlet) { + $implementation = $reportlet->getImplementation(); + + if ($implementation->providesData()) { + return true; + } + } + + return false; + } + + /** + * @return HtmlDocument + */ + public function toHtml() + { + $timerange = $this->getTimeframe()->getTimerange(); + + $html = new HtmlDocument(); + + foreach ($this->getReportlets() as $reportlet) { + $implementation = $reportlet->getImplementation(); + + $html->add($implementation->getHtml($timerange, $reportlet->getConfig())); + } + + return $html; + } + + /** + * @return string + */ + public function toCsv() + { + $timerange = $this->getTimeframe()->getTimerange(); + + $csv = []; + + foreach ($this->getReportlets() as $reportlet) { + $implementation = $reportlet->getImplementation(); + + if ($implementation->providesData()) { + $data = $implementation->getData($timerange, $reportlet->getConfig()); + $csv[] = array_merge($data->getDimensions(), $data->getValues()); + foreach ($data->getRows() as $row) { + $csv[] = array_merge($row->getDimensions(), $row->getValues()); + } + + break; + } + } + + return Str::putcsv($csv); + } + + /** + * @return string + */ + public function toJson() + { + $timerange = $this->getTimeframe()->getTimerange(); + + $json = []; + + foreach ($this->getReportlets() as $reportlet) { + $implementation = $reportlet->getImplementation(); + + if ($implementation->providesData()) { + $data = $implementation->getData($timerange, $reportlet->getConfig()); + $dimensions = $data->getDimensions(); + $values = $data->getValues(); + foreach ($data->getRows() as $row) { + $json[] = \array_combine($dimensions, $row->getDimensions()) + + \array_combine($values, $row->getValues()); + } + + break; + } + } + + return json_encode($json); + } + + /** + * @return PrintableHtmlDocument + * + * @throws Exception + */ + public function toPdf() + { + $html = (new PrintableHtmlDocument()) + ->setTitle($this->getName()) + ->addAttributes(['class' => 'icinga-module module-reporting']) + ->addHtml($this->toHtml()); + + if ($this->template !== null) { + $this->template->setMacros([ + 'title' => $this->name, + 'date' => (new DateTime())->format('jS M, Y'), + 'time_frame' => $this->timeframe->getName(), + 'time_frame_absolute' => sprintf( + 'From %s to %s', + $this->timeframe->getTimerange()->getStart()->format('r'), + $this->timeframe->getTimerange()->getEnd()->format('r') + ) + ]); + + $html->setCoverPage($this->template->getCoverPage()->setMacros($this->template->getMacros())); + $html->setHeader($this->template->getHeader()->setMacros($this->template->getMacros())); + $html->setFooter($this->template->getFooter()->setMacros($this->template->getMacros())); + } + + return $html; + } +} diff --git a/library/Reporting/ReportData.php b/library/Reporting/ReportData.php new file mode 100644 index 0000000..787f4db --- /dev/null +++ b/library/Reporting/ReportData.php @@ -0,0 +1,71 @@ +rows; + } + + public function setRows(array $rows) + { + $this->rows = $rows; + + return $this; + } + + public function getAverages() + { + $totals = $this->getTotals(); + $averages = []; + $count = \count($this); + + foreach ($totals as $total) { + $averages[] = $total / $count; + } + + return $averages; + } + +// public function getMaximums() +// { +// } + +// public function getMinimums() +// { +// } + + public function getTotals() + { + $totals = []; + + foreach ((array) $this->getRows() as $row) { + $i = 0; + foreach ((array) $row->getValues() as $value) { + if (! isset($totals[$i])) { + $totals[$i] = $value; + } else { + $totals[$i] += $value; + } + + ++$i; + } + } + + return $totals; + } + + public function count(): int + { + return count((array) $this->getRows()); + } +} diff --git a/library/Reporting/ReportRow.php b/library/Reporting/ReportRow.php new file mode 100644 index 0000000..1536488 --- /dev/null +++ b/library/Reporting/ReportRow.php @@ -0,0 +1,10 @@ +id; + } + + /** + * @param int $id + * + * @return $this + */ + public function setId($id) + { + $this->id = $id; + + return $this; + } + + /** + * @return string + */ + public function getClass() + { + return $this->class; + } + + /** + * @param string $class + * + * @return $this + */ + public function setClass($class) + { + $this->class = $class; + + return $this; + } + + /** + * @return array + */ + public function getConfig() + { + return $this->config; + } + + /** + * @param array $config + * + * @return $this + */ + public function setConfig($config) + { + $this->config = $config; + + return $this; + } + + /** + * @return \Icinga\Module\Reporting\Hook\ReportHook + */ + public function getImplementation() + { + $class = $this->getClass(); + + return new $class; + } +} diff --git a/library/Reporting/Reports/SystemReport.php b/library/Reporting/Reports/SystemReport.php new file mode 100644 index 0000000..8a3d8dd --- /dev/null +++ b/library/Reporting/Reports/SystemReport.php @@ -0,0 +1,39 @@ +loadHTML($html); + + $style = $doc->getElementsByTagName('style')->item(0); + $style->parentNode->removeChild($style); + + $title = $doc->getElementsByTagName('title')->item(0); + $title->parentNode->removeChild($title); + + $meta = $doc->getElementsByTagName('meta')->item(0); + $meta->parentNode->removeChild($meta); + + $doc->getElementsByTagName('div')->item(0)->setAttribute('class', 'system-report'); + + return new HtmlString($doc->saveHTML()); + } +} diff --git a/library/Reporting/RetryConnection.php b/library/Reporting/RetryConnection.php new file mode 100644 index 0000000..ebadfd2 --- /dev/null +++ b/library/Reporting/RetryConnection.php @@ -0,0 +1,66 @@ +getMessage(), [ + 'server has gone away', + 'no connection to the server', + 'Lost connection', + 'Error while sending', + 'is dead or not enabled', + 'decryption failed or bad record mac', + 'server closed the connection unexpectedly', + 'SSL connection has been closed unexpectedly', + 'Error writing data to the connection', + 'Resource deadlock avoided', + 'Transaction() on null', + 'child connection forced to terminate due to client_idle_limit', + 'query_wait_timeout', + 'reset by peer', + 'Physical connection is not usable', + 'TCP Provider: Error code 0x68', + 'ORA-03114', + 'Packets out of order. Expected', + 'Adaptive Server connection failed', + 'Communication link failure', + ]); + + if (! $lostConnection) { + throw $e; + } + + $this->disconnect(); + + try { + $this->connect(); + } catch (\Exception $e) { + $noConnection = Str::contains($e->getMessage(), [ + 'No such file or directory', + 'Connection refused' + ]); + + if (! $noConnection) { + throw $e; + } + + \sleep(10); + + $this->connect(); + } + + $sth = parent::prepexec($stmt, $values); + } + + return $sth; + } +} diff --git a/library/Reporting/Schedule.php b/library/Reporting/Schedule.php new file mode 100644 index 0000000..e0ffa9f --- /dev/null +++ b/library/Reporting/Schedule.php @@ -0,0 +1,160 @@ +id; + } + + /** + * @param int $id + * + * @return $this + */ + public function setId($id) + { + $this->id = $id; + + return $this; + } + + /** + * @return int + */ + public function getReportId() + { + return $this->reportId; + } + + /** + * @param int $id + * + * @return $this + */ + public function setReportId($id) + { + $this->reportId = $id; + + return $this; + } + + /** + * @return \DateTime + */ + public function getStart() + { + return $this->start; + } + + /** + * @param \DateTime $start + * + * @return $this + */ + public function setStart(\DateTime $start) + { + $this->start = $start; + + return $this; + } + + /** + * @return string + */ + public function getFrequency() + { + return $this->frequency; + } + + /** + * @param string $frequency + * + * @return $this + */ + public function setFrequency($frequency) + { + $this->frequency = $frequency; + + return $this; + } + + /** + * @return string + */ + public function getAction() + { + return $this->action; + } + + /** + * @param string $action + * + * @return $this + */ + public function setAction($action) + { + $this->action = $action; + + return $this; + } + + /** + * @return array + */ + public function getConfig() + { + return $this->config; + } + + /** + * @param array $config + * + * @return $this + */ + public function setConfig(array $config) + { + $this->config = $config; + + return $this; + } + + /** + * @return string + */ + public function getChecksum() + { + return \md5( + $this->getId() + . $this->getReportId() + . $this->getStart()->format('Y-m-d H:i:s') + . $this->getAction() + . $this->getFrequency() + . \json_encode($this->getConfig()) + ); + } +} diff --git a/library/Reporting/Scheduler.php b/library/Reporting/Scheduler.php new file mode 100644 index 0000000..1b8d9f6 --- /dev/null +++ b/library/Reporting/Scheduler.php @@ -0,0 +1,176 @@ +modify('midnight'); + + $diff = $midnight->diff($dateTime); + + return $diff->h * 60 * 60 + $diff->i * 60 + $diff->s; +} + +class Scheduler +{ + protected $db; + + protected $loop; + + /** @var array */ + protected $schedules = []; + + /** @var array */ + protected $timers = []; + + public function __construct(Connection $db) + { + $this->db = $db; + $this->loop = Loop::create(); + } + + public function run() + { + $updateTimers = function () use (&$updateTimers) { + $this->updateTimers(); + + $this->loop->addTimer(60, $updateTimers); + }; + + $this->loop->futureTick($updateTimers); + + $this->loop->run(); + } + + protected function fetchSchedules() + { + $schedules = []; + + $select = (new Select()) + ->from('schedule') + ->columns('*'); + + foreach ($this->db->select($select) as $row) { + $schedule = (new Schedule()) + ->setId((int) $row->id) + ->setReportId((int) $row->report_id) + ->setAction($row->action) + ->setConfig(\json_decode($row->config, true)) + ->setStart((new \DateTime())->setTimestamp((int) $row->start / 1000)) + ->setFrequency($row->frequency); + + $schedules[$schedule->getChecksum()] = $schedule; + } + + return $schedules; + } + + protected function updateTimers() + { + $schedules = $this->fetchSchedules(); + + $remove = \array_diff_key($this->schedules, $schedules); + + foreach ($remove as $schedule) { + printf("Removing job %s.\n", "Schedule {$schedule->getId()}"); + + $checksum = $schedule->getChecksum(); + + if (isset($this->timers[$checksum])) { + $this->loop->cancelTimer($this->timers[$checksum]); + unset($this->timers[$checksum]); + } else { + printf("Can't find timer for job %s.\n", $checksum); + } + } + + $add = \array_diff_key($schedules, $this->schedules); + + foreach ($add as $schedule) { + $this->add($schedule); + } + + $this->schedules = $schedules; + } + + + protected function add(Schedule $schedule) + { + $name = "Schedule {$schedule->getId()}"; + $frequency = $schedule->getFrequency(); + $start = clone $schedule->getStart(); + $callback = function () use ($schedule) { + $actionClass = $schedule->getAction(); + /** @var ActionHook $action */ + $action = new $actionClass; + + $action->execute( + Report::fromDb($schedule->getReportId()), + $schedule->getConfig() + ); + }; + + switch ($frequency) { + case 'minutely': + $modify = '+1 minute'; + break; + case 'hourly': + $modify = '+1 hour'; + break; + case 'daily': + $modify = '+1 day'; + break; + case 'weekly': + $modify = '+1 week'; + break; + case 'monthly': + $modify = '+1 month'; + break; + default: + throw new \InvalidArgumentException('Invalid frequency.'); + } + + $now = new \DateTime(); + + if ($start < $now) { +// printf("Scheduling job %s to run immediately.\n", $name); +// $this->loop->futureTick($callback); + + while ($start < $now) { + $start->modify($modify); + } + } + + $next = clone $start; + $next->modify($modify); + $interval = $next->getTimestamp() - $start->getTimestamp(); + + $current = $start->getTimestamp() - $now->getTimestamp(); + + printf("Scheduling job %s to run at %s.\n", $name, $start->format('Y-m-d H:i:s')); + + $loop = function () use (&$loop, $name, $callback, $interval, $schedule) { + $callback(); + + $nextRun = (new \DateTime()) + ->add(new \DateInterval("PT{$interval}S")); + + printf("Scheduling job %s to run at %s.\n", $name, $nextRun->format('Y-m-d H:i:s')); + + $timer = $this->loop->addTimer($interval, $loop); + + $this->timers[$schedule->getChecksum()] = $timer; + }; + + $timer = $this->loop->addTimer($current, $loop); + + $this->timers[$schedule->getChecksum()] = $timer; + } +} diff --git a/library/Reporting/Str.php b/library/Reporting/Str.php new file mode 100644 index 0000000..d4c7355 --- /dev/null +++ b/library/Reporting/Str.php @@ -0,0 +1,37 @@ +getDb(); + + $select = (new Select()) + ->from('timeframe') + ->columns('*') + ->where(['id = ?' => $id]); + + $row = $db->select($select)->fetch(); + + if ($row === false) { + throw new \Exception('Timeframe not found'); + } + + $timeframe + ->setId($row->id) + ->setName($row->name) + ->setTitle($row->title) + ->setStart($row->start) + ->setEnd($row->end); + + return $timeframe; + } + + /** + * @return int + */ + public function getId() + { + return $this->id; + } + + /** + * @param int $id + * + * @return $this + */ + public function setId($id) + { + $this->id = $id; + + return $this; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @param string $name + * + * @return $this + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * @param string $title + * + * @return $this + */ + public function setTitle($title) + { + $this->title = $title; + + return $this; + } + + /** + * @return string + */ + public function getStart() + { + return $this->start; + } + + /** + * @param string $start + * + * @return $this + */ + public function setStart($start) + { + $this->start = $start; + + return $this; + } + + /** + * @return string + */ + public function getEnd() + { + return $this->end; + } + + /** + * @param string $end + * + * @return $this + */ + public function setEnd($end) + { + $this->end = $end; + + return $this; + } + + public function getTimerange() + { + $start = new \DateTime($this->getStart()); + $end = new \DateTime($this->getEnd()); + + return new Timerange($start, $end); + } +} diff --git a/library/Reporting/Timerange.php b/library/Reporting/Timerange.php new file mode 100644 index 0000000..086bfb8 --- /dev/null +++ b/library/Reporting/Timerange.php @@ -0,0 +1,35 @@ +start = $start; + $this->end = $end; + } + + /** + * @return \DateTime + */ + public function getStart() + { + return $this->start; + } + + /** + * @return \DateTime + */ + public function getEnd() + { + return $this->end; + } +} diff --git a/library/Reporting/Values.php b/library/Reporting/Values.php new file mode 100644 index 0000000..3aa9b24 --- /dev/null +++ b/library/Reporting/Values.php @@ -0,0 +1,21 @@ +values; + } + + public function setValues(array $values) + { + $this->values = $values; + + return $this; + } +} diff --git a/library/Reporting/Web/Controller.php b/library/Reporting/Web/Controller.php new file mode 100644 index 0000000..5040183 --- /dev/null +++ b/library/Reporting/Web/Controller.php @@ -0,0 +1,20 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ + '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 @@ + '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 @@ +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 @@ + '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); + } +} diff --git a/library/vendor/ipl/Html/src/FormElement/FileElement.php b/library/vendor/ipl/Html/src/FormElement/FileElement.php new file mode 100644 index 0000000..88aeb8c --- /dev/null +++ b/library/vendor/ipl/Html/src/FormElement/FileElement.php @@ -0,0 +1,15 @@ +