Files
Cloud-CMS/lib/XTR/ReportScheduleTask.php
Matt Batchelder 05ce0da296 Initial Upload
2025-12-02 10:32:59 -05:00

372 lines
14 KiB
PHP

<?php
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\XTR;
use Carbon\Carbon;
use Mpdf\Mpdf;
use Mpdf\Output\Destination;
use Slim\Views\Twig;
use Xibo\Entity\ReportResult;
use Xibo\Factory\MediaFactory;
use Xibo\Factory\NotificationFactory;
use Xibo\Factory\ReportScheduleFactory;
use Xibo\Factory\SavedReportFactory;
use Xibo\Factory\UserFactory;
use Xibo\Factory\UserGroupFactory;
use Xibo\Service\MediaService;
use Xibo\Service\ReportServiceInterface;
use Xibo\Support\Exception\InvalidArgumentException;
/**
* Class ReportScheduleTask
* @package Xibo\XTR
*/
class ReportScheduleTask implements TaskInterface
{
use TaskTrait;
/** @var Twig */
private $view;
/** @var MediaFactory */
private $mediaFactory;
/** @var SavedReportFactory */
private $savedReportFactory;
/** @var UserGroupFactory */
private $userGroupFactory;
/** @var UserFactory */
private $userFactory;
/** @var ReportScheduleFactory */
private $reportScheduleFactory;
/** @var ReportServiceInterface */
private $reportService;
/** @var NotificationFactory */
private $notificationFactory;
/** @inheritdoc */
public function setFactories($container)
{
$this->view = $container->get('view');
$this->userFactory = $container->get('userFactory');
$this->mediaFactory = $container->get('mediaFactory');
$this->savedReportFactory = $container->get('savedReportFactory');
$this->userGroupFactory = $container->get('userGroupFactory');
$this->reportScheduleFactory = $container->get('reportScheduleFactory');
$this->reportService = $container->get('reportService');
$this->notificationFactory = $container->get('notificationFactory');
return $this;
}
/** @inheritdoc */
public function run()
{
$this->runMessage = '# ' . __('Report schedule') . PHP_EOL . PHP_EOL;
// Long running task
set_time_limit(0);
$this->runReportSchedule();
}
/**
* Run report schedule
* @throws InvalidArgumentException
* @throws \Xibo\Support\Exception\ConfigurationException
* @throws \Xibo\Support\Exception\NotFoundException
*/
private function runReportSchedule()
{
$libraryFolder = $this->getConfig()->getSetting('LIBRARY_LOCATION');
// Make sure the library exists
MediaService::ensureLibraryExists($libraryFolder);
$reportSchedules = $this->reportScheduleFactory->query(null, ['isActive' => 1, 'disableUserCheck' => 1]);
// Get list of ReportSchedule
foreach ($reportSchedules as $reportSchedule) {
$cron = \Cron\CronExpression::factory($reportSchedule->schedule);
$nextRunDt = $cron->getNextRunDate(\DateTime::createFromFormat('U', $reportSchedule->lastRunDt))->format('U');
$now = Carbon::now()->format('U');
// if report start date is greater than now
// then dont run the report schedule
if ($reportSchedule->fromDt > $now) {
$this->log->debug('Report schedule start date is in future '. $reportSchedule->fromDt);
continue;
}
// if report end date is less than or equal to now
// then disable report schedule
if ($reportSchedule->toDt != 0 && $reportSchedule->toDt <= $now) {
$reportSchedule->message = 'Report schedule end date has passed';
$reportSchedule->isActive = 0;
}
if ($nextRunDt <= $now && $reportSchedule->isActive) {
// random run of report schedules
$skip = $this->skipReportRun($now, $nextRunDt);
if ($skip == true) {
continue;
}
// execute the report
$reportSchedule->previousRunDt = $reportSchedule->lastRunDt;
$reportSchedule->lastRunDt = Carbon::now()->format('U');
$this->log->debug('Last run date is updated to '. $reportSchedule->lastRunDt);
try {
// Get the generated saved as report name
$saveAs = $this->reportService->generateSavedReportName(
$reportSchedule->reportName,
$reportSchedule->filterCriteria
);
// Run the report to get results
// pass in the user who saved the report
$result = $this->reportService->runReport(
$reportSchedule->reportName,
$reportSchedule->filterCriteria,
$this->userFactory->getById($reportSchedule->userId)
);
$this->log->debug(__('Run report results: %s.', json_encode($result, JSON_PRETTY_PRINT)));
// Save the result in a json file
$fileName = tempnam($this->config->getSetting('LIBRARY_LOCATION') . '/temp/', 'reportschedule');
$out = fopen($fileName, 'w');
fwrite($out, json_encode($result));
fclose($out);
$savedReportFileName = 'rs_'.$reportSchedule->reportScheduleId. '_'. Carbon::now()->format('U');
// Create a ZIP file and add our temporary file
$zipName = $this->config->getSetting('LIBRARY_LOCATION') . 'savedreport/'.$savedReportFileName.'.zip';
$zip = new \ZipArchive();
$result = $zip->open($zipName, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
if ($result !== true) {
throw new InvalidArgumentException(__('Can\'t create ZIP. Error Code: %s', $result));
}
$zip->addFile($fileName, 'reportschedule.json');
$zip->close();
// Remove the JSON file
unlink($fileName);
// Save Saved report
$savedReport = $this->savedReportFactory->create(
$saveAs,
$reportSchedule->reportScheduleId,
Carbon::now()->format('U'),
$reportSchedule->userId,
$savedReportFileName.'.zip',
filesize($zipName),
md5_file($zipName)
);
$savedReport->save();
$this->createPdfAndNotification($reportSchedule, $savedReport);
// Add the last savedreport in Report Schedule
$this->log->debug('Last savedReportId in Report Schedule: '. $savedReport->savedReportId);
$reportSchedule->lastSavedReportId = $savedReport->savedReportId;
$reportSchedule->message = null;
} catch (\Exception $error) {
$reportSchedule->isActive = 0;
$reportSchedule->message = $error->getMessage();
$this->log->error('Error: ' . $error->getMessage());
}
}
// Finally save schedule report
$reportSchedule->save();
}
}
/**
* Create the PDF and save a notification
* @param $reportSchedule
* @param $savedReport
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
* @throws \Xibo\Support\Exception\GeneralException
*/
private function createPdfAndNotification($reportSchedule, $savedReport)
{
/* @var ReportResult $savedReportData */
$savedReportData = $this->reportService->getSavedReportResults(
$savedReport->savedReportId,
$reportSchedule->reportName
);
// Get the report config
$report = $this->reportService->getReportByName($reportSchedule->reportName);
if ($report->output_type == 'both' || $report->output_type == 'chart') {
$quickChartUrl = $this->config->getSetting('QUICK_CHART_URL');
if (!empty($quickChartUrl)) {
$quickChartUrl .= '/chart?width=1000&height=300&c=';
$chartScript = $this->reportService->getReportChartScript(
$savedReport->savedReportId,
$reportSchedule->reportName
);
// Replace " with ' for the quick chart URL
$src = $quickChartUrl . str_replace('"', '\'', $chartScript);
// If multiple charts needs to be displayed
$multipleCharts = [];
$chartScriptArray = json_decode($chartScript, true);
foreach ($chartScriptArray as $key => $chartData) {
$multipleCharts[$key] = $quickChartUrl . str_replace('"', '\'', json_encode($chartData));
}
} else {
$placeholder = __('Chart could not be drawn because the CMS has not been configured with a Quick Chart URL.');
}
}
if ($report->output_type == 'both' || $report->output_type == 'table') {
$tableData = $savedReportData->table;
}
// Get report email template
$emailTemplate = $this->reportService->getReportEmailTemplate($reportSchedule->reportName);
if (!empty($emailTemplate)) {
// Save PDF attachment
ob_start();
echo $this->view->fetch(
$emailTemplate,
[
'header' => $report->description,
'logo' => $this->config->uri('img/xibologo.png', true),
'title' => $savedReport->saveAs,
'metadata' => $savedReportData->metadata,
'tableData' => $tableData ?? null,
'src' => $src ?? null,
'multipleCharts' => $multipleCharts ?? null,
'placeholder' => $placeholder ?? null
]
);
$body = ob_get_contents();
ob_end_clean();
try {
$mpdf = new Mpdf([
'tempDir' => $this->config->getSetting('LIBRARY_LOCATION') . '/temp',
'orientation' => 'L',
'mode' => 'c',
'margin_left' => 20,
'margin_right' => 20,
'margin_top' => 20,
'margin_bottom' => 20,
'margin_header' => 5,
'margin_footer' => 15
]);
$mpdf->setFooter('Page {PAGENO}') ;
$mpdf->SetDisplayMode('fullpage');
$stylesheet = file_get_contents($this->config->uri('css/email-report.css', true));
$mpdf->WriteHTML($stylesheet, 1);
$mpdf->WriteHTML($body);
$mpdf->Output(
$this->config->getSetting('LIBRARY_LOCATION') . 'attachment/filename-'.$savedReport->savedReportId.'.pdf',
Destination::FILE
);
// Create email notification with attachment
$filters = json_decode($reportSchedule->filterCriteria, true);
$sendEmail = $filters['sendEmail'] ?? null;
$nonusers = $filters['nonusers'] ?? null;
if ($sendEmail) {
$notification = $this->notificationFactory->createEmpty();
$notification->subject = $report->description;
$notification->body = __('Attached please find the report for %s', $savedReport->saveAs);
$notification->createDt = Carbon::now()->format('U');
$notification->releaseDt = Carbon::now()->format('U');
$notification->isInterrupt = 0;
$notification->userId = $savedReport->userId; // event owner
$notification->filename = 'filename-'.$savedReport->savedReportId.'.pdf';
$notification->originalFileName = 'saved_report.pdf';
$notification->nonusers = $nonusers;
$notification->type = 'report';
// Get user group to create user notification
$notificationUser = $this->userFactory->getById($savedReport->userId);
$notification->assignUserGroup($this->userGroupFactory->getById($notificationUser->groupId));
$notification->save();
}
} catch (\Exception $error) {
$this->log->error($error->getMessage());
$this->runMessage .= $error->getMessage() . PHP_EOL . PHP_EOL;
}
}
}
private function skipReportRun($now, $nextRunDt)
{
$fourHoursInSeconds = 4 * 3600;
$threeHoursInSeconds = 3 * 3600;
$twoHoursInSeconds = 2 * 3600;
$oneHourInSeconds = 1 * 3600;
$diffFromNow = $now - $nextRunDt;
$range = 100;
$random = rand(1, $range);
if ($diffFromNow < $oneHourInSeconds) {
// don't run the report
if ($random <= 70) { // 70% chance of skipping
return true;
}
} elseif ($diffFromNow < $twoHoursInSeconds) {
// don't run the report
if ($random <= 50) { // 50% chance of skipping
return true;
}
} elseif ($diffFromNow < $threeHoursInSeconds) {
// don't run the report
if ($random <= 40) { // 40% chance of skipping
return true;
}
} elseif ($diffFromNow < $fourHoursInSeconds) {
// don't run the report
if ($random <= 25) { // 25% chance of skipping
return true;
}
}
return false;
}
}