diff options
Diffstat (limited to 'library')
44 files changed, 1373 insertions, 1108 deletions
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 @@ <?php + // Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 namespace Icinga\Module\Reporting\Actions; @@ -9,6 +10,9 @@ use Icinga\Module\Reporting\Hook\ActionHook; use Icinga\Module\Reporting\Mail; use Icinga\Module\Reporting\Report; use ipl\Html\Form; +use ipl\Stdlib\Str; +use ipl\Validator\CallbackValidator; +use ipl\Validator\EmailAddressValidator; class SendMail extends ActionHook { @@ -28,7 +32,9 @@ class SendMail extends ActionHook $mail = new Mail(); - $mail->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<int, string> $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 @@ <?php + // Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 namespace Icinga\Module\Reporting\Cli; use Icinga\Application\Icinga; use Icinga\Application\Version; -use Icinga\Module\Reporting\Database; class Command extends \Icinga\Cli\Command { - use Database; - // Fix Web 2 issue where $configs is not properly initialized protected $configs = []; diff --git a/library/Reporting/Database.php b/library/Reporting/Database.php index 3dabe17..7ea32a7 100644 --- a/library/Reporting/Database.php +++ b/library/Reporting/Database.php @@ -1,4 +1,5 @@ <?php + // Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 namespace Icinga\Module\Reporting; @@ -6,53 +7,97 @@ namespace Icinga\Module\Reporting; use Icinga\Application\Config; use Icinga\Data\ResourceFactory; use ipl\Sql; +use PDO; +use stdClass; -trait Database +final class Database { - protected function getDb($resource = null) + /** @var RetryConnection Database connection */ + private static $instance; + + private function __construct() { - $config = new Sql\Config(ResourceFactory::getResourceConfig( - $resource ?: Config::module('reporting')->get('backend', 'resource', 'reporting') - )); + } - $config->options = [\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_OBJ]; - if ($config->db === 'mysql') { - $config->options[\PDO::MYSQL_ATTR_INIT_COMMAND] = "SET SESSION SQL_MODE='STRICT_TRANS_TABLES" - . ",NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'"; + /** + * 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<int, string> + */ + 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<int, string> + */ + 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<int, string> + */ + 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 @@ <?php + // Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 namespace Icinga\Module\Reporting; diff --git a/library/Reporting/Hook/ActionHook.php b/library/Reporting/Hook/ActionHook.php index ef550ee..2dd20fb 100644 --- a/library/Reporting/Hook/ActionHook.php +++ b/library/Reporting/Hook/ActionHook.php @@ -1,4 +1,5 @@ <?php + // Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 namespace Icinga\Module\Reporting\Hook; @@ -15,13 +16,14 @@ abstract class ActionHook abstract public function getName(); /** - * @param Report $report - * @param array $config + * @param Report $report + * @param array $config */ abstract public function execute(Report $report, array $config); /** - * @param Form $form + * @param Form $form + * @param Report $report */ public function initConfigForm(Form $form, Report $report) { diff --git a/library/Reporting/Hook/ReportHook.php b/library/Reporting/Hook/ReportHook.php index 13cc01e..b9467c0 100644 --- a/library/Reporting/Hook/ReportHook.php +++ b/library/Reporting/Hook/ReportHook.php @@ -1,4 +1,5 @@ <?php + // Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 namespace Icinga\Module\Reporting\Hook; @@ -20,8 +21,8 @@ abstract class ReportHook abstract public function getName(); /** - * @param Timerange $timerange - * @param array $config + * @param Timerange $timerange + * @param array|null $config * * @return ReportData|null */ @@ -33,8 +34,8 @@ abstract class ReportHook /** * Get the HTML of the report * - * @param Timerange $timerange - * @param array $config + * @param Timerange $timerange + * @param array|null $config * * @return ValidHtml|null */ @@ -46,7 +47,7 @@ abstract class ReportHook /** * Initialize the report's configuration form * - * @param Form $form + * @param Form $form */ public function initConfigForm(Form $form) { @@ -55,7 +56,7 @@ abstract class ReportHook /** * Get the description of the report * - * @return string + * @return ?string */ public function getDescription() { diff --git a/library/Reporting/Mail.php b/library/Reporting/Mail.php index 7581f45..810b166 100644 --- a/library/Reporting/Mail.php +++ b/library/Reporting/Mail.php @@ -1,4 +1,5 @@ <?php + // Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 namespace Icinga\Module\Reporting; @@ -11,15 +12,15 @@ use Zend_Mime_Part; class Mail { /** @var string */ - const DEFAULT_SUBJECT = 'Icinga Reporting'; + public const DEFAULT_SUBJECT = 'Icinga Reporting'; - /** @var string */ + /** @var ?string */ protected $from; /** @var string */ protected $subject = self::DEFAULT_SUBJECT; - /** @var Zend_Mail_Transport_Sendmail */ + /** @var ?Zend_Mail_Transport_Sendmail */ protected $transport; /** @var array */ @@ -43,7 +44,7 @@ class Mail } foreach (['HTTP_HOST', 'SERVER_NAME', 'HOSTNAME'] as $key) { - if (isset($_SEVER[$key])) { + if (isset($_SERVER[$key])) { $this->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 @@ +<?php + +/* Icinga Reporting | (c) 2023 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Reporting\Model; + +use ipl\Orm\Behavior\MillisecondTimestamp; +use ipl\Orm\Behaviors; +use ipl\Orm\Model; +use ipl\Orm\Relations; + +class Config extends Model +{ + public function getTableName() + { + return 'config'; + } + + public function getKeyName() + { + return 'id'; + } + + public function getColumns() + { + return [ + 'reportlet_id', + 'name', + 'value', + 'ctime', + 'mtime' + ]; + } + + public function createBehaviors(Behaviors $behaviors) + { + $behaviors->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 @@ +<?php + +/* Icinga Reporting | (c) 2023 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Reporting\Model; + +use DateTime; +use ipl\Orm\Behavior\MillisecondTimestamp; +use ipl\Orm\Behaviors; +use ipl\Orm\Model; +use ipl\Orm\Relations; + +/** + * A Report database model + * + * @property int $id Unique identifier of this model + * @property int $timeframe_id The timeframe id used by this report + * @property int $template_id The template id used by this report + * @property string $author The author of this report + * @property string $name The name of this report + * @property DateTime $ctime The creation time of this report + * @property DateTime $mtime Modify time of this report + */ +class Report extends Model +{ + public function getTableName() + { + return 'report'; + } + + public function getKeyName() + { + return 'id'; + } + + public function getColumns() + { + return [ + 'timeframe_id', + 'template_id', + 'author', + 'name', + 'ctime', + 'mtime' + ]; + } + + public function getDefaultSort() + { + return ['name']; + } + + public function createBehaviors(Behaviors $behaviors) + { + $behaviors->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 @@ +<?php + +/* Icinga Reporting | (c) 2023 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Reporting\Model; + +use ipl\Orm\Behavior\MillisecondTimestamp; +use ipl\Orm\Behaviors; +use ipl\Orm\Model; +use ipl\Orm\Relations; + +class Reportlet extends Model +{ + public function getTableName() + { + return 'reportlet'; + } + + public function getKeyName() + { + return 'id'; + } + + public function getColumns() + { + return [ + 'report_id', + 'class', + 'ctime', + 'mtime' + ]; + } + + public function createBehaviors(Behaviors $behaviors) + { + $behaviors->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 @@ +<?php + +/* Icinga Reporting | (c) 2023 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Reporting\Model; + +use ipl\Orm\Behavior\MillisecondTimestamp; +use ipl\Orm\Behaviors; +use ipl\Orm\Model; +use ipl\Orm\Relations; + +class Schedule extends Model +{ + public function getTableName() + { + return 'schedule'; + } + + public function getKeyName() + { + return 'id'; + } + + public function getColumns() + { + return [ + 'report_id', + 'author', + 'action', + 'config', + 'ctime', + 'mtime' + ]; + } + + public function createBehaviors(Behaviors $behaviors) + { + $behaviors->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 @@ +<?php + +/* Icinga Reporting | (c) 2023 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Reporting\Model; + +use DateTime; +use ipl\Orm\Behavior\BoolCast; +use ipl\Orm\Behavior\MillisecondTimestamp; +use ipl\Orm\Behaviors; +use ipl\Orm\Model; + +/** + * A database model for reporting schema version table + * + * @property int $id Unique identifier of the database schema entries + * @property string $version The current schema version of Icinga Web + * @property DateTime $timestamp The insert/modify time of the schema entry + * @property bool $success Whether the database migration of the current version was successful + * @property ?string $reason The reason why the database migration has failed + */ +class Schema extends Model +{ + public function getTableName(): string + { + return 'reporting_schema'; + } + + public function getKeyName() + { + return 'id'; + } + + public function getColumns(): array + { + return [ + 'version', + 'timestamp', + 'success', + 'reason' + ]; + } + + public function createBehaviors(Behaviors $behaviors): void + { + $behaviors->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 @@ +<?php + +/* Icinga Reporting | (c) 2023 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Reporting\Model; + +use ipl\Orm\Behavior\MillisecondTimestamp; +use ipl\Orm\Behaviors; +use ipl\Orm\Model; +use ipl\Orm\Relations; + +class Template extends Model +{ + public function getTableName() + { + return 'template'; + } + + public function getKeyName() + { + return 'id'; + } + + public function getColumns() + { + return [ + 'author', + 'name', + 'settings', + 'ctime', + 'mtime' + ]; + } + + public function getDefaultSort() + { + return ['name']; + } + + public function createBehaviors(Behaviors $behaviors) + { + $behaviors->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 @@ +<?php + +/* Icinga Reporting | (c) 2023 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Reporting\Model; + +use ipl\Orm\Behavior\MillisecondTimestamp; +use ipl\Orm\Behaviors; +use ipl\Orm\Model; +use ipl\Orm\Relations; + +class Timeframe extends Model +{ + public function getTableName() + { + return 'timeframe'; + } + + public function getKeyName() + { + return 'id'; + } + + public function getColumns() + { + return [ + 'name', + 'title', + 'start', + 'end', + 'ctime', + 'mtime' + ]; + } + + public function getDefaultSort(): string + { + return 'name'; + } + + public function createBehaviors(Behaviors $behaviors) + { + $behaviors->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 @@ <?php + // Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 namespace Icinga\Module\Reporting; diff --git a/library/Reporting/ProvidedHook/DbMigration.php b/library/Reporting/ProvidedHook/DbMigration.php new file mode 100644 index 0000000..2fa5cda --- /dev/null +++ b/library/Reporting/ProvidedHook/DbMigration.php @@ -0,0 +1,79 @@ +<?php + +/* Icinga Reporting | (c) 2023 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Reporting\ProvidedHook; + +use Icinga\Application\Hook\DbMigrationHook; +use Icinga\Module\Reporting\Database; +use Icinga\Module\Reporting\Model\Schema; +use ipl\Orm\Query; +use ipl\Sql\Connection; + +class DbMigration extends DbMigrationHook +{ + public function getName(): string + { + return $this->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 @@ <?php + // Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 namespace Icinga\Module\Reporting; diff --git a/library/Reporting/Report.php b/library/Reporting/Report.php index 7f2eee3..ac5c9b3 100644 --- a/library/Reporting/Report.php +++ b/library/Reporting/Report.php @@ -1,19 +1,22 @@ <?php + // Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 namespace Icinga\Module\Reporting; use DateTime; use Exception; +use Icinga\Module\Icingadb\ProvidedHook\Reporting\ServiceSlaReport; +use Icinga\Module\Icingadb\ProvidedHook\Reporting\SlaReport; use Icinga\Module\Pdfexport\PrintableHtmlDocument; +use Icinga\Module\Reporting\Model; use Icinga\Module\Reporting\Web\Widget\Template; use ipl\Html\HtmlDocument; -use ipl\Sql; + +use function ipl\I18n\t; class Report { - use Database; - /** @var int */ protected $id; @@ -36,88 +39,43 @@ class Report protected $template; /** - * @param int $id + * Create report from the given model * - * @return static + * @param Model\Report $reportModel * - * @throws Exception + * @return static + * @throws Exception If no reportlets are configured */ - public static function fromDb($id) + public static function fromModel(Model\Report $reportModel): self { $report = new static(); - $db = $report->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; @@ -132,18 +90,6 @@ class Report } /** - * @param int $id - * - * @return $this - */ - public function setId($id) - { - $this->id = $id; - - return $this; - } - - /** * @return string */ public function getName() @@ -152,18 +98,6 @@ class Report } /** - * @param string $name - * - * @return $this - */ - public function setName($name) - { - $this->name = $name; - - return $this; - } - - /** * @return string */ public function getAuthor() @@ -172,18 +106,6 @@ class Report } /** - * @param string $author - * - * @return $this - */ - public function setAuthor($author) - { - $this->author = $author; - - return $this; - } - - /** * @return Timeframe */ public function getTimeframe() @@ -192,18 +114,6 @@ class Report } /** - * @param Timeframe $timeframe - * - * @return $this - */ - public function setTimeframe(Timeframe $timeframe) - { - $this->timeframe = $timeframe; - - return $this; - } - - /** * @return Reportlet[] */ public function getReportlets() @@ -212,18 +122,6 @@ class Report } /** - * @param Reportlet[] $reportlets - * - * @return $this - */ - public function setReportlets(array $reportlets) - { - $this->reportlets = $reportlets; - - return $this; - } - - /** * @return Schedule */ public function getSchedule() @@ -232,18 +130,6 @@ class Report } /** - * @param Schedule $schedule - * - * @return $this - */ - public function setSchedule(Schedule $schedule) - { - $this->schedule = $schedule; - - return $this; - } - - /** * @return Template */ public function getTemplate() @@ -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 @@ <?php + // Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 namespace Icinga\Module\Reporting; diff --git a/library/Reporting/ReportRow.php b/library/Reporting/ReportRow.php index 1536488..e7cb53d 100644 --- a/library/Reporting/ReportRow.php +++ b/library/Reporting/ReportRow.php @@ -1,4 +1,5 @@ <?php + // Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 namespace Icinga\Module\Reporting; diff --git a/library/Reporting/Reportlet.php b/library/Reporting/Reportlet.php index 2876a00..1c74f38 100644 --- a/library/Reporting/Reportlet.php +++ b/library/Reporting/Reportlet.php @@ -1,13 +1,11 @@ <?php + // Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 namespace Icinga\Module\Reporting; class Reportlet { - /** @var int */ - protected $id; - /** @var string */ protected $class; @@ -15,23 +13,29 @@ class Reportlet protected $config; /** - * @return int - */ - public function getId() - { - return $this->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; } /** @@ -43,18 +47,6 @@ class Reportlet } /** - * @param string $class - * - * @return $this - */ - public function setClass($class) - { - $this->class = $class; - - return $this; - } - - /** * @return array */ public function getConfig() @@ -63,24 +55,12 @@ class Reportlet } /** - * @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; + 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 @@ <?php + // Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 namespace Icinga\Module\Reporting\Reports; +use Icinga\Application\Icinga; use Icinga\Module\Reporting\Hook\ReportHook; use Icinga\Module\Reporting\Timerange; use ipl\Html\HtmlString; @@ -18,22 +20,29 @@ class SystemReport extends ReportHook { ob_start(); phpinfo(); + /** @var string $html */ $html = ob_get_clean(); - $doc = new \DOMDocument(); - @$doc->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 @@ <?php + // Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 namespace Icinga\Module\Reporting; diff --git a/library/Reporting/Schedule.php b/library/Reporting/Schedule.php index e0ffa9f..ddd8bd3 100644 --- a/library/Reporting/Schedule.php +++ b/library/Reporting/Schedule.php @@ -1,160 +1,155 @@ <?php + // Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 namespace Icinga\Module\Reporting; -class Schedule -{ - /** @var int */ - protected $id; +use Exception; +use Icinga\Module\Reporting\Hook\ActionHook; +use Icinga\Util\Json; +use ipl\Scheduler\Common\TaskProperties; +use ipl\Scheduler\Contract\Task; +use Ramsey\Uuid\Uuid; +use React\EventLoop\Loop; +use React\Promise; +use React\Promise\ExtendedPromiseInterface; - /** @var int */ - protected $reportId; +use function md5; - /** @var \DateTime */ - protected $start; +class Schedule implements Task +{ + use TaskProperties; - /** @var string */ - protected $frequency; + /** @var int */ + protected $id; /** @var string */ protected $action; /** @var array */ - protected $config; + protected $config = []; - /** - * @return int - */ - public function getId() + /** @var Report */ + protected $report; + + public function __construct(string $name, string $action, array $config, Report $report) { - return $this->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 @@ -<?php -// Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 - -namespace Icinga\Module\Reporting; - -use Cron\CronExpression; -use ipl\Sql\Connection; -use ipl\Sql\Select; -use React\EventLoop\Factory as Loop; - -function datetime_get_time_of_day(\DateTime $dateTime) -{ - $midnight = clone $dateTime; - $midnight->modify('midnight'); - - $diff = $midnight->diff($dateTime); - - return $diff->h * 60 * 60 + $diff->i * 60 + $diff->s; -} - -class Scheduler -{ - protected $db; - - protected $loop; - - /** @var array */ - protected $schedules = []; - - /** @var array */ - protected $timers = []; - - public function __construct(Connection $db) - { - $this->db = $db; - $this->loop = Loop::create(); - } - - public function run() - { - $updateTimers = function () use (&$updateTimers) { - $this->updateTimers(); - - $this->loop->addTimer(60, $updateTimers); - }; - - $this->loop->futureTick($updateTimers); - - $this->loop->run(); - } - - protected function fetchSchedules() - { - $schedules = []; - - $select = (new Select()) - ->from('schedule') - ->columns('*'); - - foreach ($this->db->select($select) as $row) { - $schedule = (new Schedule()) - ->setId((int) $row->id) - ->setReportId((int) $row->report_id) - ->setAction($row->action) - ->setConfig(\json_decode($row->config, true)) - ->setStart((new \DateTime())->setTimestamp((int) $row->start / 1000)) - ->setFrequency($row->frequency); - - $schedules[$schedule->getChecksum()] = $schedule; - } - - return $schedules; - } - - protected function updateTimers() - { - $schedules = $this->fetchSchedules(); - - $remove = \array_diff_key($this->schedules, $schedules); - - foreach ($remove as $schedule) { - printf("Removing job %s.\n", "Schedule {$schedule->getId()}"); - - $checksum = $schedule->getChecksum(); - - if (isset($this->timers[$checksum])) { - $this->loop->cancelTimer($this->timers[$checksum]); - unset($this->timers[$checksum]); - } else { - printf("Can't find timer for job %s.\n", $checksum); - } - } - - $add = \array_diff_key($schedules, $this->schedules); - - foreach ($add as $schedule) { - $this->add($schedule); - } - - $this->schedules = $schedules; - } - - - protected function add(Schedule $schedule) - { - $name = "Schedule {$schedule->getId()}"; - $frequency = $schedule->getFrequency(); - $start = clone $schedule->getStart(); - $callback = function () use ($schedule) { - $actionClass = $schedule->getAction(); - /** @var ActionHook $action */ - $action = new $actionClass; - - $action->execute( - Report::fromDb($schedule->getReportId()), - $schedule->getConfig() - ); - }; - - switch ($frequency) { - case 'minutely': - $modify = '+1 minute'; - break; - case 'hourly': - $modify = '+1 hour'; - break; - case 'daily': - $modify = '+1 day'; - break; - case 'weekly': - $modify = '+1 week'; - break; - case 'monthly': - $modify = '+1 month'; - break; - default: - throw new \InvalidArgumentException('Invalid frequency.'); - } - - $now = new \DateTime(); - - if ($start < $now) { -// printf("Scheduling job %s to run immediately.\n", $name); -// $this->loop->futureTick($callback); - - while ($start < $now) { - $start->modify($modify); - } - } - - $next = clone $start; - $next->modify($modify); - $interval = $next->getTimestamp() - $start->getTimestamp(); - - $current = $start->getTimestamp() - $now->getTimestamp(); - - printf("Scheduling job %s to run at %s.\n", $name, $start->format('Y-m-d H:i:s')); - - $loop = function () use (&$loop, $name, $callback, $interval, $schedule) { - $callback(); - - $nextRun = (new \DateTime()) - ->add(new \DateInterval("PT{$interval}S")); - - printf("Scheduling job %s to run at %s.\n", $name, $nextRun->format('Y-m-d H:i:s')); - - $timer = $this->loop->addTimer($interval, $loop); - - $this->timers[$schedule->getChecksum()] = $timer; - }; - - $timer = $this->loop->addTimer($current, $loop); - - $this->timers[$schedule->getChecksum()] = $timer; - } -} diff --git a/library/Reporting/Str.php b/library/Reporting/Str.php index d4c7355..77eb65e 100644 --- a/library/Reporting/Str.php +++ b/library/Reporting/Str.php @@ -1,4 +1,5 @@ <?php + // Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 namespace Icinga\Module\Reporting; @@ -7,6 +8,7 @@ class Str { public static function putcsv(array $data, $delimiter = ',', $enclosure = '"', $escape = '\\') { + /** @var resource $fp */ $fp = fopen('php://temp', 'r+b'); foreach ($data as $row) { @@ -15,6 +17,7 @@ class Str rewind($fp); + /** @var string $csv */ $csv = stream_get_contents($fp); fclose($fp); diff --git a/library/Reporting/Timeframe.php b/library/Reporting/Timeframe.php index f295779..4e8cb9e 100644 --- a/library/Reporting/Timeframe.php +++ b/library/Reporting/Timeframe.php @@ -1,14 +1,13 @@ <?php + // Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 namespace Icinga\Module\Reporting; -use ipl\Sql\Select; +use Icinga\Module\Reporting\Model; class Timeframe { - use Database; - /** @var int */ protected $id; @@ -25,35 +24,21 @@ class Timeframe protected $end; /** - * @param int $id + * Create timeframe from the given model * - * @return static + * @param Model\Timeframe $timeframeModel * - * @throws \Exception + * @return static */ - public static function fromDb($id) + public static function fromModel(Model\Timeframe $timeframeModel): self { $timeframe = new static(); - $db = $timeframe->getDb(); - - $select = (new Select()) - ->from('timeframe') - ->columns('*') - ->where(['id = ?' => $id]); - - $row = $db->select($select)->fetch(); - - if ($row === false) { - throw new \Exception('Timeframe not found'); - } - - $timeframe - ->setId($row->id) - ->setName($row->name) - ->setTitle($row->title) - ->setStart($row->start) - ->setEnd($row->end); + $timeframe->id = $timeframeModel->id; + $timeframe->name = $timeframeModel->name; + $timeframe->title = $timeframeModel->title; + $timeframe->start = $timeframeModel->start; + $timeframe->end = $timeframeModel->end; return $timeframe; } @@ -67,18 +52,6 @@ class Timeframe } /** - * @param int $id - * - * @return $this - */ - public function setId($id) - { - $this->id = $id; - - return $this; - } - - /** * @return string */ public function getName() @@ -87,18 +60,6 @@ class Timeframe } /** - * @param string $name - * - * @return $this - */ - public function setName($name) - { - $this->name = $name; - - return $this; - } - - /** * @return string */ public function getTitle() @@ -107,18 +68,6 @@ class Timeframe } /** - * @param string $title - * - * @return $this - */ - public function setTitle($title) - { - $this->title = $title; - - return $this; - } - - /** * @return string */ public function getStart() @@ -127,18 +76,6 @@ class Timeframe } /** - * @param string $start - * - * @return $this - */ - public function setStart($start) - { - $this->start = $start; - - return $this; - } - - /** * @return string */ public function getEnd() @@ -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 @@ <?php + // Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 namespace Icinga\Module\Reporting; diff --git a/library/Reporting/Values.php b/library/Reporting/Values.php index 3aa9b24..e7a3cb1 100644 --- a/library/Reporting/Values.php +++ b/library/Reporting/Values.php @@ -1,4 +1,5 @@ <?php + // Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 namespace Icinga\Module\Reporting; diff --git a/library/Reporting/Web/Controller.php b/library/Reporting/Web/Controller.php index 5040183..1123332 100644 --- a/library/Reporting/Web/Controller.php +++ b/library/Reporting/Web/Controller.php @@ -1,20 +1,11 @@ <?php + // Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 namespace Icinga\Module\Reporting\Web; -use ipl\Html\Form; use ipl\Web\Compat\CompatController; class Controller extends CompatController { - protected function redirectForm(Form $form, $url) - { - if ($form->hasBeenSubmitted() - && ((isset($form->valid) && $form->valid === true) - || $form->isValid()) - ) { - $this->redirectNow($url); - } - } } diff --git a/library/Reporting/Web/Flatpickr.php b/library/Reporting/Web/Flatpickr.php deleted file mode 100644 index 5f6605d..0000000 --- a/library/Reporting/Web/Flatpickr.php +++ /dev/null @@ -1,77 +0,0 @@ -<?php -// Icinga Reporting | (c) 2019 Icinga GmbH | GPLv2 - -namespace Icinga\Module\Reporting\Web; - -use Icinga\Application\Version; -use ipl\Html\Html; -use ipl\Web\Compat\CompatDecorator; - -class Flatpickr extends CompatDecorator -{ - protected $allowInput = true; - - /** - * Set whether to allow manual input - * - * @param bool $state - * - * @return $this - */ - public function setAllowInput(bool $state): self - { - $this->allowInput = $state; - - return $this; - } - - protected function assembleElement() - { - if (version_compare(Version::VERSION, '2.9.0', '>=')) { - $element = parent::assembleElement(); - } else { - $element = $this->formElement; - } - - if (version_compare(Version::VERSION, '2.10.0', '<')) { - $element->getAttributes()->set('data-use-flatpickr-fallback', true); - } else { - $element->getAttributes()->set('data-use-datetime-picker', true); - } - - if (! $this->allowInput) { - return $element; - } - - $element->getAttributes() - ->set('data-input', true) - ->set('data-flatpickr-wrap', true) - ->set('data-flatpickr-allow-input', true) - ->set('data-flatpickr-click-opens', 'false'); - - return [ - $element, - Html::tag('button', ['type' => 'button', 'class' => 'icon-calendar', 'data-toggle' => true]), - Html::tag('button', ['type' => 'button', 'class' => 'icon-cancel', 'data-clear' => true]) - ]; - } - - protected function assemble() - { - if (version_compare(Version::VERSION, '2.9.0', '>=')) { - parent::assemble(); - return; - } - - if ($this->formElement->hasBeenValidated() && ! $this->formElement->isValid()) { - $this->getAttributes()->add('class', 'has-error'); - } - - $this->add(array_filter([ - $this->assembleLabel(), - $this->assembleElement(), - $this->assembleDescription(), - $this->assembleErrors() - ])); - } -} diff --git a/library/Reporting/Web/Forms/DecoratedElement.php b/library/Reporting/Web/Forms/DecoratedElement.php deleted file mode 100644 index 2578681..0000000 --- a/library/Reporting/Web/Forms/DecoratedElement.php +++ /dev/null @@ -1,17 +0,0 @@ -<?php -// Icinga Reporting | (c) 2019 Icinga GmbH | GPLv2 - -namespace Icinga\Module\Reporting\Web\Forms; - -use ipl\Html\Contract\FormElementDecorator; - -trait DecoratedElement -{ - protected function addDecoratedElement(FormElementDecorator $decorator, $type, $name, array $attributes) - { - $element = $this->createElement($type, $name, $attributes); - $decorator->decorate($element); - $this->registerElement($element); - $this->add($element); - } -} diff --git a/library/Reporting/Web/Forms/Decorator/CompatDecorator.php b/library/Reporting/Web/Forms/Decorator/CompatDecorator.php deleted file mode 100644 index b2eb536..0000000 --- a/library/Reporting/Web/Forms/Decorator/CompatDecorator.php +++ /dev/null @@ -1,63 +0,0 @@ -<?php -// Icinga Reporting | (c) 2021 Icinga GmbH | GPLv2 - -namespace Icinga\Module\Reporting\Web\Forms\Decorator; - -use Icinga\Application\Version; -use ipl\Html\Attributes; -use ipl\Html\FormElement\CheckboxElement; -use ipl\Html\HtmlElement; - -class CompatDecorator extends \ipl\Web\Compat\CompatDecorator -{ - protected function createCheckboxCompat(CheckboxElement $checkbox) - { - if (! $checkbox->getAttributes()->has('id')) { - $checkbox->setAttribute('id', base64_encode(random_bytes(8))); - } - - $checkbox->getAttributes()->add('class', 'sr-only'); - - $classes = ['toggle-switch']; - if ($checkbox->getAttributes()->get('disabled')->getValue()) { - $classes[] = 'disabled'; - } - - return [ - $checkbox, - new HtmlElement('label', Attributes::create([ - 'class' => $classes, - 'aria-hidden' => 'true', - 'for' => $checkbox->getAttributes()->get('id')->getValue() - ]), new HtmlElement('span', Attributes::create(['class' => 'toggle-slider']))) - ]; - } - - protected function assembleElementCompat() - { - if ($this->formElement instanceof CheckboxElement) { - return $this->createCheckboxCompat($this->formElement); - } - - return $this->formElement; - } - - protected function assemble() - { - if (version_compare(Version::VERSION, '2.9.0', '>=')) { - parent::assemble(); - return; - } - - if ($this->formElement->hasBeenValidated() && ! $this->formElement->isValid()) { - $this->getAttributes()->add('class', 'has-error'); - } - - $this->add(array_filter([ - $this->assembleLabel(), - $this->assembleElementCompat(), - $this->assembleDescription(), - $this->assembleErrors() - ])); - } -} diff --git a/library/Reporting/Web/Forms/ReportForm.php b/library/Reporting/Web/Forms/ReportForm.php index 6b1e692..40e376e 100644 --- a/library/Reporting/Web/Forms/ReportForm.php +++ b/library/Reporting/Web/Forms/ReportForm.php @@ -1,4 +1,5 @@ <?php + // Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 namespace Icinga\Module\Reporting\Web\Forms; @@ -6,54 +7,143 @@ namespace Icinga\Module\Reporting\Web\Forms; use Icinga\Authentication\Auth; use Icinga\Module\Reporting\Database; use Icinga\Module\Reporting\ProvidedReports; -use Icinga\Module\Reporting\Web\Forms\Decorator\CompatDecorator; use ipl\Html\Contract\FormSubmitElement; use ipl\Html\Form; +use ipl\Html\HtmlDocument; +use ipl\Validator\CallbackValidator; use ipl\Web\Compat\CompatForm; class ReportForm extends CompatForm { - use Database; use ProvidedReports; - /** @var bool Hack to disable the {@link onSuccess()} code upon deletion of the report */ - protected $callOnSuccess; - protected $id; - public function setId($id) + /** @var string Label to use for the submit button */ + protected $submitButtonLabel; + + /** @var bool Whether to render the create and show submit button (is only used from DB Web's object detail) */ + protected $renderCreateAndShowButton = false; + + /** + * Create a new form instance with the given report id + * + * @param $id + * + * @return static + */ + public static function fromId($id): self { - $this->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 @@ <?php + // Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 namespace Icinga\Module\Reporting\Web\Forms; use DateTime; -use Icinga\Application\Version; +use Icinga\Application\Icinga; +use Icinga\Application\Web; use Icinga\Authentication\Auth; use Icinga\Module\Reporting\Database; +use Icinga\Module\Reporting\Hook\ActionHook; use Icinga\Module\Reporting\ProvidedActions; use Icinga\Module\Reporting\Report; -use Icinga\Module\Reporting\Web\Flatpickr; -use Icinga\Module\Reporting\Web\Forms\Decorator\CompatDecorator; +use Icinga\Util\Json; use ipl\Html\Contract\FormSubmitElement; use ipl\Html\Form; +use ipl\Html\HtmlDocument; +use ipl\Html\HtmlElement; +use ipl\Scheduler\Contract\Frequency; use ipl\Web\Compat\CompatForm; +use ipl\Web\FormElement\ScheduleElement; + +use function ipl\Stdlib\get_php_type; class ScheduleForm extends CompatForm { - use Database; - use DecoratedElement; use ProvidedActions; /** @var Report */ protected $report; - protected $id; + /** @var ScheduleElement */ + protected $scheduleElement; - public function setReport(Report $report) + public function __construct() { - $this->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 @@ <?php + // Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 namespace Icinga\Module\Reporting\Web\Forms; use Icinga\Module\Reporting\Actions\SendMail; -use Icinga\Module\Reporting\Database; use Icinga\Module\Reporting\ProvidedReports; use Icinga\Module\Reporting\Report; -use Icinga\Module\Reporting\Web\Forms\Decorator\CompatDecorator; use ipl\Web\Compat\CompatForm; class SendForm extends CompatForm { - use Database; use ProvidedReports; /** @var Report */ @@ -27,12 +25,10 @@ class SendForm extends CompatForm protected function assemble() { - $this->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 @@ <?php + // Icinga Reporting | (c) 2019 Icinga GmbH | GPLv2 namespace Icinga\Module\Reporting\Web\Forms; +use Exception; +use GuzzleHttp\Psr7\UploadedFile; use Icinga\Authentication\Auth; use Icinga\Module\Reporting\Database; -use Icinga\Module\Reporting\Web\Forms\Decorator\CompatDecorator; +use Icinga\Util\Json; use ipl\Html\Contract\FormSubmitElement; use ipl\Html\Html; +use ipl\Html\HtmlDocument; use ipl\Web\Compat\CompatForm; -use reportingipl\Html\FormElement\FileElement; class TemplateForm extends CompatForm { - use Database; - - /** @var bool Hack to disable the {@link onSuccess()} code upon deletion of the template */ - protected $callOnSuccess; - protected $template; public function getTemplate() @@ -25,126 +23,141 @@ class TemplateForm extends CompatForm return $this->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<string, mixed> $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 @@ <?php + // Icinga Reporting | (c) 2019 Icinga GmbH | GPLv2 namespace Icinga\Module\Reporting\Web\Forms; +use DateTime; +use Exception; use Icinga\Module\Reporting\Database; -use Icinga\Module\Reporting\Web\Flatpickr; -use Icinga\Module\Reporting\Web\Forms\Decorator\CompatDecorator; use ipl\Html\Contract\FormSubmitElement; +use ipl\Html\FormElement\LocalDateTimeElement; +use ipl\Html\HtmlDocument; +use ipl\Validator\CallbackValidator; use ipl\Web\Compat\CompatForm; class TimeframeForm extends CompatForm { - use Database; - use DecoratedElement; - + /** @var int */ protected $id; - public function setId($id) + /** + * Create a new form instance with the given report + * + * @param int $id + * + * @return static + */ + public static function fromId(int $id): self { - $this->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 @@ <?php + // Icinga Reporting | (c) 2019 Icinga GmbH | GPLv2 namespace Icinga\Module\Reporting\Web; @@ -8,28 +9,29 @@ trait ReportsTimeframesAndTemplatesTabs /** * Create tabs * - * @return \Icinga\Web\Widget\Tabs + * @return \ipl\Web\Widget\Tabs */ protected function createTabs() { $tabs = $this->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 @@ <?php + // Icinga Reporting | (c) 2021 Icinga GmbH | GPLv2 namespace Icinga\Module\Reporting\Web\Widget; diff --git a/library/Reporting/Web/Widget/CoverPage.php b/library/Reporting/Web/Widget/CoverPage.php index 545ef6a..5b95a45 100644 --- a/library/Reporting/Web/Widget/CoverPage.php +++ b/library/Reporting/Web/Widget/CoverPage.php @@ -5,6 +5,7 @@ namespace Icinga\Module\Reporting\Web\Widget; use Icinga\Module\Reporting\Common\Macros; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; +use ipl\Web\Compat\StyleWithNonce; class CoverPage extends BaseHtmlElement { @@ -138,15 +139,22 @@ class CoverPage extends BaseHtmlElement protected function assemble() { if ($this->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 @@ <?php + // Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 namespace Icinga\Module\Reporting\Web\Widget; use Icinga\Module\Reporting\Common\Macros; -use Icinga\Module\Reporting\Database; +use Icinga\Module\Reporting\Model; use ipl\Html\BaseHtmlElement; -use ipl\Html\Html; -use ipl\Sql\Select; class Template extends BaseHtmlElement { - use Database; use Macros; protected $tag = 'div'; @@ -38,39 +36,35 @@ class Template extends BaseHtmlElement return sprintf('data:%s;base64,%s', $image['mime_type'], $image['content']); } - public static function fromDb($id) + /** + * Create template from the given model + * + * @param Model\Template $templateModel + * + * @return static + */ + public static function fromModel(Model\Template $templateModel): self { $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); + $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; } diff --git a/library/vendor/ipl/Html/src/FormElement/FileElement.php b/library/vendor/ipl/Html/src/FormElement/FileElement.php deleted file mode 100644 index 88aeb8c..0000000 --- a/library/vendor/ipl/Html/src/FormElement/FileElement.php +++ /dev/null @@ -1,15 +0,0 @@ -<?php - -namespace reportingipl\Html\FormElement; - -use ipl\Html\FormElement\InputElement; - -class FileElement extends InputElement -{ - protected $type = 'file'; - - public function setValue($value) - { - return $this; - } -} |