Initial Upload
This commit is contained in:
326
lib/XTR/AnonymousUsageTask.php
Normal file
326
lib/XTR/AnonymousUsageTask.php
Normal file
@@ -0,0 +1,326 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2025 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 GuzzleHttp\Client;
|
||||
use Xibo\Helper\Environment;
|
||||
use Xibo\Storage\StorageServiceInterface;
|
||||
|
||||
/**
|
||||
* Collects anonymous usage stats
|
||||
*/
|
||||
class AnonymousUsageTask implements TaskInterface
|
||||
{
|
||||
use TaskTrait;
|
||||
|
||||
private readonly string $url;
|
||||
|
||||
private StorageServiceInterface $db;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->url = 'https://api.xibosignage.com/api/stats/usage';
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function setFactories($container)
|
||||
{
|
||||
$this->db = $container->get('store');
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function run()
|
||||
{
|
||||
$isCollectUsage = $this->getConfig()->getSetting('PHONE_HOME') == 1;
|
||||
if (!$isCollectUsage) {
|
||||
$this->appendRunMessage('Anonymous usage disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure we have a key
|
||||
$key = $this->getConfig()->getSetting('PHONE_HOME_KEY');
|
||||
if (empty($key)) {
|
||||
$key = bin2hex(random_bytes(16));
|
||||
|
||||
// Save it.
|
||||
$this->getConfig()->changeSetting('PHONE_HOME_KEY', $key);
|
||||
}
|
||||
|
||||
// Set PHONE_HOME_TIME to NOW.
|
||||
$this->getConfig()->changeSetting('PHONE_HOME_DATE', Carbon::now()->format('U'));
|
||||
|
||||
// Collect the data and report it.
|
||||
$data = [
|
||||
'id' => $key,
|
||||
'version' => Environment::$WEBSITE_VERSION_NAME,
|
||||
'accountId' => defined('ACCOUNT_ID') ? constant('ACCOUNT_ID') : null,
|
||||
];
|
||||
|
||||
// What type of install are we?
|
||||
$data['installType'] = 'custom';
|
||||
if (isset($_SERVER['INSTALL_TYPE'])) {
|
||||
$data['installType'] = $_SERVER['INSTALL_TYPE'];
|
||||
} else if ($this->getConfig()->getSetting('cloud_demo') !== null) {
|
||||
$data['installType'] = 'cloud';
|
||||
}
|
||||
|
||||
// General settings
|
||||
$data['calendarType'] = strtolower($this->getConfig()->getSetting('CALENDAR_TYPE'));
|
||||
$data['defaultLanguage'] = $this->getConfig()->getSetting('DEFAULT_LANGUAGE');
|
||||
$data['isDetectLanguage'] = $this->getConfig()->getSetting('DETECT_LANGUAGE') == 1 ? 1 : 0;
|
||||
|
||||
// Connectors
|
||||
$data['isSspConnector'] = $this->runQuery('SELECT `isEnabled` FROM `connectors` WHERE `className` = :name', [
|
||||
'name' => '\\Xibo\\Connector\\XiboSspConnector'
|
||||
]) ?? 0;
|
||||
$data['isDashboardConnector'] =
|
||||
$this->runQuery('SELECT `isEnabled` FROM `connectors` WHERE `className` = :name', [
|
||||
'name' => '\\Xibo\\Connector\\XiboDashboardConnector'
|
||||
]) ?? 0;
|
||||
|
||||
// Most recent date any user log in happened
|
||||
$data['dateSinceLastUserLogin'] = $this->getDateSinceLastUserLogin();
|
||||
|
||||
// Displays
|
||||
$data = array_merge($data, $this->displayStats());
|
||||
$data['countOfDisplays'] = $this->runQuery(
|
||||
'SELECT COUNT(*) AS countOf FROM `display` WHERE `lastaccessed` > :recently',
|
||||
[
|
||||
'recently' => Carbon::now()->subDays(7)->format('U'),
|
||||
]
|
||||
);
|
||||
$data['countOfDisplaysTotal'] = $this->runQuery('SELECT COUNT(*) AS countOf FROM `display`');
|
||||
$data['countOfDisplaysUnAuthorised'] =
|
||||
$this->runQuery('SELECT COUNT(*) AS countOf FROM `display` WHERE licensed = 0');
|
||||
$data['countOfDisplayGroups'] =
|
||||
$this->runQuery('SELECT COUNT(*) AS countOf FROM `displaygroup` WHERE isDisplaySpecific = 0');
|
||||
|
||||
// Users
|
||||
$data['countOfUsers'] = $this->runQuery('SELECT COUNT(*) AS countOf FROM `user`');
|
||||
$data['countOfUsersActiveInLastTwentyFour'] =
|
||||
$this->runQuery('SELECT COUNT(*) AS countOf FROM `user` WHERE `lastAccessed` > :recently', [
|
||||
'recently' => Carbon::now()->subHours(24)->format('Y-m-d H:i:s'),
|
||||
]);
|
||||
$data['countOfUserGroups'] =
|
||||
$this->runQuery('SELECT COUNT(*) AS countOf FROM `group` WHERE isUserSpecific = 0');
|
||||
$data['countOfUsersWithStatusDashboard'] =
|
||||
$this->runQuery('SELECT COUNT(*) AS countOf FROM `user` WHERE homePageId = \'statusdashboard.view\'');
|
||||
$data['countOfUsersWithIconDashboard'] =
|
||||
$this->runQuery('SELECT COUNT(*) AS countOf FROM `user` WHERE homePageId = \'icondashboard.view\'');
|
||||
$data['countOfUsersWithMediaDashboard'] =
|
||||
$this->runQuery('SELECT COUNT(*) AS countOf FROM `user` WHERE homePageId = \'mediamanager.view\'');
|
||||
$data['countOfUsersWithPlaylistDashboard'] =
|
||||
$this->runQuery('SELECT COUNT(*) AS countOf FROM `user` WHERE homePageId = \'playlistdashboard.view\'');
|
||||
|
||||
// Other objects
|
||||
$data['countOfFolders'] = $this->runQuery('SELECT COUNT(*) AS countOf FROM `folder`');
|
||||
$data['countOfLayouts'] = $this->runQuery('
|
||||
SELECT COUNT(*) AS countOf
|
||||
FROM `campaign`
|
||||
WHERE `isLayoutSpecific` = 1
|
||||
AND `campaignId` NOT IN (
|
||||
SELECT `lkcampaignlayout`.`campaignId`
|
||||
FROM `lkcampaignlayout`
|
||||
INNER JOIN `lktaglayout`
|
||||
ON `lktaglayout`.`layoutId` = `lkcampaignlayout`.`layoutId`
|
||||
INNER JOIN `tag`
|
||||
ON `lktaglayout`.tagId = `tag`.tagId
|
||||
WHERE `tag`.`tag` = \'template\'
|
||||
)
|
||||
');
|
||||
$data['countOfLayoutsWithPlaylists'] = $this->runQuery('
|
||||
SELECT COUNT(DISTINCT `region`.`layoutId`) AS countOf
|
||||
FROM `widget`
|
||||
INNER JOIN `playlist` ON `playlist`.`playlistId` = `widget`.`playlistId`
|
||||
INNER JOIN `region` ON `playlist`.`regionId` = `region`.`regionId`
|
||||
WHERE `widget`.`type` = \'subplaylist\'
|
||||
');
|
||||
$data['countOfAdCampaigns'] =
|
||||
$this->runQuery('
|
||||
SELECT COUNT(*) AS countOf
|
||||
FROM `campaign`
|
||||
WHERE `type` = \'ad\'
|
||||
AND `isLayoutSpecific` = 0
|
||||
');
|
||||
$data['countOfListCampaigns'] =
|
||||
$this->runQuery('
|
||||
SELECT COUNT(*) AS countOf
|
||||
FROM `campaign`
|
||||
WHERE `type` = \'list\'
|
||||
AND `isLayoutSpecific` = 0
|
||||
');
|
||||
$data['countOfMedia'] =
|
||||
$this->runQuery('SELECT COUNT(*) AS countOf FROM `media`');
|
||||
$data['countOfPlaylists'] =
|
||||
$this->runQuery('SELECT COUNT(*) AS countOf FROM `playlist` WHERE `regionId` IS NULL');
|
||||
$data['countOfDataSets'] =
|
||||
$this->runQuery('SELECT COUNT(*) AS countOf FROM `dataset`');
|
||||
$data['countOfRemoteDataSets'] =
|
||||
$this->runQuery('SELECT COUNT(*) AS countOf FROM `dataset` WHERE `isRemote` = 1');
|
||||
$data['countOfDataConnectorDataSets'] =
|
||||
$this->runQuery('SELECT COUNT(*) AS countOf FROM `dataset` WHERE `isRealTime` = 1');
|
||||
$data['countOfApplications'] =
|
||||
$this->runQuery('SELECT COUNT(*) AS countOf FROM `oauth_clients`');
|
||||
$data['countOfApplicationsUsingClientCredentials'] =
|
||||
$this->runQuery('SELECT COUNT(*) AS countOf FROM `oauth_clients` WHERE `clientCredentials` = 1');
|
||||
$data['countOfApplicationsUsingUserCode'] =
|
||||
$this->runQuery('SELECT COUNT(*) AS countOf FROM `oauth_clients` WHERE `authCode` = 1');
|
||||
$data['countOfScheduledReports'] =
|
||||
$this->runQuery('SELECT COUNT(*) AS countOf FROM `reportschedule`');
|
||||
$data['countOfSavedReports'] =
|
||||
$this->runQuery('SELECT COUNT(*) AS countOf FROM `saved_report`');
|
||||
|
||||
// Widgets
|
||||
$data['countOfImageWidgets'] =
|
||||
$this->runQuery('SELECT COUNT(*) AS countOf FROM `widget` WHERE `type` = \'image\'');
|
||||
$data['countOfVideoWidgets'] =
|
||||
$this->runQuery('SELECT COUNT(*) AS countOf FROM `widget` WHERE `type` = \'video\'');
|
||||
$data['countOfPdfWidgets'] =
|
||||
$this->runQuery('SELECT COUNT(*) AS countOf FROM `widget` WHERE `type` = \'pdf\'');
|
||||
$data['countOfEmbeddedWidgets'] =
|
||||
$this->runQuery('SELECT COUNT(*) AS countOf FROM `widget` WHERE `type` = \'embedded\'');
|
||||
$data['countOfCanvasWidgets'] =
|
||||
$this->runQuery('SELECT COUNT(*) AS countOf FROM `widget` WHERE `type` = \'global\'');
|
||||
|
||||
// Schedules
|
||||
$data['countOfSchedulesThisMonth'] = $this->runQuery('
|
||||
SELECT COUNT(*) AS countOf
|
||||
FROM `schedule`
|
||||
WHERE `fromDt` <= :toDt AND `toDt` > :fromDt
|
||||
', [
|
||||
'fromDt' => Carbon::now()->startOfMonth()->unix(),
|
||||
'toDt' => Carbon::now()->endOfMonth()->unix(),
|
||||
]);
|
||||
$data['countOfSyncSchedulesThisMonth'] = $this->runQuery('
|
||||
SELECT COUNT(*) AS countOf
|
||||
FROM `schedule`
|
||||
WHERE `fromDt` <= :toDt AND `toDt` > :fromDt
|
||||
AND `eventTypeId` = 9
|
||||
', [
|
||||
'fromDt' => Carbon::now()->startOfMonth()->unix(),
|
||||
'toDt' => Carbon::now()->endOfMonth()->unix(),
|
||||
]);
|
||||
$data['countOfAlwaysSchedulesThisMonth'] = $this->runQuery('
|
||||
SELECT COUNT(*) AS countOf
|
||||
FROM `schedule`
|
||||
INNER JOIN `daypart` ON `daypart`.dayPartId = `schedule`.`dayPartId`
|
||||
WHERE `daypart`.`isAlways` = 1
|
||||
');
|
||||
$data['countOfRecurringSchedules'] =
|
||||
$this->runQuery('SELECT COUNT(*) AS countOf FROM `schedule` WHERE IFNULL(recurrence_type, \'\') <> \'\'');
|
||||
$data['countOfSchedulesWithCriteria'] =
|
||||
$this->runQuery('SELECT COUNT(DISTINCT `eventId`) AS countOf FROM `schedule_criteria`');
|
||||
$data['countOfDayParts'] =
|
||||
$this->runQuery('SELECT COUNT(*) AS countOf FROM `daypart`');
|
||||
|
||||
// Finished collecting, send.
|
||||
$this->getLogger()->debug('run: sending stats ' . json_encode($data));
|
||||
|
||||
try {
|
||||
(new Client())->post(
|
||||
$this->url,
|
||||
$this->getConfig()->getGuzzleProxy([
|
||||
'json' => $data,
|
||||
])
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
$this->appendRunMessage('Unable to send stats.');
|
||||
$this->log->error('run: stats send failed, e=' . $e->getMessage());
|
||||
}
|
||||
|
||||
$this->appendRunMessage('Completed');
|
||||
}
|
||||
|
||||
private function displayStats(): array
|
||||
{
|
||||
// Retrieve number of displays
|
||||
$stats = $this->db->select('
|
||||
SELECT client_type, COUNT(*) AS cnt
|
||||
FROM `display`
|
||||
WHERE licensed = 1
|
||||
GROUP BY client_type
|
||||
', []);
|
||||
|
||||
$counts = [
|
||||
'total' => 0,
|
||||
'android' => 0,
|
||||
'windows' => 0,
|
||||
'linux' => 0,
|
||||
'lg' => 0,
|
||||
'sssp' => 0,
|
||||
'chromeOS' => 0,
|
||||
];
|
||||
foreach ($stats as $stat) {
|
||||
$counts['total'] += intval($stat['cnt']);
|
||||
$counts[$stat['client_type']] += intval($stat['cnt']);
|
||||
}
|
||||
|
||||
return [
|
||||
'countOfDisplaysAuthorised' => $counts['total'],
|
||||
'countOfAndroid' => $counts['android'],
|
||||
'countOfLinux' => $counts['linux'],
|
||||
'countOfWebos' => $counts['lg'],
|
||||
'countOfWindows' => $counts['windows'],
|
||||
'countOfTizen' => $counts['sssp'],
|
||||
'countOfChromeOS' => $counts['chromeOS'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a query and return the value of a property
|
||||
* @param string $sql
|
||||
* @param string $property
|
||||
* @param array $params
|
||||
* @return string|null
|
||||
*/
|
||||
private function runQuery(string $sql, array $params = [], string $property = 'countOf'): ?string
|
||||
{
|
||||
try {
|
||||
$record = $this->db->select($sql, $params);
|
||||
return $record[0][$property] ?? null;
|
||||
} catch (\PDOException $PDOException) {
|
||||
$this->getLogger()->debug('runQuery: error returning specific stat, e: ' . $PDOException->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the most recent user login timestamp converted to UTC
|
||||
* @return string|null
|
||||
*/
|
||||
private function getDateSinceLastUserLogin(): string|null
|
||||
{
|
||||
$cmsTimezone = $this->getConfig()->getSetting('defaultTimezone');
|
||||
$latestUserLoginDate = $this->runQuery(
|
||||
'SELECT MAX(`lastAccessed`) AS lastAccessed FROM `user`',
|
||||
[],
|
||||
'lastAccessed'
|
||||
);
|
||||
|
||||
return $latestUserLoginDate
|
||||
? Carbon::parse($latestUserLoginDate, $cmsTimezone)->setTimezone('UTC')->timestamp
|
||||
: null;
|
||||
}
|
||||
}
|
||||
274
lib/XTR/AuditLogArchiveTask.php
Normal file
274
lib/XTR/AuditLogArchiveTask.php
Normal file
@@ -0,0 +1,274 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2025 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 Xibo\Entity\User;
|
||||
use Xibo\Factory\MediaFactory;
|
||||
use Xibo\Factory\UserFactory;
|
||||
use Xibo\Helper\DateFormatHelper;
|
||||
use Xibo\Support\Exception\GeneralException;
|
||||
use Xibo\Support\Exception\InvalidArgumentException;
|
||||
use Xibo\Support\Exception\NotFoundException;
|
||||
use Xibo\Support\Exception\TaskRunException;
|
||||
|
||||
/**
|
||||
* Class StatsArchiveTask
|
||||
* @package Xibo\XTR
|
||||
*/
|
||||
class AuditLogArchiveTask implements TaskInterface
|
||||
{
|
||||
use TaskTrait;
|
||||
|
||||
/** @var User */
|
||||
private $archiveOwner;
|
||||
|
||||
/** @var UserFactory */
|
||||
private $userFactory;
|
||||
|
||||
/** @var MediaFactory */
|
||||
private $mediaFactory;
|
||||
|
||||
/** @var \Xibo\Helper\SanitizerService */
|
||||
private $sanitizerService;
|
||||
|
||||
/** @inheritdoc */
|
||||
public function setFactories($container)
|
||||
{
|
||||
$this->mediaFactory = $container->get('mediaFactory');
|
||||
$this->userFactory = $container->get('userFactory');
|
||||
$this->sanitizerService = $container->get('sanitizerService');
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @throws \Xibo\Support\Exception\GeneralException
|
||||
*/
|
||||
public function run()
|
||||
{
|
||||
$maxPeriods = intval($this->getOption('maxPeriods', 1));
|
||||
$maxAge = Carbon::now()
|
||||
->subMonths(intval($this->getOption('maxAgeMonths', 1)))
|
||||
->startOfDay();
|
||||
|
||||
$this->setArchiveOwner();
|
||||
|
||||
// Delete or Archive?
|
||||
if ($this->getOption('deleteInstead', 1) == 1) {
|
||||
$this->appendRunMessage('# ' . __('AuditLog Delete'));
|
||||
$this->appendRunMessage('maxAge: ' . $maxAge->format(DateFormatHelper::getSystemFormat()));
|
||||
|
||||
// Delete all audit log messages older than the configured number of months
|
||||
// use a prepared statement for this, and delete the records in a loop
|
||||
$deleteStatement = $this->store->getConnection()->prepare('
|
||||
DELETE FROM `auditlog`
|
||||
WHERE `logDate` < :logDate
|
||||
LIMIT :limit
|
||||
');
|
||||
|
||||
// Convert to a simple type so that we can pass by reference to bindParam.
|
||||
// Load in other options for deleting
|
||||
$maxage = $maxAge->format('U');
|
||||
$limit = intval($this->getOption('deleteLimit', 1000));
|
||||
$maxAttempts = intval($this->getOption('maxDeleteAttempts', -1));
|
||||
$deleteSleep = intval($this->getOption('deleteSleep', 5));
|
||||
|
||||
// Bind params
|
||||
$deleteStatement->bindParam(':logDate', $maxage, \PDO::PARAM_STR);
|
||||
$deleteStatement->bindParam(':limit', $limit, \PDO::PARAM_INT);
|
||||
|
||||
try {
|
||||
$i = 0;
|
||||
$rows = 1;
|
||||
|
||||
while ($rows > 0) {
|
||||
$i++;
|
||||
|
||||
// Run delete statement
|
||||
$deleteStatement->execute();
|
||||
|
||||
// Find out how many rows we've deleted
|
||||
$rows = $deleteStatement->rowCount();
|
||||
|
||||
// We shouldn't be in a transaction, but commit anyway just in case
|
||||
$this->store->commitIfNecessary();
|
||||
|
||||
// Give SQL time to recover
|
||||
if ($rows > 0) {
|
||||
$this->log->debug('Archive delete effected ' . $rows . ' rows, sleeping.');
|
||||
sleep($deleteSleep);
|
||||
}
|
||||
|
||||
// Break if we've exceeded the maximum attempts, assuming that has been provided
|
||||
if ($maxAttempts > -1 && $i >= $maxAttempts) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (\PDOException $e) {
|
||||
$this->log->error($e->getMessage());
|
||||
throw new GeneralException('Archive rows cannot be deleted.');
|
||||
}
|
||||
} else {
|
||||
$this->appendRunMessage('# ' . __('AuditLog Archive'));
|
||||
$this->appendRunMessage('maxAge: ' . $maxAge->format(DateFormatHelper::getSystemFormat()));
|
||||
|
||||
// Get the earliest
|
||||
$earliestDate = $this->store->select('
|
||||
SELECT MIN(logDate) AS minDate FROM `auditlog` WHERE logDate < :logDate
|
||||
', [
|
||||
'logDate' => $maxAge->format('U')
|
||||
]);
|
||||
|
||||
if (count($earliestDate) <= 0 || $earliestDate[0]['minDate'] === null) {
|
||||
$this->appendRunMessage(__('Nothing to archive'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Take the earliest date and roll forward until the max age
|
||||
$earliestDate = Carbon::createFromTimestamp($earliestDate[0]['minDate'])->startOfDay();
|
||||
$i = 0;
|
||||
|
||||
// We only archive up until the max age, leaving newer records alone.
|
||||
while ($earliestDate < $maxAge && $i <= $maxPeriods) {
|
||||
$i++;
|
||||
|
||||
$this->log->debug('Running archive number ' . $i);
|
||||
|
||||
// Push forward
|
||||
$fromDt = $earliestDate->copy();
|
||||
$earliestDate->addMonth();
|
||||
|
||||
$this->exportAuditLogToLibrary($fromDt, $earliestDate);
|
||||
$this->store->commitIfNecessary();
|
||||
}
|
||||
}
|
||||
|
||||
$this->runMessage .= __('Done') . PHP_EOL . PHP_EOL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export stats to the library
|
||||
* @param Carbon $fromDt
|
||||
* @param Carbon $toDt
|
||||
* @throws \Xibo\Support\Exception\GeneralException
|
||||
*/
|
||||
private function exportAuditLogToLibrary($fromDt, $toDt)
|
||||
{
|
||||
$this->runMessage .= ' - ' . $fromDt . ' / ' . $toDt . PHP_EOL;
|
||||
|
||||
$sql = '
|
||||
SELECT *
|
||||
FROM `auditlog`
|
||||
WHERE logDate >= :fromDt
|
||||
AND logDate < :toDt
|
||||
';
|
||||
|
||||
$params = [
|
||||
'fromDt' => $fromDt->format('U'),
|
||||
'toDt' => $toDt->format('U')
|
||||
];
|
||||
|
||||
$sql .= ' ORDER BY 1 ';
|
||||
|
||||
$records = $this->store->select($sql, $params);
|
||||
|
||||
if (count($records) <= 0) {
|
||||
$this->runMessage .= __('No audit log found for these dates') . PHP_EOL;
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a temporary file for this
|
||||
$fileName = $this->config->getSetting('LIBRARY_LOCATION') . 'temp/auditlog.csv';
|
||||
|
||||
$out = fopen($fileName, 'w');
|
||||
fputcsv($out, ['logId', 'logDate', 'userId', 'message', 'entity', 'entityId', 'objectAfter']);
|
||||
|
||||
// Do some post-processing
|
||||
foreach ($records as $row) {
|
||||
$sanitizedRow = $this->getSanitizer($row);
|
||||
// Read the columns
|
||||
fputcsv($out, [
|
||||
$sanitizedRow->getInt('logId'),
|
||||
$sanitizedRow->getInt('logDate'),
|
||||
$sanitizedRow->getInt('userId'),
|
||||
$sanitizedRow->getString('message'),
|
||||
$sanitizedRow->getString('entity'),
|
||||
$sanitizedRow->getInt('entityId'),
|
||||
$sanitizedRow->getString('objectAfter')
|
||||
]);
|
||||
}
|
||||
|
||||
fclose($out);
|
||||
|
||||
$zip = new \ZipArchive();
|
||||
$result = $zip->open($fileName . '.zip', \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
|
||||
if ($result !== true) {
|
||||
throw new InvalidArgumentException(__('Can\'t create ZIP. Error Code: %s', $result));
|
||||
}
|
||||
|
||||
$zip->addFile($fileName, 'auditlog.csv');
|
||||
$zip->close();
|
||||
|
||||
// Remove the CSV file
|
||||
unlink($fileName);
|
||||
|
||||
// Upload to the library
|
||||
$media = $this->mediaFactory->create(
|
||||
__('AuditLog Export %s to %s', $fromDt->format('Y-m-d'), $toDt->format('Y-m-d')),
|
||||
'auditlog.csv.zip',
|
||||
'genericfile',
|
||||
$this->archiveOwner->getId()
|
||||
);
|
||||
$media->save();
|
||||
|
||||
// Delete the logs
|
||||
$this->store->update('DELETE FROM `auditlog` WHERE logDate >= :fromDt AND logDate < :toDt', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the archive owner
|
||||
* @throws TaskRunException
|
||||
*/
|
||||
private function setArchiveOwner()
|
||||
{
|
||||
$archiveOwner = $this->getOption('archiveOwner', null);
|
||||
|
||||
if ($archiveOwner == null) {
|
||||
$admins = $this->userFactory->getSuperAdmins();
|
||||
|
||||
if (count($admins) <= 0)
|
||||
throw new TaskRunException(__('No super admins to use as the archive owner, please set one in the configuration.'));
|
||||
|
||||
$this->archiveOwner = $admins[0];
|
||||
|
||||
} else {
|
||||
try {
|
||||
$this->archiveOwner = $this->userFactory->getByName($archiveOwner);
|
||||
} catch (NotFoundException $e) {
|
||||
throw new TaskRunException(__('Archive Owner not found'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
330
lib/XTR/CampaignSchedulerTask.php
Normal file
330
lib/XTR/CampaignSchedulerTask.php
Normal file
@@ -0,0 +1,330 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2023 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 GeoJson\Feature\FeatureCollection;
|
||||
use GeoJson\GeoJson;
|
||||
use Xibo\Entity\DayPart;
|
||||
use Xibo\Entity\Schedule;
|
||||
|
||||
/**
|
||||
* Campaign Scheduler task
|
||||
* This should be run once per hour to create interrupt schedules for applicable advertising campaigns.
|
||||
* The schedules will be created for the following hour.
|
||||
*/
|
||||
class CampaignSchedulerTask implements TaskInterface
|
||||
{
|
||||
use TaskTrait;
|
||||
|
||||
/** @var \Xibo\Factory\CampaignFactory */
|
||||
private $campaignFactory;
|
||||
|
||||
/** @var \Xibo\Factory\ScheduleFactory */
|
||||
private $scheduleFactory;
|
||||
|
||||
/** @var \Xibo\Factory\DayPartFactory */
|
||||
private $dayPartFactory;
|
||||
|
||||
/** @var \Xibo\Factory\DisplayGroupFactory */
|
||||
private $displayGroupFactory;
|
||||
|
||||
/** @var \Xibo\Factory\DisplayFactory */
|
||||
private $displayFactory;
|
||||
|
||||
/** @var \Xibo\Service\DisplayNotifyServiceInterface */
|
||||
private $displayNotifyService;
|
||||
|
||||
/** @var \Xibo\Entity\DayPart */
|
||||
private $customDayPart = null;
|
||||
|
||||
/** @inheritDoc */
|
||||
public function setFactories($container)
|
||||
{
|
||||
$this->campaignFactory = $container->get('campaignFactory');
|
||||
$this->scheduleFactory = $container->get('scheduleFactory');
|
||||
$this->dayPartFactory = $container->get('dayPartFactory');
|
||||
$this->displayGroupFactory = $container->get('displayGroupFactory');
|
||||
$this->displayFactory = $container->get('displayFactory');
|
||||
$this->displayNotifyService = $container->get('displayNotifyService');
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function run()
|
||||
{
|
||||
$nextHour = Carbon::now()->startOfHour()->addHour();
|
||||
$nextHourEnd = $nextHour->copy()->addHour();
|
||||
$activeCampaigns = $this->campaignFactory->query(null, [
|
||||
'disableUserCheck' => 1,
|
||||
'type' => 'ad',
|
||||
'startDt' => $nextHour->unix(),
|
||||
'endDt' => $nextHour->unix(),
|
||||
]);
|
||||
|
||||
// We will need to notify some displays at the end.
|
||||
$notifyDisplayGroupIds = [];
|
||||
$campaignsProcessed = 0;
|
||||
$campaignsScheduled = 0;
|
||||
|
||||
// See what we can schedule for each one.
|
||||
foreach ($activeCampaigns as $campaign) {
|
||||
$campaignsProcessed++;
|
||||
try {
|
||||
$this->log->debug('campaignSchedulerTask: active campaign found, id: ' . $campaign->campaignId);
|
||||
|
||||
// What schedules should I create?
|
||||
$activeLayouts = [];
|
||||
foreach ($campaign->loadLayouts() as $layout) {
|
||||
$this->log->debug('campaignSchedulerTask: layout assignment: ' . $layout->layoutId);
|
||||
|
||||
// Is the layout value
|
||||
if ($layout->duration <= 0) {
|
||||
$this->log->error('campaignSchedulerTask: layout without duration');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Are we on an active day of the week?
|
||||
if (!in_array($nextHour->dayOfWeekIso, explode(',', $layout->daysOfWeek))) {
|
||||
$this->log->debug('campaignSchedulerTask: day of week not active');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Is this on an active day part?
|
||||
if ($layout->dayPartId != 0) {
|
||||
$this->log->debug('campaignSchedulerTask: dayPartId set, testing');
|
||||
|
||||
// Check the day part
|
||||
try {
|
||||
$dayPart = $this->dayPartFactory->getById($layout->dayPartId);
|
||||
$dayPart->adjustForDate($nextHour);
|
||||
} catch (\Exception $exception) {
|
||||
$this->log->debug('campaignSchedulerTask: invalid dayPart, e = '
|
||||
. $exception->getMessage());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Is this day part active
|
||||
if (!$nextHour->betweenIncluded($dayPart->adjustedStart, $dayPart->adjustedEnd)) {
|
||||
$this->log->debug('campaignSchedulerTask: dayPart not active');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$this->log->debug('campaignSchedulerTask: layout is valid and needs a schedule');
|
||||
$activeLayouts[] = $layout;
|
||||
}
|
||||
|
||||
$countActiveLayouts = count($activeLayouts);
|
||||
$this->log->debug('campaignSchedulerTask: there are ' . $countActiveLayouts . ' active layouts');
|
||||
|
||||
if ($countActiveLayouts <= 0) {
|
||||
$this->log->debug('campaignSchedulerTask: no active layouts for campaign');
|
||||
continue;
|
||||
}
|
||||
|
||||
// The campaign is active
|
||||
// Display groups
|
||||
$displayGroups = [];
|
||||
$allDisplays = [];
|
||||
$countDisplays = 0;
|
||||
$costPerPlay = 0;
|
||||
$impressionsPerPlay = 0;
|
||||
|
||||
// First pass uses only logged in displays from the display group
|
||||
foreach ($campaign->loadDisplayGroupIds() as $displayGroupId) {
|
||||
$displayGroups[] = $this->displayGroupFactory->getById($displayGroupId);
|
||||
|
||||
// Record ids to notify
|
||||
if (!in_array($displayGroupId, $notifyDisplayGroupIds)) {
|
||||
$notifyDisplayGroupIds[] = $displayGroupId;
|
||||
}
|
||||
|
||||
foreach ($this->displayFactory->getByDisplayGroupId($displayGroupId) as $display) {
|
||||
// Keep track of these in case we resolve 0 logged in displays
|
||||
$allDisplays[] = $display;
|
||||
if ($display->licensed === 1 && $display->loggedIn === 1) {
|
||||
$countDisplays++;
|
||||
$costPerPlay += $display->costPerPlay;
|
||||
$impressionsPerPlay += $display->impressionsPerPlay;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->log->debug('campaignSchedulerTask: campaign has ' . $countDisplays
|
||||
. ' logged in and authorised displays');
|
||||
|
||||
// If there are 0 displays, then process again ignoring the logged in status.
|
||||
if ($countDisplays <= 0) {
|
||||
$this->log->debug('campaignSchedulerTask: processing displays again ignoring logged in status');
|
||||
|
||||
foreach ($allDisplays as $display) {
|
||||
if ($display->licensed === 1) {
|
||||
$countDisplays++;
|
||||
$costPerPlay += $display->costPerPlay;
|
||||
$impressionsPerPlay += $display->impressionsPerPlay;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->log->debug('campaignSchedulerTask: campaign has ' . $countDisplays
|
||||
. ' authorised displays');
|
||||
|
||||
if ($countDisplays <= 0) {
|
||||
$this->log->debug('campaignSchedulerTask: skipping campaign due to no authorised displays');
|
||||
continue;
|
||||
}
|
||||
|
||||
// What is the total amount of time we want this campaign to play in this hour period?
|
||||
// We work out how much we should have played vs how much we have played
|
||||
$progress = $campaign->getProgress($nextHour->copy());
|
||||
|
||||
// A simple assessment of how much of the target we need in this hour period (we assume the campaign
|
||||
// will play for 24 hours a day and that adjustments to later scheduling will solve any underplay)
|
||||
$targetNeededPerDay = $progress->targetPerDay / 24;
|
||||
|
||||
// If we are more than 5% ahead of where we should be, or we are at 100% already, then don't
|
||||
// schedule anything else
|
||||
if ($progress->progressTarget >= 100) {
|
||||
$this->log->debug('campaignSchedulerTask: campaign has completed, skipping');
|
||||
continue;
|
||||
} else if ($progress->progressTime > 0
|
||||
&& ($progress->progressTime - $progress->progressTarget + 5) <= 0
|
||||
) {
|
||||
$this->log->debug('campaignSchedulerTask: campaign is 5% or more ahead of schedule, skipping');
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($progress->progressTime > 0 && $progress->progressTarget > 0) {
|
||||
// If we're behind, then increase our play rate accordingly
|
||||
$ratio = $progress->progressTime / $progress->progressTarget;
|
||||
$targetNeededPerDay = $targetNeededPerDay * $ratio;
|
||||
|
||||
$this->log->debug('campaignSchedulerTask: targetNeededPerDay is ' . $targetNeededPerDay
|
||||
. ', adjusted by ' . $ratio);
|
||||
}
|
||||
|
||||
// Spread across the layouts
|
||||
$targetNeededPerLayout = $targetNeededPerDay / $countActiveLayouts;
|
||||
|
||||
// Modify the target depending on what units it is expressed in
|
||||
// This also caters for spreading the target across the active displays because the
|
||||
// cost/impressions/displays are sums.
|
||||
if ($campaign->targetType === 'budget') {
|
||||
$playsNeededPerLayout = $targetNeededPerLayout / $costPerPlay;
|
||||
} else if ($campaign->targetType === 'imp') {
|
||||
$playsNeededPerLayout = $targetNeededPerLayout / $impressionsPerPlay;
|
||||
} else {
|
||||
$playsNeededPerLayout = $targetNeededPerLayout / $countDisplays;
|
||||
}
|
||||
|
||||
// Take the ceiling because we can't do part plays
|
||||
$playsNeededPerLayout = intval(ceil($playsNeededPerLayout));
|
||||
|
||||
$this->log->debug('campaignSchedulerTask: targetNeededPerLayout is ' . $targetNeededPerLayout
|
||||
. ', targetType: ' . $campaign->targetType
|
||||
. ', playsNeededPerLayout: ' . $playsNeededPerLayout
|
||||
. ', there are ' . $countDisplays . ' displays.');
|
||||
|
||||
foreach ($activeLayouts as $layout) {
|
||||
// We are on an active day of the week and within an active day part
|
||||
// create a scheduled event for all displays assigned.
|
||||
// and for each geo fence
|
||||
// Create our schedule
|
||||
$schedule = $this->scheduleFactory->createEmpty();
|
||||
$schedule->setCampaignFactory($this->campaignFactory);
|
||||
|
||||
// Date
|
||||
$schedule->fromDt = $nextHour->unix();
|
||||
$schedule->toDt = $nextHourEnd->unix();
|
||||
|
||||
// Displays
|
||||
foreach ($displayGroups as $displayGroup) {
|
||||
$schedule->assignDisplayGroup($displayGroup);
|
||||
}
|
||||
|
||||
// Interrupt Layout
|
||||
$schedule->eventTypeId = Schedule::$INTERRUPT_EVENT;
|
||||
$schedule->userId = $campaign->ownerId;
|
||||
$schedule->parentCampaignId = $campaign->campaignId;
|
||||
$schedule->campaignId = $layout->layoutCampaignId;
|
||||
$schedule->displayOrder = 0;
|
||||
$schedule->isPriority = 0;
|
||||
$schedule->dayPartId = $this->getCustomDayPart()->dayPartId;
|
||||
$schedule->isGeoAware = 0;
|
||||
$schedule->syncTimezone = 0;
|
||||
$schedule->syncEvent = 0;
|
||||
|
||||
// We cap SOV at 3600
|
||||
$schedule->shareOfVoice = min($playsNeededPerLayout * $layout->duration, 3600);
|
||||
$schedule->maxPlaysPerHour = $playsNeededPerLayout;
|
||||
|
||||
// Do we have a geofence? (geo schedules do not count against totalSovAvailable)
|
||||
if (!empty($layout->geoFence)) {
|
||||
$this->log->debug('campaignSchedulerTask: layout has a geo fence');
|
||||
$schedule->isGeoAware = 1;
|
||||
|
||||
// Get some GeoJSON and pull out each Feature (create a schedule for each one)
|
||||
$geoJson = GeoJson::jsonUnserialize(json_decode($layout->geoFence, true));
|
||||
if ($geoJson instanceof FeatureCollection) {
|
||||
$this->log->debug('campaignSchedulerTask: layout has multiple features');
|
||||
foreach ($geoJson->getFeatures() as $feature) {
|
||||
$schedule->geoLocation = json_encode($feature->jsonSerialize());
|
||||
$schedule->save(['notify' => false]);
|
||||
|
||||
// Clone a new one
|
||||
$schedule = clone $schedule;
|
||||
}
|
||||
} else {
|
||||
$schedule->geoLocation = $layout->geoFence;
|
||||
$schedule->save(['notify' => false]);
|
||||
}
|
||||
} else {
|
||||
$schedule->save(['notify' => false]);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle notify
|
||||
foreach ($notifyDisplayGroupIds as $displayGroupId) {
|
||||
$this->displayNotifyService->notifyByDisplayGroupId($displayGroupId);
|
||||
}
|
||||
|
||||
$campaignsScheduled++;
|
||||
} catch (\Exception $exception) {
|
||||
$this->log->error('campaignSchedulerTask: ' . $exception->getMessage());
|
||||
$this->appendRunMessage($campaign->campaign . ' failed');
|
||||
}
|
||||
}
|
||||
|
||||
$this->appendRunMessage($campaignsProcessed . ' campaigns processed, of which ' . $campaignsScheduled
|
||||
. ' were scheduled. Skipped ' . ($campaignsProcessed - $campaignsScheduled) . ' for various reasons');
|
||||
}
|
||||
|
||||
private function getCustomDayPart(): DayPart
|
||||
{
|
||||
if ($this->customDayPart === null) {
|
||||
$this->customDayPart = $this->dayPartFactory->getCustomDayPart();
|
||||
}
|
||||
return $this->customDayPart;
|
||||
}
|
||||
}
|
||||
85
lib/XTR/ClearCachedMediaDataTask.php
Normal file
85
lib/XTR/ClearCachedMediaDataTask.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
/**
|
||||
* Copyright (C) 2019 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - http://www.xibo.org.uk
|
||||
*
|
||||
* 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 Xibo\Factory\MediaFactory;
|
||||
use Xibo\Helper\DateFormatHelper;
|
||||
|
||||
/**
|
||||
* Class ClearCachedMediaDataTask
|
||||
* @package Xibo\XTR
|
||||
*/
|
||||
class ClearCachedMediaDataTask implements TaskInterface
|
||||
{
|
||||
use TaskTrait;
|
||||
|
||||
/** @var MediaFactory */
|
||||
private $mediaFactory;
|
||||
|
||||
/** @inheritdoc */
|
||||
public function setFactories($container)
|
||||
{
|
||||
$this->mediaFactory = $container->get('mediaFactory');
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function run()
|
||||
{
|
||||
$this->runMessage = '# ' . __('Clear Cached Media Data') . PHP_EOL . PHP_EOL;
|
||||
|
||||
// Long running task
|
||||
set_time_limit(0);
|
||||
|
||||
$this->runClearCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates all md5/filesizes to empty for any image/module file created since 2.2.0 release date
|
||||
*/
|
||||
private function runClearCache()
|
||||
{
|
||||
|
||||
$cutOffDate = Carbon::createFromFormat('Y-m-d', '2019-11-26')->startOfDay()->format(DateFormatHelper::getSystemFormat());
|
||||
|
||||
// Update the MD5 and fileSize to null
|
||||
$this->store->update('UPDATE `media` SET md5 = :md5, fileSize = :fileSize, modifiedDt = :modifiedDt
|
||||
WHERE (`media`.type = \'image\' OR (`media`.type = \'module\' AND `media`.moduleSystemFile = 0)) AND createdDt >= :createdDt ', [
|
||||
'fileSize' => null,
|
||||
'md5' => null,
|
||||
'createdDt' => $cutOffDate,
|
||||
'modifiedDt' => date(DateFormatHelper::getSystemFormat())
|
||||
|
||||
]);
|
||||
|
||||
// Disable the task
|
||||
$this->appendRunMessage('# Disabling task.');
|
||||
|
||||
$this->getTask()->isActive = 0;
|
||||
$this->getTask()->save();
|
||||
|
||||
$this->appendRunMessage(__('Done.'. PHP_EOL));
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
129
lib/XTR/DataSetConvertTask.php
Normal file
129
lib/XTR/DataSetConvertTask.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
/**
|
||||
* Copyright (C) 2020 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - http://www.xibo.org.uk
|
||||
*
|
||||
* 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 Xibo\Entity\DataSet;
|
||||
use Xibo\Factory\DataSetFactory;
|
||||
use Xibo\Support\Exception\GeneralException;
|
||||
|
||||
/**
|
||||
* Class DataSetConvertTask
|
||||
* @package Xibo\XTR
|
||||
*/
|
||||
class DataSetConvertTask implements TaskInterface
|
||||
{
|
||||
use TaskTrait;
|
||||
|
||||
/** @var DataSetFactory */
|
||||
private $dataSetFactory;
|
||||
|
||||
/** @inheritdoc */
|
||||
public function setFactories($container)
|
||||
{
|
||||
$this->dataSetFactory = $container->get('dataSetFactory');
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function run()
|
||||
{
|
||||
// Protect against us having run before
|
||||
if ($this->store->exists('SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = :schema AND TABLE_NAME = :name', [
|
||||
'schema' => $_SERVER['MYSQL_DATABASE'],
|
||||
'name' => 'datasetdata'
|
||||
])) {
|
||||
|
||||
// Get all DataSets
|
||||
foreach ($this->dataSetFactory->query() as $dataSet) {
|
||||
/* @var \Xibo\Entity\DataSet $dataSet */
|
||||
|
||||
// Rebuild the data table
|
||||
$dataSet->rebuild();
|
||||
|
||||
// Load the existing data from datasetdata
|
||||
foreach (self::getExistingData($dataSet) as $row) {
|
||||
$dataSet->addRow($row);
|
||||
}
|
||||
}
|
||||
|
||||
// Drop data set data
|
||||
$this->store->update('DROP TABLE `datasetdata`;', []);
|
||||
}
|
||||
|
||||
// Disable the task
|
||||
$this->getTask()->isActive = 0;
|
||||
|
||||
$this->appendRunMessage('Conversion Completed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Data Set Results
|
||||
* @param DataSet $dataSet
|
||||
* @return array
|
||||
* @throws GeneralException
|
||||
*/
|
||||
public function getExistingData($dataSet)
|
||||
{
|
||||
$dbh = $this->store->getConnection();
|
||||
$params = array('dataSetId' => $dataSet->dataSetId);
|
||||
|
||||
$selectSQL = '';
|
||||
$outerSelect = '';
|
||||
|
||||
foreach ($dataSet->getColumn() as $col) {
|
||||
/* @var \Xibo\Entity\DataSetColumn $col */
|
||||
if ($col->dataSetColumnTypeId != 1)
|
||||
continue;
|
||||
|
||||
$selectSQL .= sprintf("MAX(CASE WHEN DataSetColumnID = %d THEN `Value` ELSE null END) AS '%s', ", $col->dataSetColumnId, $col->heading);
|
||||
$outerSelect .= sprintf(' `%s`,', $col->heading);
|
||||
}
|
||||
|
||||
$outerSelect = rtrim($outerSelect, ',');
|
||||
|
||||
// We are ready to build the select and from part of the SQL
|
||||
$SQL = "SELECT $outerSelect ";
|
||||
$SQL .= " FROM ( ";
|
||||
$SQL .= " SELECT $outerSelect ,";
|
||||
$SQL .= " RowNumber ";
|
||||
$SQL .= " FROM ( ";
|
||||
$SQL .= " SELECT $selectSQL ";
|
||||
$SQL .= " RowNumber ";
|
||||
$SQL .= " FROM (";
|
||||
$SQL .= " SELECT datasetcolumn.DataSetColumnID, datasetdata.RowNumber, datasetdata.`Value` ";
|
||||
$SQL .= " FROM datasetdata ";
|
||||
$SQL .= " INNER JOIN datasetcolumn ";
|
||||
$SQL .= " ON datasetcolumn.DataSetColumnID = datasetdata.DataSetColumnID ";
|
||||
$SQL .= " WHERE datasetcolumn.DataSetID = :dataSetId ";
|
||||
$SQL .= " ) datasetdatainner ";
|
||||
$SQL .= " GROUP BY RowNumber ";
|
||||
$SQL .= " ) datasetdata ";
|
||||
$SQL .= ' ) finalselect ';
|
||||
$SQL .= " ORDER BY RowNumber ";
|
||||
|
||||
$sth = $dbh->prepare($SQL);
|
||||
$sth->execute($params);
|
||||
|
||||
return $sth->fetchAll(\PDO::FETCH_ASSOC);
|
||||
}
|
||||
}
|
||||
31
lib/XTR/DropPlayerCacheTask.php
Normal file
31
lib/XTR/DropPlayerCacheTask.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
/*
|
||||
* Spring Signage Ltd - http://www.springsignage.com
|
||||
* Copyright (C) 2018 Spring Signage Ltd
|
||||
* (DropPlayerCacheTask.php)
|
||||
*/
|
||||
|
||||
|
||||
namespace Xibo\XTR;
|
||||
|
||||
/**
|
||||
* Class DropPlayerCacheTask
|
||||
* @package Xibo\XTR
|
||||
*/
|
||||
class DropPlayerCacheTask implements TaskInterface
|
||||
{
|
||||
use TaskTrait;
|
||||
|
||||
/** @inheritdoc */
|
||||
public function setFactories($container)
|
||||
{
|
||||
// Nothing needed here
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function run()
|
||||
{
|
||||
$this->pool->deleteItem('display');
|
||||
}
|
||||
}
|
||||
330
lib/XTR/DynamicPlaylistSyncTask.php
Normal file
330
lib/XTR/DynamicPlaylistSyncTask.php
Normal file
@@ -0,0 +1,330 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2023 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 Xibo\Entity\Media;
|
||||
use Xibo\Entity\Playlist;
|
||||
use Xibo\Entity\Task;
|
||||
use Xibo\Factory\MediaFactory;
|
||||
use Xibo\Factory\ModuleFactory;
|
||||
use Xibo\Factory\PlaylistFactory;
|
||||
use Xibo\Factory\WidgetFactory;
|
||||
use Xibo\Helper\DateFormatHelper;
|
||||
use Xibo\Storage\StorageServiceInterface;
|
||||
use Xibo\Support\Exception\GeneralException;
|
||||
use Xibo\Support\Exception\NotFoundException;
|
||||
|
||||
/**
|
||||
* Class DynamicPlaylistSyncTask
|
||||
* @package Xibo\XTR
|
||||
*
|
||||
* Keep dynamic Playlists in sync with changes to the Media table.
|
||||
*/
|
||||
class DynamicPlaylistSyncTask implements TaskInterface
|
||||
{
|
||||
use TaskTrait;
|
||||
|
||||
/** @var StorageServiceInterface */
|
||||
private $store;
|
||||
|
||||
/** @var PlaylistFactory */
|
||||
private $playlistFactory;
|
||||
|
||||
/** @var MediaFactory */
|
||||
private $mediaFactory;
|
||||
|
||||
/** @var ModuleFactory */
|
||||
private $moduleFactory;
|
||||
|
||||
/** @var WidgetFactory */
|
||||
private $widgetFactory;
|
||||
|
||||
/** @inheritdoc */
|
||||
public function setFactories($container)
|
||||
{
|
||||
$this->store = $container->get('store');
|
||||
$this->playlistFactory = $container->get('playlistFactory');
|
||||
$this->mediaFactory = $container->get('mediaFactory');
|
||||
$this->moduleFactory = $container->get('moduleFactory');
|
||||
$this->widgetFactory = $container->get('widgetFactory');
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function run()
|
||||
{
|
||||
// If we're in the error state, then always run, otherwise check the dates we modified various triggers
|
||||
if ($this->getTask()->lastRunStatus !== Task::$STATUS_ERROR) {
|
||||
// Run a little query to get the last modified date from the media table
|
||||
$lastMediaUpdate = $this->store->select('
|
||||
SELECT MAX(modifiedDt) AS modifiedDt
|
||||
FROM `media`
|
||||
WHERE `type` <> \'module\' AND `type` <> \'genericfile\'
|
||||
', [])[0]['modifiedDt'];
|
||||
|
||||
$lastPlaylistUpdate = $this->store->select('
|
||||
SELECT MAX(modifiedDt) AS modifiedDt
|
||||
FROM `playlist`
|
||||
', [])[0]['modifiedDt'];
|
||||
|
||||
if (empty($lastMediaUpdate) || empty($lastPlaylistUpdate)) {
|
||||
$this->appendRunMessage('No library media or Playlists to assess');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->log->debug('Last media updated date is ' . $lastMediaUpdate);
|
||||
$this->log->debug('Last playlist updated date is ' . $lastPlaylistUpdate);
|
||||
|
||||
$lastMediaUpdate = Carbon::createFromFormat(DateFormatHelper::getSystemFormat(), $lastMediaUpdate);
|
||||
$lastPlaylistUpdate = Carbon::createFromFormat(DateFormatHelper::getSystemFormat(), $lastPlaylistUpdate);
|
||||
$lastTaskRun = Carbon::createFromTimestamp($this->getTask()->lastRunDt);
|
||||
|
||||
if ($lastMediaUpdate->lessThanOrEqualTo($lastTaskRun)
|
||||
&& $lastPlaylistUpdate->lessThanOrEqualTo($lastTaskRun))
|
||||
{
|
||||
$this->appendRunMessage('No library media/playlist updates since we last ran');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
|
||||
// Get all Dynamic Playlists
|
||||
foreach ($this->playlistFactory->query(null, ['isDynamic' => 1]) as $playlist) {
|
||||
try {
|
||||
// We want to detect any differences in what should be assigned to this Playlist.
|
||||
$playlist->load(['checkDisplayOrder' => true]);
|
||||
|
||||
$this->log->debug('Assessing Playlist: ' . $playlist->name);
|
||||
|
||||
if (empty($playlist->filterMediaName) && empty($playlist->filterMediaTags) && empty($playlist->filterFolderId)) {
|
||||
// if this Dynamic Playlist was populated will all Media in the system
|
||||
// before we introduced measures against it, we need to go through and unassign all Widgets from it.
|
||||
// if it is fresh Playlist added recently, it will not have any Widgets on it with empty filters.
|
||||
if (!empty($playlist->widgets)) {
|
||||
foreach ($playlist->widgets as $widget) {
|
||||
$playlist->deleteWidget($widget);
|
||||
}
|
||||
}
|
||||
$this->log->debug(sprintf(
|
||||
'Dynamic Playlist ID %d , with no filters set, skipping.',
|
||||
$playlist->playlistId
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Query for media which would be assigned to this Playlist and see if there are any differences
|
||||
$media = [];
|
||||
$mediaIds = [];
|
||||
$displayOrder = [];
|
||||
foreach ($this->mediaFactory->query(null, [
|
||||
'name' => $playlist->filterMediaName,
|
||||
'logicalOperatorName' => $playlist->filterMediaNameLogicalOperator,
|
||||
'tags' => $playlist->filterMediaTags,
|
||||
'exactTags' => $playlist->filterExactTags,
|
||||
'logicalOperator' => $playlist->filterMediaTagsLogicalOperator,
|
||||
'folderId' => !empty($playlist->filterFolderId) ? $playlist->filterFolderId : null,
|
||||
'userCheckUserId' => $playlist->getOwnerId(),
|
||||
'start' => 0,
|
||||
'length' => $playlist->maxNumberOfItems
|
||||
]) as $index => $item) {
|
||||
$media[$item->mediaId] = $item;
|
||||
$mediaIds[] = $item->mediaId;
|
||||
// store the expected display order
|
||||
$displayOrder[$item->mediaId] = $index + 1;
|
||||
}
|
||||
|
||||
// Work out if the set of widgets is different or not.
|
||||
// This is only the first loose check
|
||||
$different = (count($playlist->widgets) !== count($media));
|
||||
|
||||
$this->log->debug('There are ' . count($media) . ' that should be assigned and '
|
||||
. count($playlist->widgets) . ' currently assigned with max number of items set to '
|
||||
. $playlist->maxNumberOfItems . ' First check difference is '
|
||||
. var_export($different, true));
|
||||
|
||||
if (!$different) {
|
||||
// Try a more complete check, using mediaIds
|
||||
$compareMediaIds = $mediaIds;
|
||||
|
||||
// ordering should be the same, so the first time we get one out of order, we can stop
|
||||
foreach ($playlist->widgets as $widget) {
|
||||
try {
|
||||
$widgetMediaId = $widget->getPrimaryMediaId();
|
||||
if ($widgetMediaId !== $compareMediaIds[0]
|
||||
|| $widget->duration !== $media[$widgetMediaId]->duration
|
||||
) {
|
||||
$different = true;
|
||||
break;
|
||||
}
|
||||
} catch (NotFoundException $notFoundException) {
|
||||
$this->log->error('Playlist ' . $playlist->getId()
|
||||
. ' has a Widget without any associated media. widgetId = ' . $widget->getId());
|
||||
|
||||
// We ought to recalculate
|
||||
$different = true;
|
||||
break;
|
||||
}
|
||||
|
||||
array_shift($compareMediaIds);
|
||||
}
|
||||
}
|
||||
|
||||
$this->log->debug('Second check difference is ' . var_export($different, true));
|
||||
|
||||
if ($different) {
|
||||
// We will update this Playlist
|
||||
$assignmentMade = false;
|
||||
$count++;
|
||||
|
||||
// Remove the ones no-longer present, add the ones we're missing
|
||||
// we don't delete and re-add the lot to avoid regenerating the widgetIds (makes stats harder to
|
||||
// interpret)
|
||||
foreach ($playlist->widgets as $widget) {
|
||||
try {
|
||||
$widgetMediaId = $widget->getPrimaryMediaId();
|
||||
|
||||
if (!in_array($widgetMediaId, $mediaIds)) {
|
||||
$playlist->deleteWidget($widget);
|
||||
} else {
|
||||
// It's present in the array
|
||||
// Check to see if the duration is different
|
||||
if ($widget->duration !== $media[$widgetMediaId]->duration) {
|
||||
// The media duration has changed, so update the widget
|
||||
$widget->useDuration = 1;
|
||||
$widget->duration = $media[$widgetMediaId]->duration;
|
||||
$widget->calculatedDuration = $widget->duration;
|
||||
$widget->save([
|
||||
'saveWidgetOptions' => false,
|
||||
'saveWidgetAudio' => false,
|
||||
'saveWidgetMedia' => false,
|
||||
'notify' => false,
|
||||
'notifyPlaylists' => false,
|
||||
'notifyDisplays' => false,
|
||||
'audit' => true,
|
||||
'alwaysUpdate' => true
|
||||
]);
|
||||
}
|
||||
|
||||
// Pop it off the list of ones to assign.
|
||||
$mediaIds = array_diff($mediaIds, [$widgetMediaId]);
|
||||
|
||||
// We do want to save the Playlist here.
|
||||
$assignmentMade = true;
|
||||
}
|
||||
} catch (NotFoundException) {
|
||||
// Delete it
|
||||
$playlist->deleteWidget($widget);
|
||||
}
|
||||
}
|
||||
|
||||
// Do we have any mediaId's left which should be assigned and aren't?
|
||||
// Add the ones we have left
|
||||
foreach ($media as $item) {
|
||||
if (in_array($item->mediaId, $mediaIds)) {
|
||||
if (count($playlist->widgets) >= $playlist->maxNumberOfItems) {
|
||||
$this->log->debug(
|
||||
sprintf(
|
||||
'Dynamic Playlist ID %d, has reached the maximum number of items %d, finishing assignments',//phpcs:ignore
|
||||
$playlist->playlistId,
|
||||
$playlist->maxNumberOfItems
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
$assignmentMade = true;
|
||||
// make sure we pass the expected displayOrder for the new item we are about to add.
|
||||
$this->createAndAssign($playlist, $item, $displayOrder[$item->mediaId]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($assignmentMade) {
|
||||
// We've made an assignment change, so audit this change
|
||||
// don't audit any downstream save operations
|
||||
$playlist->save([
|
||||
'auditPlaylist' => true,
|
||||
'audit' => false
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
$this->log->debug('No differences detected');
|
||||
}
|
||||
} catch (GeneralException $exception) {
|
||||
$this->log->debug($exception->getTraceAsString());
|
||||
$this->log->error('Problem with PlaylistId: ' . $playlist->getId()
|
||||
. ', e = ' . $exception->getMessage());
|
||||
$this->appendRunMessage('Error with Playlist: ' . $playlist->name);
|
||||
}
|
||||
}
|
||||
|
||||
$this->appendRunMessage('Updated ' . $count . ' Playlists');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Playlist $playlist
|
||||
* @param Media $media
|
||||
* @param int $displayOrder
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
private function createAndAssign(Playlist $playlist, Media $media, int $displayOrder): void
|
||||
{
|
||||
$this->log->debug('Media Item needs to be assigned ' . $media->name . ' in sequence ' . $displayOrder);
|
||||
|
||||
// Create a module
|
||||
try {
|
||||
$module = $this->moduleFactory->getByType($media->mediaType);
|
||||
} catch (NotFoundException) {
|
||||
$this->log->error('createAndAssign: dynamic playlist matched missing module: ' . $media->mediaType);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($module->assignable == 0) {
|
||||
$this->log->error('createAndAssign: dynamic playlist matched unassignable media: ' . $media->mediaId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine the duration
|
||||
$mediaDuration = $media->duration;
|
||||
if ($mediaDuration <= 0) {
|
||||
$mediaDuration = $module->defaultDuration;
|
||||
}
|
||||
|
||||
// Create a widget
|
||||
$widget = $this->widgetFactory->create(
|
||||
$playlist->getOwnerId(),
|
||||
$playlist->playlistId,
|
||||
$media->mediaType,
|
||||
$mediaDuration,
|
||||
$module->schemaVersion
|
||||
);
|
||||
$widget->useDuration = 1;
|
||||
$widget->displayOrder = $displayOrder;
|
||||
$widget->calculateDuration($module);
|
||||
$widget->assignMedia($media->mediaId);
|
||||
|
||||
// Assign the widget to the playlist
|
||||
// making sure we pass the displayOrder here, otherwise it would be added to the end of the array.
|
||||
$playlist->assignWidget($widget, $displayOrder);
|
||||
}
|
||||
}
|
||||
207
lib/XTR/EmailNotificationsTask.php
Normal file
207
lib/XTR/EmailNotificationsTask.php
Normal file
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2025 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 Slim\Views\Twig;
|
||||
use Xibo\Entity\UserNotification;
|
||||
use Xibo\Factory\UserGroupFactory;
|
||||
use Xibo\Factory\UserNotificationFactory;
|
||||
|
||||
/**
|
||||
* Class EmailNotificationsTask
|
||||
* @package Xibo\XTR
|
||||
*/
|
||||
class EmailNotificationsTask implements TaskInterface
|
||||
{
|
||||
use TaskTrait;
|
||||
|
||||
/** @var Twig */
|
||||
private $view;
|
||||
|
||||
/** @var UserNotificationFactory */
|
||||
private $userNotificationFactory;
|
||||
|
||||
/** @var UserGroupFactory */
|
||||
private $userGroupFactory;
|
||||
|
||||
/** @inheritdoc */
|
||||
public function setFactories($container)
|
||||
{
|
||||
$this->view = $container->get('view');
|
||||
$this->userNotificationFactory = $container->get('userNotificationFactory');
|
||||
$this->userGroupFactory = $container->get('userGroupFactory');
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function run()
|
||||
{
|
||||
$this->runMessage = '# ' . __('Email Notifications') . PHP_EOL . PHP_EOL;
|
||||
|
||||
$this->processQueue();
|
||||
}
|
||||
|
||||
/** Process Queue of emails
|
||||
* @throws \PHPMailer\PHPMailer\Exception
|
||||
*/
|
||||
private function processQueue()
|
||||
{
|
||||
// Handle queue of notifications to email.
|
||||
$this->runMessage .= '## ' . __('Email Notifications') . PHP_EOL;
|
||||
|
||||
$msgFrom = $this->config->getSetting('mail_from');
|
||||
$msgFromName = $this->config->getSetting('mail_from_name');
|
||||
$processedNotifications = [];
|
||||
|
||||
$this->log->debug('Notification Queue sending from ' . $msgFrom);
|
||||
|
||||
foreach ($this->userNotificationFactory->getEmailQueue() as $notification) {
|
||||
$this->log->debug('Notification found: ' . $notification->notificationId);
|
||||
|
||||
if (!empty($notification->email) || $notification->isSystem == 1) {
|
||||
$mail = new \PHPMailer\PHPMailer\PHPMailer();
|
||||
|
||||
$this->log->debug('Sending Notification email to ' . $notification->email);
|
||||
|
||||
if ($this->checkEmailPreferences($notification)) {
|
||||
$mail->addAddress($notification->email);
|
||||
}
|
||||
|
||||
// System notifications, add mail_to to addresses if set.
|
||||
if ($notification->isSystem == 1) {
|
||||
// We should send the system notification to:
|
||||
// - all assigned users
|
||||
// - the mail_to (if set)
|
||||
$mailTo = $this->config->getSetting('mail_to');
|
||||
|
||||
// Make sure we've been able to resolve an address.
|
||||
if (empty($notification->email) && empty($mailTo)) {
|
||||
$this->log->error('Discarding NotificationId ' . $notification->notificationId
|
||||
. ' as no email address could be resolved.');
|
||||
continue;
|
||||
}
|
||||
|
||||
// if mail_to is set and is different from user email, and we did not
|
||||
// process this notification yet (the same notificationId will be returned for each assigned user)
|
||||
// add it to addresses
|
||||
if ($mailTo !== $notification->email
|
||||
&& !empty($mailTo)
|
||||
&& !in_array($notification->notificationId, $processedNotifications)
|
||||
) {
|
||||
$this->log->debug('Sending Notification email to mailTo ' . $mailTo);
|
||||
$mail->addAddress($mailTo);
|
||||
}
|
||||
}
|
||||
|
||||
// Email them
|
||||
$mail->CharSet = 'UTF-8';
|
||||
$mail->Encoding = 'base64';
|
||||
$mail->From = $msgFrom;
|
||||
|
||||
// Add attachment
|
||||
if ($notification->filename != null) {
|
||||
$mail->addAttachment(
|
||||
$this->config->getSetting('LIBRARY_LOCATION') . 'attachment/' . $notification->filename,
|
||||
$notification->originalFileName
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($msgFromName)) {
|
||||
$mail->FromName = $msgFromName;
|
||||
}
|
||||
|
||||
$mail->Subject = $notification->subject;
|
||||
|
||||
$addresses = explode(',', $notification->nonusers);
|
||||
foreach ($addresses as $address) {
|
||||
$mail->AddAddress($address);
|
||||
}
|
||||
|
||||
// Body
|
||||
$mail->isHTML(true);
|
||||
$mail->AltBody = $notification->body;
|
||||
$mail->Body = $this->generateEmailBody($notification->subject, $notification->body);
|
||||
|
||||
if (!$mail->send()) {
|
||||
$this->log->error('Unable to send email notification mail to ' . $notification->email);
|
||||
$this->runMessage .= ' - E' . PHP_EOL;
|
||||
$this->log->error('Unable to send email notification Error: ' . $mail->ErrorInfo);
|
||||
} else {
|
||||
$this->runMessage .= ' - A' . PHP_EOL;
|
||||
}
|
||||
|
||||
$this->log->debug('Marking notification as sent');
|
||||
} else {
|
||||
$this->log->error('Discarding NotificationId ' . $notification->notificationId
|
||||
. ' as no email address could be resolved.');
|
||||
}
|
||||
|
||||
$processedNotifications[] = $notification->notificationId;
|
||||
// Mark as sent
|
||||
$notification->setEmailed(Carbon::now()->format('U'));
|
||||
$notification->save();
|
||||
}
|
||||
|
||||
$this->runMessage .= ' - Done' . PHP_EOL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an email body
|
||||
* @param $subject
|
||||
* @param $body
|
||||
* @return string
|
||||
*/
|
||||
private function generateEmailBody($subject, $body)
|
||||
{
|
||||
// Generate Body
|
||||
// Start an object buffer
|
||||
ob_start();
|
||||
|
||||
// Render the template
|
||||
echo $this->view->fetch(
|
||||
'email-template.twig',
|
||||
['config' => $this->config, 'subject' => $subject, 'body' => $body]
|
||||
);
|
||||
|
||||
$body = ob_get_contents();
|
||||
|
||||
ob_end_clean();
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should we send email to this user?
|
||||
* check relevant flag for the notification type on the user group.
|
||||
* @param UserNotification $notification
|
||||
* @return bool
|
||||
*/
|
||||
private function checkEmailPreferences(UserNotification $notification): bool
|
||||
{
|
||||
return $this->userGroupFactory->checkNotificationEmailPreferences(
|
||||
$notification->userId,
|
||||
$notification->getTypeForGroup()
|
||||
);
|
||||
}
|
||||
}
|
||||
164
lib/XTR/ImageProcessingTask.php
Normal file
164
lib/XTR/ImageProcessingTask.php
Normal file
@@ -0,0 +1,164 @@
|
||||
<?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 Xibo\Factory\DisplayFactory;
|
||||
use Xibo\Factory\MediaFactory;
|
||||
use Xibo\Helper\DateFormatHelper;
|
||||
use Xibo\Service\ImageProcessingServiceInterface;
|
||||
|
||||
/**
|
||||
* Class ImageProcessingTask
|
||||
* @package Xibo\XTR
|
||||
*/
|
||||
class ImageProcessingTask implements TaskInterface
|
||||
{
|
||||
use TaskTrait;
|
||||
|
||||
/** @var ImageProcessingServiceInterface */
|
||||
private $imageProcessingService;
|
||||
|
||||
/** @var MediaFactory */
|
||||
private $mediaFactory;
|
||||
|
||||
/** @var DisplayFactory */
|
||||
private $displayFactory;
|
||||
|
||||
/** @inheritdoc */
|
||||
public function setFactories($container)
|
||||
{
|
||||
$this->mediaFactory = $container->get('mediaFactory');
|
||||
$this->displayFactory = $container->get('displayFactory');
|
||||
$this->imageProcessingService = $container->get('imageProcessingService');
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function run()
|
||||
{
|
||||
$this->runMessage = '# ' . __('Image Processing') . PHP_EOL . PHP_EOL;
|
||||
|
||||
// Long running task
|
||||
set_time_limit(0);
|
||||
|
||||
$this->runImageProcessing();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private function runImageProcessing()
|
||||
{
|
||||
$images = $this->mediaFactory->query(null, ['released' => 0, 'allModules' => 1, 'imageProcessing' => 1]);
|
||||
|
||||
$libraryLocation = $this->config->getSetting('LIBRARY_LOCATION');
|
||||
$resizeThreshold = $this->config->getSetting('DEFAULT_RESIZE_THRESHOLD');
|
||||
$count = 0;
|
||||
|
||||
// All displayIds
|
||||
$displayIds = [];
|
||||
|
||||
// Get list of Images
|
||||
foreach ($images as $media) {
|
||||
|
||||
$filePath = $libraryLocation . $media->storedAs;
|
||||
list($imgWidth, $imgHeight) = @getimagesize($filePath);
|
||||
|
||||
// Orientation of the image
|
||||
if ($imgWidth > $imgHeight) { // 'landscape';
|
||||
$updatedImg = $this->imageProcessingService->resizeImage($filePath, $resizeThreshold, null);
|
||||
} else { // 'portrait';
|
||||
$updatedImg = $this->imageProcessingService->resizeImage($filePath, null, $resizeThreshold);
|
||||
}
|
||||
|
||||
// Clears file status cache
|
||||
clearstatcache(true, $updatedImg['filePath']);
|
||||
|
||||
$count++;
|
||||
|
||||
// Release image and save
|
||||
$media->release(
|
||||
md5_file($updatedImg['filePath']),
|
||||
filesize($updatedImg['filePath']),
|
||||
$updatedImg['height'],
|
||||
$updatedImg['width']
|
||||
);
|
||||
$this->store->commitIfNecessary();
|
||||
|
||||
$mediaDisplays= [];
|
||||
$sql = 'SELECT displayId FROM `requiredfile` WHERE itemId = :itemId';
|
||||
foreach ($this->store->select($sql, ['itemId' => $media->mediaId]) as $row) {
|
||||
$displayIds[] = $row['displayId'];
|
||||
$mediaDisplays[] = $row['displayId'];
|
||||
}
|
||||
|
||||
// Update Required Files
|
||||
foreach ($mediaDisplays as $displayId) {
|
||||
|
||||
$this->store->update('UPDATE `requiredfile` SET released = :released, size = :size
|
||||
WHERE `requiredfile`.displayId = :displayId AND `requiredfile`.itemId = :itemId ', [
|
||||
'released' => 1,
|
||||
'size' => $media->fileSize,
|
||||
'displayId' => $displayId,
|
||||
'itemId' => $media->mediaId
|
||||
]);
|
||||
}
|
||||
|
||||
// Mark any effected Layouts to be rebuilt.
|
||||
$this->store->update('
|
||||
UPDATE `layout`
|
||||
SET status = :status, `modifiedDT` = :modifiedDt
|
||||
WHERE layoutId IN (
|
||||
SELECT DISTINCT region.layoutId
|
||||
FROM lkwidgetmedia
|
||||
INNER JOIN widget
|
||||
ON widget.widgetId = lkwidgetmedia.widgetId
|
||||
INNER JOIN lkplaylistplaylist
|
||||
ON lkplaylistplaylist.childId = widget.playlistId
|
||||
INNER JOIN playlist
|
||||
ON lkplaylistplaylist.parentId = playlist.playlistId
|
||||
INNER JOIN region
|
||||
ON playlist.regionId = region.regionId
|
||||
WHERE lkwidgetmedia.mediaId = :mediaId
|
||||
)
|
||||
', [
|
||||
'status' => 3,
|
||||
'modifiedDt' => Carbon::now()->format(DateFormatHelper::getSystemFormat()),
|
||||
'mediaId' => $media->mediaId
|
||||
]);
|
||||
}
|
||||
|
||||
// Notify display
|
||||
if ($count > 0) {
|
||||
foreach (array_unique($displayIds) as $displayId) {
|
||||
|
||||
// Get display
|
||||
$display = $this->displayFactory->getById($displayId);
|
||||
$display->notify();
|
||||
}
|
||||
}
|
||||
|
||||
$this->appendRunMessage('Released and modified image count. ' . $count);
|
||||
|
||||
}
|
||||
}
|
||||
209
lib/XTR/LayoutConvertTask.php
Normal file
209
lib/XTR/LayoutConvertTask.php
Normal file
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
/*
|
||||
* Spring Signage Ltd - http://www.springsignage.com
|
||||
* Copyright (C) 2018 Spring Signage Ltd
|
||||
* (LayoutConvertTask.php)
|
||||
*/
|
||||
|
||||
|
||||
namespace Xibo\XTR;
|
||||
use Xibo\Entity\Region;
|
||||
use Xibo\Entity\Widget;
|
||||
use Xibo\Factory\LayoutFactory;
|
||||
use Xibo\Factory\PermissionFactory;
|
||||
use Xibo\Helper\Environment;
|
||||
|
||||
/**
|
||||
* Class LayoutConvertTask
|
||||
* @package Xibo\XTR
|
||||
*/
|
||||
class LayoutConvertTask implements TaskInterface
|
||||
{
|
||||
use TaskTrait;
|
||||
|
||||
/** @var PermissionFactory */
|
||||
private $permissionFactory;
|
||||
|
||||
/** @var LayoutFactory */
|
||||
private $layoutFactory;
|
||||
|
||||
/** @var \Xibo\Factory\ModuleFactory */
|
||||
private $moduleFactory;
|
||||
|
||||
/** @inheritdoc */
|
||||
public function setFactories($container)
|
||||
{
|
||||
$this->permissionFactory = $container->get('permissionFactory');
|
||||
$this->layoutFactory = $container->get('layoutFactory');
|
||||
$this->moduleFactory = $container->get('moduleFactory');
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function run()
|
||||
{
|
||||
// lklayoutmedia is removed at the end of this task
|
||||
if (!$this->store->exists('SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :name', [
|
||||
'name' => 'lklayoutmedia'
|
||||
])) {
|
||||
$this->appendRunMessage('Already converted');
|
||||
|
||||
// Disable the task
|
||||
$this->disableTask();
|
||||
|
||||
// Don't do anything further
|
||||
return;
|
||||
}
|
||||
|
||||
// Permissions handling
|
||||
// -------------------
|
||||
// Layout permissions should remain the same
|
||||
// the lklayoutmediagroup table and lklayoutregiongroup table will be removed
|
||||
// We do not have simple switch for the lklayoutmediagroup table as these are supposed to represent "Widgets"
|
||||
// which did not exist at this point.
|
||||
// Build a keyed array of existing widget permissions
|
||||
$mediaPermissions = [];
|
||||
foreach ($this->store->select('
|
||||
SELECT `lklayoutmediagroup`.groupId, `lkwidgetmedia`.widgetId, `view`, `edit`, `del`
|
||||
FROM `lklayoutmediagroup`
|
||||
INNER JOIN `lkwidgetmedia`
|
||||
ON `lklayoutmediagroup`.`mediaId` = `lkwidgetmedia`.widgetId
|
||||
WHERE `lkwidgetmedia`.widgetId IN (
|
||||
SELECT widget.widgetId
|
||||
FROM `widget`
|
||||
INNER JOIN `playlist`
|
||||
ON `playlist`.playlistId = `widget`.playlistId
|
||||
WHERE `playlist`.regionId = `lklayoutmediagroup`.regionId
|
||||
)
|
||||
', []) as $row) {
|
||||
$permission = $this->permissionFactory->create(
|
||||
$row['groupId'],
|
||||
Widget::class,
|
||||
$row['widgetId'],
|
||||
$row['view'],
|
||||
$row['edit'],
|
||||
$row['del']
|
||||
);
|
||||
|
||||
$mediaPermissions[$row['mediaId']] = $permission;
|
||||
}
|
||||
|
||||
// Build a keyed array of existing region permissions
|
||||
$regionPermissions = [];
|
||||
foreach ($this->store->select('SELECT groupId, layoutId, regionId, `view`, `edit`, `del` FROM `lklayoutregiongroup`', []) as $row) {
|
||||
$permission = $this->permissionFactory->create(
|
||||
$row['groupId'],
|
||||
Region::class,
|
||||
$row['regionId'],
|
||||
$row['view'],
|
||||
$row['edit'],
|
||||
$row['del']
|
||||
);
|
||||
|
||||
$regionPermissions[$row['regionId']] = $permission;
|
||||
}
|
||||
|
||||
// Get the library location to store backups of existing XLF
|
||||
$libraryLocation = $this->config->getSetting('LIBRARY_LOCATION');
|
||||
|
||||
// We need to go through each layout, save the XLF as a backup in the library and then upgrade it.
|
||||
// This task applies to Layouts which are schemaVersion 2 or lower. xibosignage/xibo#2056
|
||||
foreach ($this->store->select('SELECT layoutId, xml FROM `layout` WHERE schemaVersion <= :schemaVersion', [
|
||||
'schemaVersion' => 2
|
||||
]) as $oldLayout) {
|
||||
|
||||
$oldLayoutId = intval($oldLayout['layoutId']);
|
||||
|
||||
try {
|
||||
// Does this layout have any XML associated with it? If not, then it is an empty layout.
|
||||
if (empty($oldLayout['xml'])) {
|
||||
// This is frankly, odd, so we better log it
|
||||
$this->log->critical('Layout upgrade without any existing XLF, i.e. empty. ID = ' . $oldLayoutId);
|
||||
|
||||
// Pull out the layout record, and set some best guess defaults
|
||||
$layout = $this->layoutFactory->getById($oldLayoutId);
|
||||
|
||||
// We have to guess something here as we do not have any XML to go by. Default to landscape 1080p.
|
||||
$layout->width = 1920;
|
||||
$layout->height = 1080;
|
||||
|
||||
} else {
|
||||
// Save off a copy of the XML in the library
|
||||
file_put_contents($libraryLocation . 'archive_' . $oldLayoutId . '.xlf', $oldLayout['xml']);
|
||||
|
||||
// Create a new layout from the XML
|
||||
$layout = $this->layoutFactory->loadByXlf($oldLayout['xml'], $this->layoutFactory->getById($oldLayoutId));
|
||||
}
|
||||
|
||||
// We need one final pass through all widgets on the layout so that we can set the durations properly.
|
||||
foreach ($layout->getRegionWidgets() as $widget) {
|
||||
$widget->calculateDuration($this->moduleFactory->getByType($widget->type), true);
|
||||
|
||||
// Get global stat setting of widget to set to on/off/inherit
|
||||
$widget->setOptionValue('enableStat', 'attrib', $this->config->getSetting('WIDGET_STATS_ENABLED_DEFAULT'));
|
||||
}
|
||||
|
||||
// Save the layout
|
||||
$layout->schemaVersion = Environment::$XLF_VERSION;
|
||||
$layout->save(['notify' => false, 'audit' => false]);
|
||||
|
||||
// Now that we have new ID's we need to cross reference them with the old IDs and recreate the permissions
|
||||
foreach ($layout->regions as $region) {
|
||||
/* @var \Xibo\Entity\Region $region */
|
||||
if (array_key_exists($region->tempId, $regionPermissions)) {
|
||||
$permission = $regionPermissions[$region->tempId];
|
||||
/* @var \Xibo\Entity\Permission $permission */
|
||||
// Double check we are for the same layout
|
||||
if ($permission->objectId == $layout->layoutId) {
|
||||
$permission = clone $permission;
|
||||
$permission->objectId = $region->regionId;
|
||||
$permission->save();
|
||||
}
|
||||
}
|
||||
|
||||
/* @var \Xibo\Entity\Playlist $playlist */
|
||||
foreach ($region->getPlaylist()->widgets as $widget) {
|
||||
/* @var \Xibo\Entity\Widget $widget */
|
||||
if (array_key_exists($widget->tempId, $mediaPermissions)) {
|
||||
$permission = $mediaPermissions[$widget->tempId];
|
||||
/* @var \Xibo\Entity\Permission $permission */
|
||||
if ($permission->objectId == $layout->layoutId && $region->tempId == $permission->objectIdString) {
|
||||
$permission = clone $permission;
|
||||
$permission->objectId = $widget->widgetId;
|
||||
$permission->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->appendRunMessage('Error upgrading Layout, this should be checked post-upgrade. ID: ' . $oldLayoutId);
|
||||
$this->log->critical('Error upgrading Layout, this should be checked post-upgrade. ID: ' . $oldLayoutId);
|
||||
$this->log->error($e->getMessage() . ' - ' . $e->getTraceAsString());
|
||||
}
|
||||
}
|
||||
|
||||
$this->appendRunMessage('Finished converting, dropping unnecessary tables.');
|
||||
|
||||
// Drop the permissions
|
||||
$this->store->update('DROP TABLE `lklayoutmediagroup`;', []);
|
||||
$this->store->update('DROP TABLE `lklayoutregiongroup`;', []);
|
||||
$this->store->update('DROP TABLE lklayoutmedia', []);
|
||||
$this->store->update('ALTER TABLE `layout` DROP `xml`;', []);
|
||||
|
||||
// Disable the task
|
||||
$this->disableTask();
|
||||
|
||||
$this->appendRunMessage('Conversion Completed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables and saves this task immediately
|
||||
* @throws \Xibo\Support\Exception\InvalidArgumentException
|
||||
*/
|
||||
private function disableTask()
|
||||
{
|
||||
$this->getTask()->isActive = 0;
|
||||
$this->getTask()->save();
|
||||
$this->store->commitIfNecessary();
|
||||
}
|
||||
}
|
||||
344
lib/XTR/MaintenanceDailyTask.php
Normal file
344
lib/XTR/MaintenanceDailyTask.php
Normal file
@@ -0,0 +1,344 @@
|
||||
<?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 GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Xibo\Controller\Module;
|
||||
use Xibo\Event\MaintenanceDailyEvent;
|
||||
use Xibo\Factory\DataSetFactory;
|
||||
use Xibo\Factory\FontFactory;
|
||||
use Xibo\Factory\LayoutFactory;
|
||||
use Xibo\Factory\ModuleFactory;
|
||||
use Xibo\Factory\ModuleTemplateFactory;
|
||||
use Xibo\Factory\UserFactory;
|
||||
use Xibo\Helper\DatabaseLogHandler;
|
||||
use Xibo\Helper\DateFormatHelper;
|
||||
use Xibo\Helper\Random;
|
||||
use Xibo\Service\MediaService;
|
||||
use Xibo\Service\MediaServiceInterface;
|
||||
use Xibo\Support\Exception\GeneralException;
|
||||
use Xibo\Support\Exception\NotFoundException;
|
||||
|
||||
/**
|
||||
* Class MaintenanceDailyTask
|
||||
* @package Xibo\XTR
|
||||
*/
|
||||
class MaintenanceDailyTask implements TaskInterface
|
||||
{
|
||||
use TaskTrait;
|
||||
|
||||
/** @var LayoutFactory */
|
||||
private $layoutFactory;
|
||||
|
||||
/** @var UserFactory */
|
||||
private $userFactory;
|
||||
|
||||
/** @var Module */
|
||||
private $moduleController;
|
||||
|
||||
/** @var MediaServiceInterface */
|
||||
private $mediaService;
|
||||
|
||||
/** @var DataSetFactory */
|
||||
private $dataSetFactory;
|
||||
|
||||
/** @var FontFactory */
|
||||
private $fontFactory;
|
||||
|
||||
/** @var ModuleFactory */
|
||||
private $moduleFactory;
|
||||
|
||||
/** @var ModuleTemplateFactory */
|
||||
private $moduleTemplateFactory;
|
||||
|
||||
/** @var string */
|
||||
private $libraryLocation;
|
||||
|
||||
/** @inheritdoc */
|
||||
public function setFactories($container)
|
||||
{
|
||||
$this->moduleController = $container->get('\Xibo\Controller\Module');
|
||||
$this->layoutFactory = $container->get('layoutFactory');
|
||||
$this->userFactory = $container->get('userFactory');
|
||||
$this->dataSetFactory = $container->get('dataSetFactory');
|
||||
$this->mediaService = $container->get('mediaService');
|
||||
$this->fontFactory = $container->get('fontFactory');
|
||||
$this->moduleFactory = $container->get('moduleFactory');
|
||||
$this->moduleTemplateFactory = $container->get('moduleTemplateFactory');
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function run()
|
||||
{
|
||||
$this->runMessage = '# ' . __('Daily Maintenance') . PHP_EOL . PHP_EOL;
|
||||
|
||||
// Long-running task
|
||||
set_time_limit(0);
|
||||
|
||||
// Make sure our library structure is as it should be
|
||||
try {
|
||||
$this->libraryLocation = $this->getConfig()->getSetting('LIBRARY_LOCATION');
|
||||
MediaService::ensureLibraryExists($this->libraryLocation);
|
||||
} catch (\Exception $exception) {
|
||||
$this->getLogger()->error('Library structure invalid, e = ' . $exception->getMessage());
|
||||
$this->appendRunMessage(__('Library structure invalid'));
|
||||
}
|
||||
|
||||
// Import layouts
|
||||
$this->importLayouts();
|
||||
|
||||
// Cycle the XMR Key
|
||||
$this->cycleXmrKey();
|
||||
|
||||
try {
|
||||
$this->appendRunMessage(__('## Build caches'));
|
||||
|
||||
// TODO: should we remove all bundle/asset cache before we start?
|
||||
// Player bundle
|
||||
$this->cachePlayerBundle();
|
||||
|
||||
// Cache Assets
|
||||
$this->cacheAssets();
|
||||
|
||||
// Fonts
|
||||
$this->mediaService->setUser($this->userFactory->getSystemUser())->updateFontsCss();
|
||||
} catch (\Exception $exception) {
|
||||
$this->getLogger()->error('Failure to build caches, e = ' . $exception->getMessage());
|
||||
$this->appendRunMessage(__('Failure to build caches'));
|
||||
}
|
||||
|
||||
// Tidy logs
|
||||
$this->tidyLogs();
|
||||
|
||||
// Tidy Cache
|
||||
$this->tidyCache();
|
||||
|
||||
// Dispatch an event so that consumers can hook into daily maintenance.
|
||||
$event = new MaintenanceDailyEvent();
|
||||
$this->getDispatcher()->dispatch($event, MaintenanceDailyEvent::$NAME);
|
||||
foreach ($event->getMessages() as $message) {
|
||||
$this->appendRunMessage($message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tidy the DB logs
|
||||
*/
|
||||
private function tidyLogs()
|
||||
{
|
||||
$this->runMessage .= '## ' . __('Tidy Logs') . PHP_EOL;
|
||||
|
||||
$maxage = $this->config->getSetting('MAINTENANCE_LOG_MAXAGE');
|
||||
if ($maxage != 0) {
|
||||
// Run this in the log handler so that we share the same connection and don't deadlock.
|
||||
DatabaseLogHandler::tidyLogs(
|
||||
Carbon::now()
|
||||
->subDays(intval($maxage))
|
||||
->format(DateFormatHelper::getSystemFormat())
|
||||
);
|
||||
|
||||
$this->runMessage .= ' - ' . __('Done') . PHP_EOL . PHP_EOL;
|
||||
} else {
|
||||
$this->runMessage .= ' - ' . __('Disabled') . PHP_EOL . PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tidy Cache
|
||||
*/
|
||||
private function tidyCache()
|
||||
{
|
||||
$this->runMessage .= '## ' . __('Tidy Cache') . PHP_EOL;
|
||||
$this->pool->purge();
|
||||
$this->runMessage .= ' - ' . __('Done.') . PHP_EOL . PHP_EOL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import Layouts
|
||||
* @throws GeneralException|\FontLib\Exception\FontNotFoundException
|
||||
*/
|
||||
private function importLayouts()
|
||||
{
|
||||
$this->runMessage .= '## ' . __('Import Layouts and Fonts') . PHP_EOL;
|
||||
|
||||
if ($this->config->getSetting('DEFAULTS_IMPORTED') == 0) {
|
||||
// Make sure the library exists
|
||||
$this->mediaService->initLibrary();
|
||||
|
||||
// Import any layouts
|
||||
$folder = $this->config->uri('layouts', true);
|
||||
|
||||
foreach (array_diff(scandir($folder), array('..', '.')) as $file) {
|
||||
if (stripos($file, '.zip')) {
|
||||
try {
|
||||
$layout = $this->layoutFactory->createFromZip(
|
||||
$folder . '/' . $file,
|
||||
null,
|
||||
$this->userFactory->getSystemUser()->getId(),
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
$this->dataSetFactory,
|
||||
null,
|
||||
$this->mediaService,
|
||||
1
|
||||
);
|
||||
|
||||
$layout->save([
|
||||
'audit' => false,
|
||||
'import' => true
|
||||
]);
|
||||
|
||||
if (!empty($layout->getUnmatchedProperty('thumbnail'))) {
|
||||
rename($layout->getUnmatchedProperty('thumbnail'), $layout->getThumbnailUri());
|
||||
}
|
||||
|
||||
try {
|
||||
$this->layoutFactory->getById($this->config->getSetting('DEFAULT_LAYOUT'));
|
||||
} catch (NotFoundException $exception) {
|
||||
$this->config->changeSetting('DEFAULT_LAYOUT', $layout->layoutId);
|
||||
}
|
||||
} catch (\Exception $exception) {
|
||||
$this->log->error('Unable to import layout: ' . $file . '. E = ' . $exception->getMessage());
|
||||
$this->log->debug($exception->getTraceAsString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fonts
|
||||
// -----
|
||||
// install fonts from the theme folder
|
||||
$libraryLocation = $this->config->getSetting('LIBRARY_LOCATION');
|
||||
$fontFolder = $this->config->uri('fonts', true);
|
||||
foreach (array_diff(scandir($fontFolder), array('..', '.')) as $file) {
|
||||
// check if we already have this font file
|
||||
if (count($this->fontFactory->getByFileName($file)) <= 0) {
|
||||
// if we don't add it
|
||||
$filePath = $fontFolder . DIRECTORY_SEPARATOR . $file;
|
||||
$fontLib = \FontLib\Font::load($filePath);
|
||||
|
||||
// check embed flag, just in case
|
||||
$embed = intval($fontLib->getData('OS/2', 'fsType'));
|
||||
// if it's not embeddable, log error and skip it
|
||||
if ($embed != 0 && $embed != 8) {
|
||||
$this->log->error('Unable to install default Font: ' . $file
|
||||
. ' . Font file is not embeddable due to its permissions');
|
||||
continue;
|
||||
}
|
||||
|
||||
$font = $this->fontFactory->createEmpty();
|
||||
$font->modifiedBy = $this->userFactory->getSystemUser()->userName;
|
||||
$font->name = $fontLib->getFontName() . ' ' . $fontLib->getFontSubfamily();
|
||||
$font->familyName = strtolower(preg_replace('/\s+/', ' ', preg_replace('/\d+/u', '', $font->name)));
|
||||
$font->fileName = $file;
|
||||
$font->size = filesize($filePath);
|
||||
$font->md5 = md5_file($filePath);
|
||||
$font->save();
|
||||
|
||||
$copied = copy($filePath, $libraryLocation . 'fonts/' . $file);
|
||||
if (!$copied) {
|
||||
$this->getLogger()->error('importLayouts: Unable to copy fonts to ' . $libraryLocation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->config->changeSetting('DEFAULTS_IMPORTED', 1);
|
||||
|
||||
$this->runMessage .= ' - ' . __('Done.') . PHP_EOL . PHP_EOL;
|
||||
} else {
|
||||
$this->runMessage .= ' - ' . __('Not Required.') . PHP_EOL . PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the cache of assets
|
||||
* @return void
|
||||
* @throws GeneralException
|
||||
*/
|
||||
private function cacheAssets(): void
|
||||
{
|
||||
// Assets
|
||||
$failedCount = 0;
|
||||
$assets = array_merge($this->moduleFactory->getAllAssets(), $this->moduleTemplateFactory->getAllAssets());
|
||||
foreach ($assets as $asset) {
|
||||
try {
|
||||
$asset->updateAssetCache($this->libraryLocation, true);
|
||||
} catch (GeneralException $exception) {
|
||||
$failedCount++;
|
||||
$this->log->error('Unable to copy asset: ' . $asset->id . ', e: ' . $exception->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$this->appendRunMessage(sprintf(__('Assets cached, %d failed.'), $failedCount));
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache the player bundle.
|
||||
* @return void
|
||||
*/
|
||||
private function cachePlayerBundle(): void
|
||||
{
|
||||
// Output the player bundle
|
||||
$bundlePath = $this->getConfig()->getSetting('LIBRARY_LOCATION') . 'assets/bundle.min.js';
|
||||
$bundleMd5CachePath = $bundlePath . '.md5';
|
||||
|
||||
copy(PROJECT_ROOT . '/modules/bundle.min.js', $bundlePath);
|
||||
file_put_contents($bundleMd5CachePath, md5_file($bundlePath));
|
||||
|
||||
$this->appendRunMessage(__('Player bundle cached'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Once per day we cycle the XMR CMS key
|
||||
* the old key should remain valid in XMR for up to 1 hour further to allow for cross over
|
||||
* @return void
|
||||
*/
|
||||
private function cycleXmrKey(): void
|
||||
{
|
||||
$this->log->debug('cycleXmrKey: adding new key');
|
||||
try {
|
||||
$key = Random::generateString(20, 'xmr_');
|
||||
|
||||
$this->getConfig()->changeSetting('XMR_CMS_KEY', $key);
|
||||
$client = new Client($this->config->getGuzzleProxy([
|
||||
'base_uri' => $this->getConfig()->getSetting('XMR_ADDRESS'),
|
||||
]));
|
||||
|
||||
$client->post('/', [
|
||||
'json' => [
|
||||
'id' => constant('SECRET_KEY'),
|
||||
'type' => 'keys',
|
||||
'key' => $key,
|
||||
],
|
||||
]);
|
||||
$this->log->debug('cycleXmrKey: added new key');
|
||||
} catch (GuzzleException | \Exception $e) {
|
||||
$this->log->error('cycleXmrKey: failed. E = ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
685
lib/XTR/MaintenanceRegularTask.php
Normal file
685
lib/XTR/MaintenanceRegularTask.php
Normal file
@@ -0,0 +1,685 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2025 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 GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Xibo\Controller\Display;
|
||||
use Xibo\Event\DisplayGroupLoadEvent;
|
||||
use Xibo\Event\MaintenanceRegularEvent;
|
||||
use Xibo\Factory\DisplayFactory;
|
||||
use Xibo\Factory\DisplayGroupFactory;
|
||||
use Xibo\Factory\LayoutFactory;
|
||||
use Xibo\Factory\ModuleFactory;
|
||||
use Xibo\Factory\NotificationFactory;
|
||||
use Xibo\Factory\PlaylistFactory;
|
||||
use Xibo\Factory\ScheduleFactory;
|
||||
use Xibo\Factory\UserGroupFactory;
|
||||
use Xibo\Helper\ByteFormatter;
|
||||
use Xibo\Helper\DateFormatHelper;
|
||||
use Xibo\Helper\Profiler;
|
||||
use Xibo\Helper\Status;
|
||||
use Xibo\Helper\WakeOnLan;
|
||||
use Xibo\Service\MediaServiceInterface;
|
||||
use Xibo\Support\Exception\GeneralException;
|
||||
use Xibo\Support\Exception\NotFoundException;
|
||||
|
||||
/**
|
||||
* Class MaintenanceRegularTask
|
||||
* @package Xibo\XTR
|
||||
*/
|
||||
class MaintenanceRegularTask implements TaskInterface
|
||||
{
|
||||
use TaskTrait;
|
||||
|
||||
/** @var Display */
|
||||
private $displayController;
|
||||
|
||||
/** @var MediaServiceInterface */
|
||||
private $mediaService;
|
||||
|
||||
/** @var DisplayFactory */
|
||||
private $displayFactory;
|
||||
|
||||
/** @var DisplayGroupFactory */
|
||||
private $displayGroupFactory;
|
||||
|
||||
/** @var NotificationFactory */
|
||||
private $notificationFactory;
|
||||
|
||||
/** @var UserGroupFactory */
|
||||
private $userGroupFactory;
|
||||
|
||||
/** @var LayoutFactory */
|
||||
private $layoutFactory;
|
||||
|
||||
/** @var PlaylistFactory */
|
||||
private $playlistFactory;
|
||||
|
||||
/** @var ModuleFactory */
|
||||
private $moduleFactory;
|
||||
|
||||
/** @var \Xibo\Helper\SanitizerService */
|
||||
private $sanitizerService;
|
||||
/**
|
||||
* @var ScheduleFactory
|
||||
*/
|
||||
private $scheduleFactory;
|
||||
|
||||
/** @inheritdoc */
|
||||
public function setFactories($container)
|
||||
{
|
||||
$this->displayController = $container->get('\Xibo\Controller\Display');
|
||||
$this->mediaService = $container->get('mediaService');
|
||||
|
||||
$this->displayFactory = $container->get('displayFactory');
|
||||
$this->displayGroupFactory = $container->get('displayGroupFactory');
|
||||
$this->notificationFactory = $container->get('notificationFactory');
|
||||
$this->userGroupFactory = $container->get('userGroupFactory');
|
||||
$this->layoutFactory = $container->get('layoutFactory');
|
||||
$this->playlistFactory = $container->get('playlistFactory');
|
||||
$this->moduleFactory = $container->get('moduleFactory');
|
||||
$this->sanitizerService = $container->get('sanitizerService');
|
||||
$this->scheduleFactory = $container->get('scheduleFactory');
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function run()
|
||||
{
|
||||
$this->runMessage = '# ' . __('Regular Maintenance') . PHP_EOL . PHP_EOL;
|
||||
|
||||
$this->assertXmrKey();
|
||||
|
||||
$this->displayDownEmailAlerts();
|
||||
|
||||
$this->licenceSlotValidation();
|
||||
|
||||
$this->wakeOnLan();
|
||||
|
||||
$this->updatePlaylistDurations();
|
||||
|
||||
$this->buildLayouts();
|
||||
|
||||
$this->tidyLibrary();
|
||||
|
||||
$this->checkLibraryUsage();
|
||||
|
||||
$this->checkOverRequestedFiles();
|
||||
|
||||
$this->publishLayouts();
|
||||
|
||||
$this->assessDynamicDisplayGroups();
|
||||
|
||||
$this->tidyAdCampaignSchedules();
|
||||
|
||||
$this->tidyUnusedFullScreenLayout();
|
||||
|
||||
// Dispatch an event so that consumers can hook into regular maintenance.
|
||||
$event = new MaintenanceRegularEvent();
|
||||
$this->getDispatcher()->dispatch($event, MaintenanceRegularEvent::$NAME);
|
||||
foreach ($event->getMessages() as $message) {
|
||||
$this->appendRunMessage($message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display Down email alerts
|
||||
* - just runs validate displays
|
||||
*/
|
||||
private function displayDownEmailAlerts()
|
||||
{
|
||||
$this->runMessage .= '## ' . __('Email Alerts') . PHP_EOL;
|
||||
|
||||
$this->displayController->validateDisplays($this->displayFactory->query());
|
||||
|
||||
$this->appendRunMessage(__('Done'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Licence Slot Validation
|
||||
*/
|
||||
private function licenceSlotValidation()
|
||||
{
|
||||
$maxDisplays = $this->config->getSetting('MAX_LICENSED_DISPLAYS');
|
||||
|
||||
if ($maxDisplays > 0) {
|
||||
$this->runMessage .= '## ' . __('Licence Slot Validation') . PHP_EOL;
|
||||
|
||||
// Get a list of all displays
|
||||
try {
|
||||
$dbh = $this->store->getConnection();
|
||||
$sth = $dbh->prepare('SELECT displayId, display FROM `display` WHERE licensed = 1 ORDER BY lastAccessed');
|
||||
$sth->execute();
|
||||
|
||||
$displays = $sth->fetchAll(\PDO::FETCH_ASSOC);
|
||||
|
||||
if (count($displays) > $maxDisplays) {
|
||||
// :(
|
||||
// We need to un-licence some displays
|
||||
$difference = count($displays) - $maxDisplays;
|
||||
|
||||
$this->log->alert(sprintf('Max %d authorised displays exceeded, we need to un-authorise %d of %d displays', $maxDisplays, $difference, count($displays)));
|
||||
|
||||
$update = $dbh->prepare('UPDATE `display` SET licensed = 0 WHERE displayId = :displayId');
|
||||
|
||||
foreach ($displays as $display) {
|
||||
$sanitizedDisplay = $this->getSanitizer($display);
|
||||
|
||||
// If we are down to 0 difference, then stop
|
||||
if ($difference == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
$this->appendRunMessage(sprintf(__('Disabling %s'), $sanitizedDisplay->getString('display')));
|
||||
$update->execute(['displayId' => $display['displayId']]);
|
||||
|
||||
$this->log->audit('Display', $display['displayId'], 'Regular Maintenance unauthorised display due to max number of slots exceeded.', ['display' => $display['display']]);
|
||||
|
||||
$difference--;
|
||||
}
|
||||
}
|
||||
|
||||
$this->runMessage .= ' - Done' . PHP_EOL . PHP_EOL;
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
$this->log->error($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wake on LAN
|
||||
*/
|
||||
private function wakeOnLan()
|
||||
{
|
||||
$this->runMessage = '# ' . __('Wake On LAN') . PHP_EOL;
|
||||
|
||||
try {
|
||||
// Get a list of all displays which have WOL enabled
|
||||
foreach($this->displayFactory->query(null, ['wakeOnLan' => 1]) as $display) {
|
||||
/** @var \Xibo\Entity\Display $display */
|
||||
// Time to WOL (with respect to today)
|
||||
$timeToWake = strtotime(date('Y-m-d') . ' ' . $display->wakeOnLanTime);
|
||||
$timeNow = Carbon::now()->format('U');
|
||||
|
||||
// Should the display be awake?
|
||||
if ($timeNow >= $timeToWake) {
|
||||
// Client should be awake, so has this displays WOL time been passed
|
||||
if ($display->lastWakeOnLanCommandSent < $timeToWake) {
|
||||
// Call the Wake On Lan method of the display object
|
||||
if ($display->macAddress == '' || $display->broadCastAddress == '') {
|
||||
$this->log->error('This display has no mac address recorded against it yet. Make sure the display is running.');
|
||||
$this->runMessage .= ' - ' . $display->display . ' Did not send MAC address yet' . PHP_EOL;
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->log->notice('About to send WOL packet to ' . $display->broadCastAddress . ' with Mac Address ' . $display->macAddress);
|
||||
|
||||
try {
|
||||
WakeOnLan::TransmitWakeOnLan($display->macAddress, $display->secureOn, $display->broadCastAddress, $display->cidr, '9', $this->log);
|
||||
$this->runMessage .= ' - ' . $display->display . ' Sent WOL Message. Previous WOL send time: ' . Carbon::createFromTimestamp($display->lastWakeOnLanCommandSent)->format(DateFormatHelper::getSystemFormat()) . PHP_EOL;
|
||||
|
||||
$display->lastWakeOnLanCommandSent = Carbon::now()->format('U');
|
||||
$display->save(['validate' => false, 'audit' => true]);
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
$this->runMessage .= ' - ' . $display->display . ' Error=' . $e->getMessage() . PHP_EOL;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$this->runMessage .= ' - ' . $display->display . ' Display already awake. Previous WOL send time: ' . Carbon::createFromTimestamp($display->lastWakeOnLanCommandSent)->format(DateFormatHelper::getSystemFormat()) . PHP_EOL;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$this->runMessage .= ' - ' . $display->display . ' Sleeping' . PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
$this->runMessage .= ' - Done' . PHP_EOL . PHP_EOL;
|
||||
}
|
||||
catch (\PDOException $e) {
|
||||
$this->log->error($e->getMessage());
|
||||
$this->runMessage .= ' - Error' . PHP_EOL . PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build layouts
|
||||
*/
|
||||
private function buildLayouts()
|
||||
{
|
||||
$this->runMessage .= '## ' . __('Build Layouts') . PHP_EOL;
|
||||
|
||||
// Build Layouts
|
||||
// We do not want to build any draft Layouts - they are built in the Layout Designer or on Publish
|
||||
foreach ($this->layoutFactory->query(null, ['status' => 3, 'showDrafts' => 0, 'disableUserCheck' => 1]) as $layout) {
|
||||
/* @var \Xibo\Entity\Layout $layout */
|
||||
try {
|
||||
$layout = $this->layoutFactory->concurrentRequestLock($layout);
|
||||
try {
|
||||
$layout->xlfToDisk(['notify' => true]);
|
||||
|
||||
// Commit after each build
|
||||
// https://github.com/xibosignage/xibo/issues/1593
|
||||
$this->store->commitIfNecessary();
|
||||
} finally {
|
||||
$this->layoutFactory->concurrentRequestRelease($layout);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->log->error(sprintf('Maintenance cannot build Layout %d, %s.', $layout->layoutId, $e->getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
$this->runMessage .= ' - Done' . PHP_EOL . PHP_EOL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tidy library
|
||||
*/
|
||||
private function tidyLibrary()
|
||||
{
|
||||
$this->runMessage .= '## ' . __('Tidy Library') . PHP_EOL;
|
||||
|
||||
// Keep tidy
|
||||
$this->mediaService->removeExpiredFiles();
|
||||
$this->mediaService->removeTempFiles();
|
||||
|
||||
$this->runMessage .= ' - Done' . PHP_EOL . PHP_EOL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check library usage
|
||||
* @throws \Xibo\Support\Exception\NotFoundException
|
||||
* @throws \Xibo\Support\Exception\InvalidArgumentException
|
||||
*/
|
||||
private function checkLibraryUsage()
|
||||
{
|
||||
$libraryLimit = $this->config->getSetting('LIBRARY_SIZE_LIMIT_KB') * 1024;
|
||||
|
||||
if ($libraryLimit <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$results = $this->store->select('SELECT IFNULL(SUM(FileSize), 0) AS SumSize FROM media', []);
|
||||
|
||||
$sanitizedResults = $this->getSanitizer($results);
|
||||
|
||||
$size = $sanitizedResults->getInt('SumSize');
|
||||
|
||||
if ($size >= $libraryLimit) {
|
||||
// Create a notification if we don't already have one today for this display.
|
||||
$subject = __('Library allowance exceeded');
|
||||
$date = Carbon::now();
|
||||
$notifications = $this->notificationFactory->getBySubjectAndDate(
|
||||
$subject,
|
||||
$date->startOfDay()->format('U'),
|
||||
$date->addDay()->startOfDay()->format('U')
|
||||
);
|
||||
|
||||
if (count($notifications) <= 0) {
|
||||
$body = __(
|
||||
sprintf(
|
||||
'Library allowance of %s exceeded. Used %s',
|
||||
ByteFormatter::format($libraryLimit),
|
||||
ByteFormatter::format($size)
|
||||
)
|
||||
);
|
||||
|
||||
$notification = $this->notificationFactory->createSystemNotification(
|
||||
$subject,
|
||||
$body,
|
||||
Carbon::now(),
|
||||
'library'
|
||||
);
|
||||
|
||||
$notification->save();
|
||||
|
||||
$this->log->critical($subject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if there are any overrequested files.
|
||||
* @throws \Xibo\Support\Exception\NotFoundException
|
||||
* @throws \Xibo\Support\Exception\InvalidArgumentException
|
||||
*/
|
||||
private function checkOverRequestedFiles()
|
||||
{
|
||||
$items = $this->store->select('
|
||||
SELECT display.displayId,
|
||||
display.display,
|
||||
COUNT(*) AS countFiles
|
||||
FROM `requiredfile`
|
||||
INNER JOIN `display`
|
||||
ON display.displayId = requiredfile.displayId
|
||||
WHERE `bytesRequested` > 0
|
||||
AND `requiredfile`.bytesRequested >= `requiredfile`.`size` * :factor
|
||||
AND `requiredfile`.type NOT IN (\'W\', \'D\')
|
||||
AND display.lastAccessed > :lastAccessed
|
||||
AND `requiredfile`.complete = 0
|
||||
GROUP BY display.displayId, display.display
|
||||
', [
|
||||
'factor' => 3,
|
||||
'lastAccessed' => Carbon::now()->subDay()->format('U'),
|
||||
]);
|
||||
|
||||
foreach ($items as $item) {
|
||||
$sanitizedItem = $this->getSanitizer($item);
|
||||
// Create a notification if we don't already have one today for this display.
|
||||
$subject = sprintf(
|
||||
__('%s is downloading %d files too many times'),
|
||||
$sanitizedItem->getString('display'),
|
||||
$sanitizedItem->getInt('countFiles')
|
||||
);
|
||||
$date = Carbon::now();
|
||||
$notifications = $this->notificationFactory->getBySubjectAndDate(
|
||||
$subject,
|
||||
$date->startOfDay()->format('U'),
|
||||
$date->addDay()->startOfDay()->format('U')
|
||||
);
|
||||
|
||||
if (count($notifications) <= 0) {
|
||||
$body = sprintf(
|
||||
__('Please check the bandwidth graphs and display status for %s to investigate the issue.'),
|
||||
$sanitizedItem->getString('display')
|
||||
);
|
||||
|
||||
$notification = $this->notificationFactory->createSystemNotification(
|
||||
$subject,
|
||||
$body,
|
||||
Carbon::now(),
|
||||
'display'
|
||||
);
|
||||
|
||||
$display = $this->displayFactory->getById($item['displayId']);
|
||||
|
||||
// Add in any displayNotificationGroups, with permissions
|
||||
foreach ($this->userGroupFactory->getDisplayNotificationGroups($display->displayGroupId) as $group) {
|
||||
$notification->assignUserGroup($group);
|
||||
}
|
||||
|
||||
$notification->save();
|
||||
|
||||
$this->log->critical($subject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Playlist Durations
|
||||
*/
|
||||
private function updatePlaylistDurations()
|
||||
{
|
||||
$this->runMessage .= '## ' . __('Playlist Duration Updates') . PHP_EOL;
|
||||
|
||||
// Build Layouts
|
||||
foreach ($this->playlistFactory->query(null, ['requiresDurationUpdate' => 1]) as $playlist) {
|
||||
try {
|
||||
$playlist->setModuleFactory($this->moduleFactory);
|
||||
$playlist->updateDuration();
|
||||
} catch (GeneralException $xiboException) {
|
||||
$this->log->error(
|
||||
'Maintenance cannot update Playlist ' . $playlist->playlistId .
|
||||
', ' . $xiboException->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$this->runMessage .= ' - Done' . PHP_EOL . PHP_EOL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish layouts with set publishedDate
|
||||
* @throws GeneralException
|
||||
*/
|
||||
private function publishLayouts()
|
||||
{
|
||||
$this->runMessage .= '## ' . __('Publishing layouts with set publish dates') . PHP_EOL;
|
||||
|
||||
$layouts = $this->layoutFactory->query(
|
||||
null,
|
||||
['havePublishDate' => 1, 'disableUserCheck' => 1, 'excludeTemplates' => -1]
|
||||
);
|
||||
|
||||
// check if we have any layouts with set publish date
|
||||
if (count($layouts) > 0) {
|
||||
foreach ($layouts as $layout) {
|
||||
// check if the layout should be published now according to the date
|
||||
if (Carbon::createFromFormat(DateFormatHelper::getSystemFormat(), $layout->publishedDate)
|
||||
->isBefore(Carbon::now()->format(DateFormatHelper::getSystemFormat()))
|
||||
) {
|
||||
try {
|
||||
// publish the layout
|
||||
$layout = $this->layoutFactory->concurrentRequestLock($layout, true);
|
||||
try {
|
||||
$draft = $this->layoutFactory->getByParentId($layout->layoutId);
|
||||
if ($draft->status === Status::$STATUS_INVALID
|
||||
&& isset($draft->statusMessage)
|
||||
&& (
|
||||
count($draft->getStatusMessage()) > 1 ||
|
||||
count($draft->getStatusMessage()) === 1 &&
|
||||
!$draft->checkForEmptyRegion()
|
||||
)
|
||||
) {
|
||||
throw new GeneralException(json_encode($draft->statusMessage));
|
||||
}
|
||||
$draft->publishDraft();
|
||||
$draft->load();
|
||||
$draft->xlfToDisk([
|
||||
'notify' => true,
|
||||
'exceptionOnError' => true,
|
||||
'exceptionOnEmptyRegion' => false
|
||||
]);
|
||||
} finally {
|
||||
$this->layoutFactory->concurrentRequestRelease($layout, true);
|
||||
}
|
||||
$this->log->info(
|
||||
'Published layout ID ' . $layout->layoutId . ' new layout id is ' . $draft->layoutId
|
||||
);
|
||||
} catch (GeneralException $e) {
|
||||
$this->log->error(
|
||||
'Error publishing layout ID ' . $layout->layoutId .
|
||||
' with name ' . $layout->layout . ' Failed with message: ' . $e->getMessage()
|
||||
);
|
||||
|
||||
// create a notification
|
||||
$subject = __(sprintf('Error publishing layout ID %d', $layout->layoutId));
|
||||
$date = Carbon::now();
|
||||
|
||||
$notifications = $this->notificationFactory->getBySubjectAndDate(
|
||||
$subject,
|
||||
$date->startOfDay()->format('U'),
|
||||
$date->addDay()->startOfDay()->format('U')
|
||||
);
|
||||
|
||||
if (count($notifications) <= 0) {
|
||||
$body = __(
|
||||
sprintf(
|
||||
'Publishing layout ID %d with name %s failed. With message %s',
|
||||
$layout->layoutId,
|
||||
$layout->layout,
|
||||
$e->getMessage()
|
||||
)
|
||||
);
|
||||
|
||||
$notification = $this->notificationFactory->createSystemNotification(
|
||||
$subject,
|
||||
$body,
|
||||
Carbon::now(),
|
||||
'layout'
|
||||
);
|
||||
$notification->save();
|
||||
|
||||
$this->log->critical($subject);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->log->debug(
|
||||
'Layouts with published date were found, they are set to publish later than current time'
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->log->debug('No layouts to publish.');
|
||||
}
|
||||
|
||||
$this->runMessage .= ' - Done' . PHP_EOL . PHP_EOL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assess any eligible dynamic display groups if necessary
|
||||
* @return void
|
||||
* @throws \Xibo\Support\Exception\NotFoundException
|
||||
*/
|
||||
private function assessDynamicDisplayGroups(): void
|
||||
{
|
||||
$this->runMessage .= '## ' . __('Assess Dynamic Display Groups') . PHP_EOL;
|
||||
|
||||
// Do we have a cache key set to say that dynamic display group assessment has been completed?
|
||||
$cache = $this->pool->getItem('DYNAMIC_DISPLAY_GROUP_ASSESSED');
|
||||
if ($cache->isMiss()) {
|
||||
Profiler::start('RegularMaintenance::assessDynamicDisplayGroups', $this->log);
|
||||
|
||||
// Set the cache key with a long expiry and save.
|
||||
$cache->set(true);
|
||||
$cache->expiresAt(Carbon::now()->addYear());
|
||||
$this->pool->save($cache);
|
||||
|
||||
// Process each dynamic display group
|
||||
$count = 0;
|
||||
|
||||
foreach ($this->displayGroupFactory->getByIsDynamic(1) as $group) {
|
||||
$count++;
|
||||
try {
|
||||
// Loads displays.
|
||||
$this->getDispatcher()->dispatch(
|
||||
new DisplayGroupLoadEvent($group),
|
||||
DisplayGroupLoadEvent::$NAME
|
||||
);
|
||||
$group->save([
|
||||
'validate' => false,
|
||||
'saveGroup' => false,
|
||||
'saveTags' => false,
|
||||
'manageLinks' => false,
|
||||
'manageDisplayLinks' => false,
|
||||
'manageDynamicDisplayLinks' => true,
|
||||
'allowNotify' => true
|
||||
]);
|
||||
} catch (GeneralException $exception) {
|
||||
$this->log->error('assessDynamicDisplayGroups: Unable to manage group: '
|
||||
. $group->displayGroup);
|
||||
}
|
||||
}
|
||||
Profiler::end('RegularMaintenance::assessDynamicDisplayGroups', $this->log);
|
||||
$this->runMessage .= ' - Done ' . $count . PHP_EOL . PHP_EOL;
|
||||
} else {
|
||||
$this->runMessage .= ' - Done (not required)' . PHP_EOL . PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
private function tidyAdCampaignSchedules()
|
||||
{
|
||||
$this->runMessage .= '## ' . __('Tidy Ad Campaign Schedules') . PHP_EOL;
|
||||
Profiler::start('RegularMaintenance::tidyAdCampaignSchedules', $this->log);
|
||||
$count = 0;
|
||||
|
||||
foreach ($this->scheduleFactory->query(null, [
|
||||
'adCampaignsOnly' => 1,
|
||||
'toDt' => Carbon::now()->subDays(90)->unix()
|
||||
]) as $event) {
|
||||
if (!empty($event->parentCampaignId)) {
|
||||
$count++;
|
||||
$this->log->debug('tidyAdCampaignSchedules : Found old Ad Campaign interrupt event ID '
|
||||
. $event->eventId . ' deleting');
|
||||
$event->delete(['notify' => false]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->log->debug('tidyAdCampaignSchedules : Deleted ' . $count . ' events');
|
||||
Profiler::end('RegularMaintenance::tidyAdCampaignSchedules', $this->log);
|
||||
$this->runMessage .= ' - Done ' . $count . PHP_EOL . PHP_EOL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Once per hour assert the current XMR to push its expiry time with XMR
|
||||
* this also reseeds the key if XMR restarts
|
||||
* @return void
|
||||
*/
|
||||
private function assertXmrKey(): void
|
||||
{
|
||||
$this->log->debug('assertXmrKey: asserting key');
|
||||
try {
|
||||
$key = $this->getConfig()->getSetting('XMR_CMS_KEY');
|
||||
if (!empty($key)) {
|
||||
$client = new Client($this->config->getGuzzleProxy([
|
||||
'base_uri' => $this->getConfig()->getSetting('XMR_ADDRESS'),
|
||||
]));
|
||||
|
||||
$client->post('/', [
|
||||
'json' => [
|
||||
'id' => constant('SECRET_KEY'),
|
||||
'type' => 'keys',
|
||||
'key' => $key,
|
||||
],
|
||||
]);
|
||||
$this->log->debug('assertXmrKey: asserted key');
|
||||
} else {
|
||||
$this->log->error('assertXmrKey: key empty');
|
||||
}
|
||||
} catch (GuzzleException | \Exception $e) {
|
||||
$this->log->error('assertXmrKey: failed. E = ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes unused full screen layouts
|
||||
* @throws NotFoundException
|
||||
* @throws GeneralException
|
||||
*/
|
||||
private function tidyUnusedFullScreenLayout(): void
|
||||
{
|
||||
$this->runMessage .= '## ' . __('Tidy Unused FullScreen Layout') . PHP_EOL;
|
||||
Profiler::start('RegularMaintenance::tidyUnusedFullScreenLayout', $this->log);
|
||||
$count = 0;
|
||||
|
||||
foreach ($this->layoutFactory->query(null, [
|
||||
'filterLayoutStatusId' => 3,
|
||||
'isFullScreenCampaign' => 1
|
||||
]) as $layout) {
|
||||
$count++;
|
||||
|
||||
$this->log->debug('tidyUnusedFullScreenLayout : Found unused fullscreen layout ID '
|
||||
. $layout->layoutId . ' deleting');
|
||||
|
||||
$layout->delete();
|
||||
}
|
||||
|
||||
$this->log->debug('tidyUnusedFullScreenLayout : Deleted ' . $count . ' layouts');
|
||||
|
||||
Profiler::end('RegularMaintenance::tidyUnusedFullScreenLayout', $this->log);
|
||||
|
||||
$this->runMessage .= ' - Done ' . $count . PHP_EOL . PHP_EOL;
|
||||
}
|
||||
}
|
||||
95
lib/XTR/MediaOrientationTask.php
Normal file
95
lib/XTR/MediaOrientationTask.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (c) 2022 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - http://www.xibo.org.uk
|
||||
*
|
||||
* 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 Xibo\Factory\MediaFactory;
|
||||
|
||||
class MediaOrientationTask implements TaskInterface
|
||||
{
|
||||
use TaskTrait;
|
||||
|
||||
/**
|
||||
* @var MediaFactory
|
||||
*/
|
||||
private $mediaFactory;
|
||||
|
||||
/** @inheritdoc */
|
||||
public function setFactories($container)
|
||||
{
|
||||
$this->mediaFactory = $container->get('mediaFactory');
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function run()
|
||||
{
|
||||
$this->runMessage = '# ' . __('Media Orientation') . PHP_EOL . PHP_EOL;
|
||||
|
||||
// Long running task
|
||||
set_time_limit(0);
|
||||
|
||||
$this->setMediaOrientation();
|
||||
}
|
||||
|
||||
private function setMediaOrientation()
|
||||
{
|
||||
$this->appendRunMessage('# Setting Media Orientation on Library Media files.');
|
||||
|
||||
// onlyMenuBoardAllowed filter means images and videos
|
||||
$filesToCheck = $this->mediaFactory->query(null, ['requiresMetaUpdate' => 1, 'onlyMenuBoardAllowed' => 1]);
|
||||
$count = 0;
|
||||
|
||||
foreach ($filesToCheck as $media) {
|
||||
$count++;
|
||||
$filePath = '';
|
||||
$libraryFolder = $this->config->getSetting('LIBRARY_LOCATION');
|
||||
|
||||
if ($media->mediaType === 'image') {
|
||||
$filePath = $libraryFolder . $media->storedAs;
|
||||
} elseif ($media->mediaType === 'video' && file_exists($libraryFolder . $media->mediaId . '_videocover.png')) {
|
||||
$filePath = $libraryFolder . $media->mediaId . '_videocover.png';
|
||||
}
|
||||
|
||||
if (!empty($filePath)) {
|
||||
list($imgWidth, $imgHeight) = @getimagesize($filePath);
|
||||
$media->width = $imgWidth;
|
||||
$media->height = $imgHeight;
|
||||
$media->orientation = ($imgWidth >= $imgHeight) ? 'landscape' : 'portrait';
|
||||
$media->save(['saveTags' => false, 'validate' => false]);
|
||||
}
|
||||
}
|
||||
$this->appendRunMessage('Updated ' . $count . ' items');
|
||||
$this->disableTask();
|
||||
}
|
||||
|
||||
private function disableTask()
|
||||
{
|
||||
$this->appendRunMessage('# Disabling task.');
|
||||
$this->log->debug('Disabling task.');
|
||||
|
||||
$this->getTask()->isActive = 0;
|
||||
$this->getTask()->save();
|
||||
|
||||
$this->appendRunMessage(__('Done.'. PHP_EOL));
|
||||
}
|
||||
}
|
||||
117
lib/XTR/NotificationTidyTask.php
Normal file
117
lib/XTR/NotificationTidyTask.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
/*
|
||||
* Spring Signage Ltd - http://www.springsignage.com
|
||||
* Copyright (C) 2016 Spring Signage Ltd
|
||||
* (NotificationTidyTask.php)
|
||||
*/
|
||||
|
||||
|
||||
namespace Xibo\XTR;
|
||||
|
||||
use Carbon\Carbon;
|
||||
|
||||
/**
|
||||
* Class NotificationTidyTask
|
||||
* @package Xibo\XTR
|
||||
*/
|
||||
class NotificationTidyTask implements TaskInterface
|
||||
{
|
||||
use TaskTrait;
|
||||
|
||||
/** @inheritdoc */
|
||||
public function setFactories($container)
|
||||
{
|
||||
// No factories required
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function run()
|
||||
{
|
||||
// Delete notifications older than X days
|
||||
$maxAgeDays = intval($this->getOption('maxAgeDays', 7));
|
||||
$systemOnly = intval($this->getOption('systemOnly', 1));
|
||||
$readOnly = intval($this->getOption('readOnly', 0));
|
||||
|
||||
$this->runMessage = '# ' . __('Notification Tidy') . PHP_EOL . PHP_EOL;
|
||||
|
||||
$this->log->info('Deleting notifications older than ' . $maxAgeDays
|
||||
. ' days. System Only: ' . $systemOnly
|
||||
. '. Read Only' . $readOnly
|
||||
);
|
||||
|
||||
// Where clause
|
||||
$where = ' WHERE `releaseDt` < :releaseDt ';
|
||||
if ($systemOnly == 1) {
|
||||
$where .= ' AND `isSystem` = 1 ';
|
||||
}
|
||||
|
||||
// Params for all deletes
|
||||
$params = [
|
||||
'releaseDt' => Carbon::now()->subDays($maxAgeDays)->format('U')
|
||||
];
|
||||
|
||||
// Delete all notifications older than now minus X days
|
||||
$sql = '
|
||||
DELETE FROM `lknotificationdg`
|
||||
WHERE `notificationId` IN (SELECT DISTINCT `notificationId` FROM `notification` ' . $where . ')
|
||||
';
|
||||
|
||||
if ($readOnly == 1) {
|
||||
$sql .= ' AND `notificationId` IN (SELECT `notificationId` FROM `lknotificationuser` WHERE read <> 0) ';
|
||||
}
|
||||
|
||||
$this->store->update($sql, $params);
|
||||
|
||||
// Delete notification groups
|
||||
$sql = '
|
||||
DELETE FROM `lknotificationgroup`
|
||||
WHERE `notificationId` IN (SELECT DISTINCT `notificationId` FROM `notification` ' . $where . ')
|
||||
';
|
||||
|
||||
if ($readOnly == 1) {
|
||||
$sql .= ' AND `notificationId` IN (SELECT `notificationId` FROM `lknotificationuser` WHERE read <> 0) ';
|
||||
}
|
||||
|
||||
$this->store->update($sql, $params);
|
||||
|
||||
// Delete from notification user
|
||||
$sql = '
|
||||
DELETE FROM `lknotificationuser`
|
||||
WHERE `notificationId` IN (SELECT DISTINCT `notificationId` FROM `notification` ' . $where . ')
|
||||
';
|
||||
|
||||
if ($readOnly == 1) {
|
||||
$sql .= ' AND `read` <> 0 ';
|
||||
}
|
||||
|
||||
$this->store->update($sql, $params);
|
||||
|
||||
// Remove the attached file
|
||||
$sql = 'SELECT filename FROM `notification` ' . $where;
|
||||
|
||||
foreach ($this->store->select($sql, $params) as $row) {
|
||||
$filename = $row['filename'];
|
||||
|
||||
/*Delete the attachment*/
|
||||
if (!empty($filename)) {
|
||||
// Library location
|
||||
$attachmentLocation = $this->config->getSetting('LIBRARY_LOCATION'). 'attachment/';
|
||||
if (file_exists($attachmentLocation . $filename)) {
|
||||
unlink($attachmentLocation . $filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete from notification
|
||||
$sql = 'DELETE FROM `notification` ' . $where;
|
||||
|
||||
if ($readOnly == 1) {
|
||||
$sql .= ' AND `notificationId` NOT IN (SELECT `notificationId` FROM `lknotificationuser`) ';
|
||||
}
|
||||
|
||||
$this->store->update($sql, $params);
|
||||
|
||||
$this->runMessage .= __('Done') . PHP_EOL . PHP_EOL;
|
||||
}
|
||||
}
|
||||
61
lib/XTR/PurgeListCleanupTask.php
Normal file
61
lib/XTR/PurgeListCleanupTask.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
/**
|
||||
* Copyright (C) 2021 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - http://www.xibo.org.uk
|
||||
*
|
||||
* 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 Xibo\Helper\DateFormatHelper;
|
||||
|
||||
class PurgeListCleanupTask implements TaskInterface
|
||||
{
|
||||
use TaskTrait;
|
||||
|
||||
/** @inheritdoc */
|
||||
public function setFactories($container)
|
||||
{
|
||||
$this->sanitizerService = $container->get('sanitizerService');
|
||||
$this->store = $container->get('store');
|
||||
$this->config = $container->get('configService');
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function run()
|
||||
{
|
||||
$this->tidyPurgeList();
|
||||
}
|
||||
|
||||
public function tidyPurgeList()
|
||||
{
|
||||
$this->runMessage = '# ' . __('Purge List Cleanup Start') . PHP_EOL . PHP_EOL;
|
||||
|
||||
$count = $this->store->update('DELETE FROM `purge_list` WHERE expiryDate < :expiryDate', [
|
||||
'expiryDate' => Carbon::now()->format(DateFormatHelper::getSystemFormat())
|
||||
]);
|
||||
|
||||
if ($count <= 0) {
|
||||
$this->appendRunMessage('# ' . __('Nothing to remove') . PHP_EOL . PHP_EOL);
|
||||
} else {
|
||||
$this->appendRunMessage('# ' . sprintf(__('Removed %d rows'), $count) . PHP_EOL . PHP_EOL);
|
||||
}
|
||||
}
|
||||
}
|
||||
280
lib/XTR/RemoteDataSetFetchTask.php
Normal file
280
lib/XTR/RemoteDataSetFetchTask.php
Normal file
@@ -0,0 +1,280 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2023 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 Xibo\Entity\DataSet;
|
||||
use Xibo\Factory\DataSetFactory;
|
||||
use Xibo\Factory\NotificationFactory;
|
||||
use Xibo\Factory\UserFactory;
|
||||
use Xibo\Factory\UserGroupFactory;
|
||||
use Xibo\Support\Exception\GeneralException;
|
||||
|
||||
/**
|
||||
* Class RemoteDataSetFetchTask
|
||||
* @package Xibo\XTR
|
||||
*/
|
||||
class RemoteDataSetFetchTask implements TaskInterface
|
||||
{
|
||||
use TaskTrait;
|
||||
|
||||
/** @var DataSetFactory */
|
||||
private $dataSetFactory;
|
||||
|
||||
/** @var NotificationFactory */
|
||||
private $notificationFactory;
|
||||
|
||||
/** @var UserFactory */
|
||||
private $userFactory;
|
||||
|
||||
/** @var UserGroupFactory */
|
||||
private $userGroupFactory;
|
||||
|
||||
/** @inheritdoc */
|
||||
public function setFactories($container)
|
||||
{
|
||||
$this->dataSetFactory = $container->get('dataSetFactory');
|
||||
$this->notificationFactory = $container->get('notificationFactory');
|
||||
$this->userFactory = $container->get('userFactory');
|
||||
$this->userGroupFactory = $container->get('userGroupFactory');
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function run()
|
||||
{
|
||||
$this->runMessage = '# ' . __('Fetching Remote-DataSets') . PHP_EOL . PHP_EOL;
|
||||
|
||||
$runTime = Carbon::now()->format('U');
|
||||
|
||||
/** @var DataSet $dataSet */
|
||||
$dataSet = null;
|
||||
|
||||
// Process all Remote DataSets (and their dependants)
|
||||
$dataSets = $this->orderDataSetsByDependency($this->dataSetFactory->query(null, ['isRemote' => 1]));
|
||||
|
||||
// Log the order.
|
||||
$this->log->debug(
|
||||
'Order of processing: ' . json_encode(array_map(function ($element) {
|
||||
return $element->dataSetId . ' - ' . $element->runsAfter;
|
||||
}, $dataSets))
|
||||
);
|
||||
|
||||
// Reorder this list according to which order we want to run in
|
||||
foreach ($dataSets as $dataSet) {
|
||||
$this->log->debug('Processing ' . $dataSet->dataSet . '. ID:' . $dataSet->dataSetId);
|
||||
$hardRowLimit = $this->config->getSetting('DATASET_HARD_ROW_LIMIT');
|
||||
$softRowLimit = $dataSet->rowLimit;
|
||||
$limitPolicy = $dataSet->limitPolicy;
|
||||
$currentNumberOfRows = intval($this->store->select('SELECT COUNT(*) AS total FROM `dataset_' . $dataSet->dataSetId . '`', [])[0]['total']);
|
||||
|
||||
try {
|
||||
// Has this dataSet been accessed recently?
|
||||
if (!$dataSet->isActive()) {
|
||||
// Skipping dataSet due to it not being accessed recently
|
||||
$this->log->info('Skipping dataSet ' . $dataSet->dataSetId . ' due to it not being accessed recently');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get all columns
|
||||
$columns = $dataSet->getColumn();
|
||||
|
||||
// Filter columns where dataSetColumnType is "Remote"
|
||||
$filteredColumns = array_filter($columns, function ($column) {
|
||||
return $column->dataSetColumnTypeId == '3';
|
||||
});
|
||||
|
||||
// Check if there are any remote columns defined in the dataset
|
||||
if (count($filteredColumns) === 0) {
|
||||
$this->log->info('Skipping dataSet ' . $dataSet->dataSetId . ': No remote columns defined in the dataset.');
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->log->debug('Comparing run time ' . $runTime . ' to next sync time ' . $dataSet->getNextSyncTime());
|
||||
|
||||
if ($runTime >= $dataSet->getNextSyncTime()) {
|
||||
// Getting the dependant DataSet to process the current DataSet on
|
||||
$dependant = null;
|
||||
if ($dataSet->runsAfter != null && $dataSet->runsAfter != $dataSet->dataSetId) {
|
||||
$dependant = $this->dataSetFactory->getById($dataSet->runsAfter);
|
||||
}
|
||||
|
||||
$this->log->debug('Fetch and process ' . $dataSet->dataSet);
|
||||
|
||||
$results = $this->dataSetFactory->callRemoteService($dataSet, $dependant);
|
||||
|
||||
if ($results->number > 0) {
|
||||
// Truncate only if we also fetch new Data
|
||||
if ($dataSet->isTruncateEnabled()
|
||||
&& $results->isEligibleToTruncate
|
||||
&& $runTime >= $dataSet->getNextClearTime()
|
||||
) {
|
||||
$this->log->debug('Truncate ' . $dataSet->dataSet);
|
||||
$dataSet->deleteData();
|
||||
|
||||
// Update the last clear time.
|
||||
$dataSet->saveLastClear($runTime);
|
||||
}
|
||||
|
||||
$rowsToAdd = $results->number;
|
||||
$this->log->debug('Current number of rows in DataSet ID ' . $dataSet->dataSetId . ' is: ' . $currentNumberOfRows . ' number of records to add ' . $rowsToAdd);
|
||||
|
||||
// row limit reached
|
||||
if ($currentNumberOfRows + $rowsToAdd >= $hardRowLimit || $softRowLimit != null && $currentNumberOfRows + $rowsToAdd >= $softRowLimit) {
|
||||
// handle remote DataSets created before introduction of limit policy
|
||||
if ($limitPolicy == null) {
|
||||
$this->log->debug('No limit policy set, default to stop syncing.');
|
||||
$limitPolicy = 'stop';
|
||||
}
|
||||
|
||||
// which limit policy was set?
|
||||
if ($limitPolicy === 'stop') {
|
||||
$this->log->info('DataSet ID ' . $dataSet->dataSetId . ' reached the row limit, due to selected limit policy, it will stop syncing');
|
||||
continue;
|
||||
} elseif ($limitPolicy === 'fifo') {
|
||||
// FiFo
|
||||
$this->log->info('DataSet ID ' . $dataSet->dataSetId . ' reached the row limit, due to selected limit policy, oldest rows will be removed');
|
||||
|
||||
$this->store->update('DELETE FROM `dataset_' . $dataSet->dataSetId . '` ORDER BY id ASC LIMIT ' . $rowsToAdd, []);
|
||||
} elseif ($limitPolicy === 'truncate') {
|
||||
// truncate
|
||||
$this->log->info('DataSet ID ' . $dataSet->dataSetId . ' reached the row limit, due to selected limit policy, we will truncate the DataSet data');
|
||||
$dataSet->deleteData();
|
||||
|
||||
// Update the last clear time.
|
||||
$dataSet->saveLastClear($runTime);
|
||||
}
|
||||
}
|
||||
|
||||
if ($dataSet->sourceId === 1) {
|
||||
$this->dataSetFactory->processResults($dataSet, $results);
|
||||
} else {
|
||||
$this->dataSetFactory->processCsvEntries($dataSet, $results);
|
||||
}
|
||||
|
||||
// notify here
|
||||
$dataSet->notify();
|
||||
} else if ($dataSet->truncateOnEmpty
|
||||
&& $results->isEligibleToTruncate
|
||||
&& $dataSet->isTruncateEnabled()
|
||||
&& $runTime >= $dataSet->getNextClearTime()
|
||||
) {
|
||||
$this->log->debug('Truncate ' . $dataSet->dataSet);
|
||||
$dataSet->deleteData();
|
||||
|
||||
// Update the last clear time.
|
||||
$dataSet->saveLastClear($runTime);
|
||||
$this->appendRunMessage(__('No results for %s, truncate with no new data enabled', $dataSet->dataSet));
|
||||
} else {
|
||||
$this->appendRunMessage(__('No results for %s', $dataSet->dataSet));
|
||||
}
|
||||
|
||||
$dataSet->saveLastSync($runTime);
|
||||
} else {
|
||||
$this->log->debug('Sync not required for ' . $dataSet->dataSetId);
|
||||
}
|
||||
} catch (GeneralException $e) {
|
||||
$this->appendRunMessage(__('Error syncing DataSet %s', $dataSet->dataSet));
|
||||
$this->log->error('Error syncing DataSet ' . $dataSet->dataSetId . '. E = ' . $e->getMessage());
|
||||
$this->log->debug($e->getTraceAsString());
|
||||
|
||||
// Send a notification to the dataSet owner, informing them of the failure.
|
||||
$notification = $this->notificationFactory->createEmpty();
|
||||
$notification->subject = __('Remote DataSet %s failed to synchronise', $dataSet->dataSet);
|
||||
$notification->body = 'The error is: ' . $e->getMessage();
|
||||
$notification->createDt = Carbon::now()->format('U');
|
||||
$notification->releaseDt = $notification->createDt;
|
||||
$notification->isInterrupt = 0;
|
||||
$notification->userId = $this->user->userId;
|
||||
$notification->type = 'dataset';
|
||||
|
||||
// Assign me
|
||||
$dataSetUser = $this->userFactory->getById($dataSet->userId);
|
||||
$notification->assignUserGroup($this->userGroupFactory->getById($dataSetUser->groupId));
|
||||
|
||||
// Send
|
||||
$notification->save();
|
||||
|
||||
// You might say at this point that if there are other data sets further down the list, we shouldn't
|
||||
// continue because they might depend directly on this one
|
||||
// however, it is my opinion that they should be processed anyway with the current cache of data.
|
||||
// hence continue
|
||||
}
|
||||
}
|
||||
|
||||
$this->appendRunMessage(__('Done'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Order the list of DataSets to be processed so that it is dependent aware.
|
||||
*
|
||||
* @param DataSet[] $dataSets Reference to an Array which holds all not yet processed DataSets
|
||||
* @return DataSet[] Ordered list of DataSets to process
|
||||
*
|
||||
*
|
||||
* What is going on here: RemoteDataSets can depend on others, so we have to be sure to fetch
|
||||
* the data from the dependant first.
|
||||
* For Example (id, dependant): (1,4), (2,3), (3,4), (4,1), (5,2), (6,6)
|
||||
* Should be processed like: 4, 1, 3, 2, 5, 6
|
||||
*
|
||||
*/
|
||||
private function orderDataSetsByDependency(array $dataSets)
|
||||
{
|
||||
// DataSets are in no particular order
|
||||
// sort them according to their dependencies
|
||||
usort($dataSets, function ($a, $b) {
|
||||
/** @var DataSet $a */
|
||||
/** @var DataSet $b */
|
||||
// if a doesn't have a dependent, then a must be lower in the list (move b up)
|
||||
if ($a->runsAfter === null) {
|
||||
return -1;
|
||||
}
|
||||
// if b doesn't have a dependent, then a must be higher in the list (move b down)
|
||||
if ($b->runsAfter === null) {
|
||||
return 1;
|
||||
}
|
||||
// either a or b have a dependent
|
||||
// if they are the same, keep them where they are
|
||||
if ($a->runsAfter === $b->runsAfter) {
|
||||
return 0;
|
||||
}
|
||||
// the dependents are different.
|
||||
// if a depends on b, then move b up
|
||||
if ($a->runsAfter === $b->dataSetId) {
|
||||
return -1;
|
||||
}
|
||||
// if b depends on a, then move b down
|
||||
if ($b->runsAfter === $a->dataSetId) {
|
||||
return 1;
|
||||
}
|
||||
// Unsorted
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Process in reverse order (LastIn-FirstOut)
|
||||
return array_reverse($dataSets);
|
||||
}
|
||||
}
|
||||
73
lib/XTR/RemoveOldScreenshotsTask.php
Normal file
73
lib/XTR/RemoveOldScreenshotsTask.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
/**
|
||||
* Copyright (C) 2020 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - http://www.xibo.org.uk
|
||||
*
|
||||
* 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 Xibo\Factory\MediaFactory;
|
||||
|
||||
class RemoveOldScreenshotsTask implements TaskInterface
|
||||
{
|
||||
use TaskTrait;
|
||||
|
||||
/** @var MediaFactory */
|
||||
private $mediaFactory;
|
||||
|
||||
/** @inheritdoc */
|
||||
public function setFactories($container)
|
||||
{
|
||||
$this->mediaFactory = $container->get('mediaFactory');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function run()
|
||||
{
|
||||
$this->runMessage = '# ' . __('Remove Old Screenshots') . PHP_EOL . PHP_EOL;
|
||||
|
||||
$screenshotLocation = $this->config->getSetting('LIBRARY_LOCATION') . 'screenshots/';
|
||||
$screenshotTTL = $this->config->getSetting('DISPLAY_SCREENSHOT_TTL');
|
||||
$count = 0;
|
||||
|
||||
if ($screenshotTTL > 0) {
|
||||
foreach (array_diff(scandir($screenshotLocation), ['..', '.']) as $file) {
|
||||
$fileLocation = $screenshotLocation . $file;
|
||||
|
||||
$lastModified = Carbon::createFromTimestamp(filemtime($fileLocation));
|
||||
$now = Carbon::now();
|
||||
$diff = $now->diffInDays($lastModified);
|
||||
|
||||
if ($diff > $screenshotTTL) {
|
||||
unlink($fileLocation);
|
||||
$count++;
|
||||
|
||||
$this->log->debug('Removed old Display screenshot:' . $file);
|
||||
}
|
||||
}
|
||||
$this->appendRunMessage(sprintf(__('Removed %d old Display screenshots'), $count));
|
||||
} else {
|
||||
$this->appendRunMessage(__('Display Screenshot Time to keep set to 0, nothing to remove.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
371
lib/XTR/ReportScheduleTask.php
Normal file
371
lib/XTR/ReportScheduleTask.php
Normal file
@@ -0,0 +1,371 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
241
lib/XTR/ScheduleReminderTask.php
Normal file
241
lib/XTR/ScheduleReminderTask.php
Normal file
@@ -0,0 +1,241 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2023 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 Xibo\Entity\ScheduleReminder;
|
||||
use Xibo\Factory\CampaignFactory;
|
||||
use Xibo\Factory\NotificationFactory;
|
||||
use Xibo\Factory\ScheduleFactory;
|
||||
use Xibo\Factory\ScheduleReminderFactory;
|
||||
use Xibo\Factory\UserFactory;
|
||||
use Xibo\Factory\UserGroupFactory;
|
||||
use Xibo\Support\Exception\NotFoundException;
|
||||
|
||||
/**
|
||||
* Class ScheduleReminderTask
|
||||
* @package Xibo\XTR
|
||||
*/
|
||||
class ScheduleReminderTask implements TaskInterface
|
||||
{
|
||||
use TaskTrait;
|
||||
|
||||
/** @var UserFactory */
|
||||
private $userFactory;
|
||||
|
||||
/** @var ScheduleFactory */
|
||||
private $scheduleFactory;
|
||||
|
||||
/** @var CampaignFactory */
|
||||
private $campaignFactory;
|
||||
|
||||
/** @var ScheduleReminderFactory */
|
||||
private $scheduleReminderFactory;
|
||||
|
||||
/** @var NotificationFactory */
|
||||
private $notificationFactory;
|
||||
|
||||
/** @var UserGroupFactory */
|
||||
private $userGroupFactory;
|
||||
|
||||
/** @inheritdoc */
|
||||
public function setFactories($container)
|
||||
{
|
||||
$this->userFactory = $container->get('userFactory');
|
||||
$this->scheduleFactory = $container->get('scheduleFactory');
|
||||
$this->campaignFactory = $container->get('campaignFactory');
|
||||
$this->scheduleReminderFactory = $container->get('scheduleReminderFactory');
|
||||
$this->notificationFactory = $container->get('notificationFactory');
|
||||
$this->userGroupFactory = $container->get('userGroupFactory');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function run()
|
||||
{
|
||||
$this->runMessage = '# ' . __('Schedule reminder') . PHP_EOL . PHP_EOL;
|
||||
|
||||
$this->runScheduleReminder();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private function runScheduleReminder()
|
||||
{
|
||||
|
||||
$task = $this->getTask();
|
||||
$nextRunDate = $task->nextRunDate();
|
||||
$task->lastRunDt = Carbon::now()->format('U');
|
||||
$task->save();
|
||||
|
||||
// Get those reminders that have reminderDt <= nextRunDate && reminderDt > lastReminderDt
|
||||
// Those which have reminderDt < lastReminderDt exclude them
|
||||
$reminders = $this->scheduleReminderFactory->getDueReminders($nextRunDate);
|
||||
|
||||
foreach($reminders as $reminder) {
|
||||
|
||||
// Get the schedule
|
||||
$schedule = $this->scheduleFactory->getById($reminder->eventId);
|
||||
$schedule->setCampaignFactory($this->campaignFactory);
|
||||
$title = $schedule->getEventTitle();
|
||||
|
||||
switch ($reminder->type) {
|
||||
case ScheduleReminder::$TYPE_MINUTE:
|
||||
$type = ScheduleReminder::$MINUTE;
|
||||
$typeText = 'Minute(s)';
|
||||
break;
|
||||
case ScheduleReminder::$TYPE_HOUR:
|
||||
$type = ScheduleReminder::$HOUR;
|
||||
$typeText = 'Hour(s)';
|
||||
break;
|
||||
case ScheduleReminder::$TYPE_DAY:
|
||||
$type = ScheduleReminder::$DAY;
|
||||
$typeText = 'Day(s)';
|
||||
break;
|
||||
case ScheduleReminder::$TYPE_WEEK:
|
||||
$type = ScheduleReminder::$WEEK;
|
||||
$typeText = 'Week(s)';
|
||||
break;
|
||||
case ScheduleReminder::$TYPE_MONTH:
|
||||
$type = ScheduleReminder::$MONTH;
|
||||
$typeText = 'Month(s)';
|
||||
break;
|
||||
default:
|
||||
$this->log->error('Unknown schedule reminder type has been provided');
|
||||
continue 2;
|
||||
}
|
||||
|
||||
switch ($reminder->option) {
|
||||
case ScheduleReminder::$OPTION_BEFORE_START:
|
||||
$typeOptionText = 'starting';
|
||||
break;
|
||||
case ScheduleReminder::$OPTION_AFTER_START:
|
||||
$typeOptionText = 'started';
|
||||
break;
|
||||
case ScheduleReminder::$OPTION_BEFORE_END:
|
||||
$typeOptionText = 'ending';
|
||||
break;
|
||||
case ScheduleReminder::$OPTION_AFTER_END:
|
||||
$typeOptionText = 'ended';
|
||||
break;
|
||||
default:
|
||||
$this->log->error('Unknown schedule reminder option has been provided');
|
||||
continue 2;
|
||||
}
|
||||
|
||||
// Create a notification
|
||||
$subject = sprintf(__("Reminder for %s"), $title);
|
||||
if ($reminder->option == ScheduleReminder::$OPTION_BEFORE_START || $reminder->option == ScheduleReminder::$OPTION_BEFORE_END) {
|
||||
$body = sprintf(__("The event (%s) is %s in %d %s"), $title, $typeOptionText, $reminder->value, $typeText);
|
||||
} elseif ($reminder->option == ScheduleReminder::$OPTION_AFTER_START || $reminder->option == ScheduleReminder::$OPTION_AFTER_END) {
|
||||
$body = sprintf(__("The event (%s) has %s %d %s ago"), $title, $typeOptionText, $reminder->value, $typeText);
|
||||
}
|
||||
|
||||
// Is this schedule a recurring event?
|
||||
if ($schedule->recurrenceType != '') {
|
||||
|
||||
$now = Carbon::now();
|
||||
$remindSeconds = $reminder->value * $type;
|
||||
|
||||
// Get the next reminder date
|
||||
$nextReminderDate = 0;
|
||||
try {
|
||||
$nextReminderDate = $schedule->getNextReminderDate($now, $reminder, $remindSeconds);
|
||||
} catch (NotFoundException $error) {
|
||||
$this->log->error('No next occurrence of reminderDt found.');
|
||||
}
|
||||
|
||||
$i = 0;
|
||||
$lastReminderDate = $reminder->reminderDt;
|
||||
while ($nextReminderDate != 0 && $nextReminderDate < $nextRunDate) {
|
||||
|
||||
// Keep the last reminder date
|
||||
$lastReminderDate = $nextReminderDate;
|
||||
|
||||
$now = Carbon::createFromTimestamp($nextReminderDate + 1);
|
||||
try {
|
||||
$nextReminderDate = $schedule->getNextReminderDate($now, $reminder, $remindSeconds);
|
||||
} catch (NotFoundException $error) {
|
||||
$nextReminderDate = 0;
|
||||
$this->log->debug('No next occurrence of reminderDt found. ReminderDt set to 0.');
|
||||
}
|
||||
|
||||
$this->createNotification($subject, $body, $reminder, $schedule, $lastReminderDate);
|
||||
|
||||
$i++;
|
||||
}
|
||||
|
||||
if ($i == 0) {
|
||||
// Create only 1 notification as the next event is outside the nextRunDt
|
||||
$this->createNotification($subject, $body, $reminder, $schedule, $reminder->reminderDt);
|
||||
$this->log->debug('Create only 1 notification as the next event is outside the nextRunDt.');
|
||||
|
||||
} else {
|
||||
$this->log->debug($i. ' notifications created.');
|
||||
}
|
||||
|
||||
$reminder->reminderDt = $nextReminderDate;
|
||||
$reminder->lastReminderDt = $lastReminderDate;
|
||||
$reminder->save();
|
||||
|
||||
} else { // one-off event
|
||||
|
||||
$this->createNotification($subject, $body, $reminder, $schedule, $reminder->reminderDt);
|
||||
|
||||
// Current reminderDt will be used as lastReminderDt
|
||||
$reminder->lastReminderDt = $reminder->reminderDt;
|
||||
}
|
||||
|
||||
// Save
|
||||
$reminder->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $subject
|
||||
* @param $body
|
||||
* @param $reminder
|
||||
* @param $schedule
|
||||
* @param null $releaseDt
|
||||
* @throws NotFoundException
|
||||
* @throws \Xibo\Support\Exception\InvalidArgumentException
|
||||
*/
|
||||
private function createNotification($subject, $body, $reminder, $schedule, $releaseDt = null) {
|
||||
|
||||
$notification = $this->notificationFactory->createEmpty();
|
||||
$notification->subject = $subject;
|
||||
$notification->body = $body;
|
||||
$notification->createDt = Carbon::now()->format('U');
|
||||
$notification->releaseDt = $releaseDt;
|
||||
$notification->isInterrupt = 0;
|
||||
$notification->userId = $schedule->userId; // event owner
|
||||
$notification->type = 'schedule';
|
||||
|
||||
// Get user group to create user notification
|
||||
$notificationUser = $this->userFactory->getById($schedule->userId);
|
||||
$notification->assignUserGroup($this->userGroupFactory->getById($notificationUser->groupId));
|
||||
|
||||
$notification->save();
|
||||
}
|
||||
}
|
||||
923
lib/XTR/SeedDatabaseTask.php
Normal file
923
lib/XTR/SeedDatabaseTask.php
Normal file
@@ -0,0 +1,923 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2023 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 Exception;
|
||||
use Xibo\Entity\Display;
|
||||
use Xibo\Entity\Schedule;
|
||||
use Xibo\Factory\CampaignFactory;
|
||||
use Xibo\Factory\CommandFactory;
|
||||
use Xibo\Factory\DataSetColumnFactory;
|
||||
use Xibo\Factory\DataSetFactory;
|
||||
use Xibo\Factory\DisplayFactory;
|
||||
use Xibo\Factory\DisplayGroupFactory;
|
||||
use Xibo\Factory\FolderFactory;
|
||||
use Xibo\Factory\FontFactory;
|
||||
use Xibo\Factory\LayoutFactory;
|
||||
use Xibo\Factory\MediaFactory;
|
||||
use Xibo\Factory\ModuleFactory;
|
||||
use Xibo\Factory\ScheduleFactory;
|
||||
use Xibo\Factory\SyncGroupFactory;
|
||||
use Xibo\Factory\TaskFactory;
|
||||
use Xibo\Factory\UserFactory;
|
||||
use Xibo\Factory\UserGroupFactory;
|
||||
use Xibo\Factory\WidgetFactory;
|
||||
use Xibo\Helper\Random;
|
||||
use Xibo\Service\MediaServiceInterface;
|
||||
use Xibo\Support\Exception\DuplicateEntityException;
|
||||
use Xibo\Support\Exception\GeneralException;
|
||||
use Xibo\Support\Exception\InvalidArgumentException;
|
||||
use Xibo\Support\Exception\NotFoundException;
|
||||
|
||||
/**
|
||||
* Class SeedDatabaseTask
|
||||
* Run only once, by default disabled
|
||||
* @package Xibo\XTR
|
||||
*/
|
||||
class SeedDatabaseTask implements TaskInterface
|
||||
{
|
||||
use TaskTrait;
|
||||
|
||||
private ModuleFactory $moduleFactory;
|
||||
private WidgetFactory $widgetFactory;
|
||||
private LayoutFactory $layoutFactory;
|
||||
private CampaignFactory $campaignFactory;
|
||||
private TaskFactory $taskFactory;
|
||||
private DisplayFactory $displayFactory;
|
||||
private DataSetFactory $dataSetFactory;
|
||||
private DataSetColumnFactory $dataSetColumnFactory;
|
||||
private SyncGroupFactory $syncGroupFactory;
|
||||
private ScheduleFactory $scheduleFactory;
|
||||
private UserFactory $userFactory;
|
||||
private UserGroupFactory $userGroupFactory;
|
||||
private FontFactory $fontFactory;
|
||||
private MediaServiceInterface $mediaService;
|
||||
/** @var array The cache for layout */
|
||||
private array $layoutCache = [];
|
||||
private FolderFactory $folderFactory;
|
||||
private CommandFactory $commandFactory;
|
||||
private DisplayGroupFactory $displayGroupFactory;
|
||||
private MediaFactory $mediaFactory;
|
||||
private array $displayGroups;
|
||||
private array $displays;
|
||||
private array $layouts;
|
||||
private array $parentCampaigns = [];
|
||||
private array $syncGroups;
|
||||
|
||||
/** @inheritdoc */
|
||||
public function setFactories($container)
|
||||
{
|
||||
$this->moduleFactory = $container->get('moduleFactory');
|
||||
$this->widgetFactory = $container->get('widgetFactory');
|
||||
$this->layoutFactory = $container->get('layoutFactory');
|
||||
$this->campaignFactory = $container->get('campaignFactory');
|
||||
$this->taskFactory = $container->get('taskFactory');
|
||||
$this->displayFactory = $container->get('displayFactory');
|
||||
$this->displayGroupFactory = $container->get('displayGroupFactory');
|
||||
$this->mediaService = $container->get('mediaService');
|
||||
$this->userFactory = $container->get('userFactory');
|
||||
$this->userGroupFactory = $container->get('userGroupFactory');
|
||||
$this->fontFactory = $container->get('fontFactory');
|
||||
$this->dataSetFactory = $container->get('dataSetFactory');
|
||||
$this->dataSetColumnFactory = $container->get('dataSetColumnFactory');
|
||||
$this->syncGroupFactory = $container->get('syncGroupFactory');
|
||||
$this->scheduleFactory = $container->get('scheduleFactory');
|
||||
$this->folderFactory = $container->get('folderFactory');
|
||||
$this->commandFactory = $container->get('commandFactory');
|
||||
$this->mediaFactory = $container->get('mediaFactory');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @inheritdoc
|
||||
* @throws Exception
|
||||
*/
|
||||
public function run()
|
||||
{
|
||||
// This task should only be run once
|
||||
$this->runMessage = '# ' . __('Seeding Database') . PHP_EOL . PHP_EOL;
|
||||
|
||||
// Create display groups
|
||||
$this->createDisplayGroups();
|
||||
|
||||
// Create displays
|
||||
$this->createDisplays();
|
||||
|
||||
// Assign displays to display groups
|
||||
$this->assignDisplaysToDisplayGroups();
|
||||
|
||||
// Import layouts
|
||||
$this->importLayouts();
|
||||
|
||||
// Create campaign
|
||||
$this->createAdCampaigns();
|
||||
$this->createListCampaigns();
|
||||
|
||||
// Create stats
|
||||
$this->createStats();
|
||||
|
||||
// Create Schedules
|
||||
$this->createSchedules();
|
||||
|
||||
// Create Sync Groups
|
||||
$this->createSyncGroups();
|
||||
$this->createSynchronizedSchedules();
|
||||
|
||||
// Create User
|
||||
$this->createUsers();
|
||||
|
||||
// Create Folders
|
||||
$this->createFolders();
|
||||
$this->createCommands();
|
||||
|
||||
// Create bandwidth data display 1
|
||||
$this->createBandwidthReportData();
|
||||
|
||||
// Create disconnected display event for yesterday for 10 minutes for display 1
|
||||
$this->createDisconnectedDisplayEvent();
|
||||
|
||||
$this->runMessage .= ' - ' . __('Done.') . PHP_EOL . PHP_EOL;
|
||||
|
||||
$this->log->info('Task completed');
|
||||
$this->appendRunMessage('Task completed');
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
private function createDisplayGroups(): void
|
||||
{
|
||||
$displayGroups = [
|
||||
'POP Display Group',
|
||||
'Display Group 1',
|
||||
'Display Group 2',
|
||||
|
||||
// Display groups for displaygroups.cy.js test
|
||||
'disp5_dispgrp',
|
||||
];
|
||||
|
||||
foreach ($displayGroups as $displayGroupName) {
|
||||
try {
|
||||
// Don't create if the display group exists
|
||||
$groups = $this->displayGroupFactory->query(null, ['displayGroup' => $displayGroupName]);
|
||||
if (count($groups) > 0) {
|
||||
foreach ($groups as $displayGroup) {
|
||||
$this->displayGroups[$displayGroup->displayGroup] = $displayGroup->getId();
|
||||
}
|
||||
} else {
|
||||
$displayGroup = $this->displayGroupFactory->createEmpty();
|
||||
$displayGroup->displayGroup = $displayGroupName;
|
||||
$displayGroup->userId = $this->userFactory->getSystemUser()->getId();
|
||||
$displayGroup->save();
|
||||
$this->store->commitIfNecessary();
|
||||
// Cache
|
||||
$this->displayGroups[$displayGroup->displayGroup] = $displayGroup->getId();
|
||||
}
|
||||
} catch (GeneralException $e) {
|
||||
$this->log->error('Error creating display group: '. $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private function createDisplays(): void
|
||||
{
|
||||
// Create Displays
|
||||
$displays = [
|
||||
'POP Display 1' => ['license' => Random::generateString(12, 'seed'), 'licensed' => false,
|
||||
'clientType' => 'android', 'clientCode' => 400, 'clientVersion' => 4],
|
||||
'POP Display 2' => ['license' => Random::generateString(12, 'seed'), 'licensed' => false,
|
||||
'clientType' => 'android', 'clientCode' => 400, 'clientVersion' => 4],
|
||||
'List Campaign Display 1' => ['license' => Random::generateString(12, 'seed'), 'licensed' => true,
|
||||
'clientType' => 'android', 'clientCode' => 400, 'clientVersion' => 4],
|
||||
'List Campaign Display 2' => ['license' => Random::generateString(12, 'seed'), 'licensed' => true,
|
||||
'clientType' => 'android', 'clientCode' => 400, 'clientVersion' => 4],
|
||||
|
||||
// Displays for displays.cy.js test
|
||||
'dis_disp1' => ['license' => 'dis_disp1', 'licensed' => true, 'clientType' => 'android', 'clientCode' => 400, 'clientVersion' => 4],
|
||||
'dis_disp2' => ['license' => 'dis_disp2', 'licensed' => true, 'clientType' => 'android', 'clientCode' => 400, 'clientVersion' => 4],
|
||||
'dis_disp3' => ['license' => 'dis_disp3', 'licensed' => false, 'clientType' => 'android', 'clientCode' => 400, 'clientVersion' => 4],
|
||||
'dis_disp4' => ['license' => 'dis_disp4', 'licensed' => true, 'clientType' => 'android', 'clientCode' => 400, 'clientVersion' => 4],
|
||||
'dis_disp5' => ['license' => 'dis_disp5', 'licensed' => true, 'clientType' => 'android', 'clientCode' => 400, 'clientVersion' => 4],
|
||||
|
||||
// Displays for displaygroups.cy.js test
|
||||
'dispgrp_disp1' => ['license' => 'dispgrp_disp1', 'licensed' => true, 'clientType' => 'android', 'clientCode' => 400, 'clientVersion' => 4],
|
||||
'dispgrp_disp2' => ['license' => 'dispgrp_disp2', 'licensed' => true, 'clientType' => 'android', 'clientCode' => 400, 'clientVersion' => 4],
|
||||
'dispgrp_disp_dynamic1' => ['license' => 'dispgrp_disp_dynamic1', 'licensed' => true, 'clientType' => 'android', 'clientCode' => 400, 'clientVersion' => 4],
|
||||
'dispgrp_disp_dynamic2' => ['license' => 'dispgrp_disp_dynamic2', 'licensed' => true, 'clientType' => 'android', 'clientCode' => 400, 'clientVersion' => 4],
|
||||
|
||||
|
||||
// 6 displays for xmds
|
||||
'phpunitv7' => ['license' => 'PHPUnit7', 'licensed' => true, 'clientType' => 'android', 'clientCode' => 400, 'clientVersion' => 4],
|
||||
'phpunitwaiting' => ['license' => 'PHPUnitWaiting', 'licensed' => false, 'clientType' => 'android', 'clientCode' => 400, 'clientVersion' => 4],
|
||||
'phpunitv6' => ['license' => 'PHPUnit6', 'licensed' => true, 'clientType' => 'windows', 'clientCode' => 304, 'clientVersion' => 3],
|
||||
'phpunitv5' => ['license' => 'PHPUnit5', 'licensed' => true, 'clientType' => 'windows', 'clientCode' => 304, 'clientVersion' => 3],
|
||||
'phpunitv4' => ['license' => 'PHPUnit4', 'licensed' => true, 'clientType' => 'android', 'clientCode' => 217, 'clientVersion' => 2],
|
||||
'phpunitv3' => ['license' => 'PHPUnit3', 'licensed' => true, 'clientType' => 'android', 'clientCode' => 217, 'clientVersion' => 2],
|
||||
];
|
||||
|
||||
foreach ($displays as $displayName => $displayData) {
|
||||
try {
|
||||
// Don't create if the display exists
|
||||
$disps = $this->displayFactory->query(null, ['display' => $displayName]);
|
||||
if (count($disps) > 0) {
|
||||
foreach ($disps as $display) {
|
||||
// Cache
|
||||
$this->displays[$display->display] = $display->displayId;
|
||||
}
|
||||
} else {
|
||||
$display = $this->displayFactory->createEmpty();
|
||||
$display->display = $displayName;
|
||||
$display->auditingUntil = 0;
|
||||
$display->defaultLayoutId = $this->getConfig()->getSetting('DEFAULT_LAYOUT');
|
||||
$display->license = $displayData['license'];
|
||||
$display->licensed = $displayData['licensed'] ? 1 : 0; // Authorised?
|
||||
$display->clientType = $displayData['clientType'];
|
||||
$display->clientCode = $displayData['clientCode'];
|
||||
$display->clientVersion = $displayData['clientVersion'];
|
||||
|
||||
$display->incSchedule = 0;
|
||||
$display->clientAddress = '';
|
||||
|
||||
if (!$display->isDisplaySlotAvailable()) {
|
||||
$display->licensed = 0;
|
||||
}
|
||||
$display->lastAccessed = Carbon::now()->format('U');
|
||||
$display->loggedIn = 1;
|
||||
|
||||
$display->save(Display::$saveOptionsMinimum);
|
||||
$this->store->commitIfNecessary();
|
||||
// Cache
|
||||
$this->displays[$display->display] = $display->displayId;
|
||||
}
|
||||
} catch (GeneralException $e) {
|
||||
$this->log->error('Error creating display: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
private function assignDisplaysToDisplayGroups(): void
|
||||
{
|
||||
$displayGroup = $this->displayGroupFactory->getById($this->displayGroups['POP Display Group']);
|
||||
$displayGroup->load();
|
||||
|
||||
$display = $this->displayFactory->getById($this->displays['POP Display 1']);
|
||||
|
||||
try {
|
||||
$displayGroup->assignDisplay($display);
|
||||
$displayGroup->save();
|
||||
} catch (GeneralException $e) {
|
||||
$this->log->error('Error assign display to display group: '. $e->getMessage());
|
||||
}
|
||||
|
||||
$this->store->commitIfNecessary();
|
||||
}
|
||||
|
||||
/**
|
||||
* Import Layouts
|
||||
* @throws GeneralException
|
||||
*/
|
||||
private function importLayouts(): void
|
||||
{
|
||||
$this->runMessage .= '## ' . __('Import Layout To Seed Database') . PHP_EOL;
|
||||
|
||||
// Make sure the library exists
|
||||
$this->mediaService->initLibrary();
|
||||
|
||||
// all layouts name and file name
|
||||
$layoutNames = [
|
||||
'dataset test ' => 'export-dataset-test.zip',
|
||||
'layout_with_8_items_dataset' => 'export-layout-with-8-items-dataset.zip',
|
||||
'Image test' => 'export-image-test.zip',
|
||||
'Layout for Schedule 1' => 'export-layout-for-schedule-1.zip',
|
||||
'List Campaign Layout 1' => 'export-list-campaign-layout-1.zip',
|
||||
'List Campaign Layout 2' => 'export-list-campaign-layout-2.zip',
|
||||
'POP Layout 1' => 'export-pop-layout-1.zip',
|
||||
|
||||
// Layout for displaygroups.cy.js test
|
||||
'disp4_default_layout' => 'export-disp4-default-layout.zip',
|
||||
|
||||
// Layout editor tests
|
||||
'Audio-Video-PDF' => 'export-audio-video-pdf.zip'
|
||||
];
|
||||
|
||||
// Get all layouts
|
||||
$importedLayouts = [];
|
||||
foreach ($this->layoutFactory->query() as $layout) {
|
||||
// cache
|
||||
if (array_key_exists($layout->layout, $layoutNames)) {
|
||||
$importedLayouts[] = $layoutNames[$layout->layout];
|
||||
}
|
||||
|
||||
// Cache
|
||||
$this->layouts[trim($layout->layout)] = $layout->layoutId;
|
||||
}
|
||||
|
||||
// Import a layout
|
||||
$folder = PROJECT_ROOT . '/tests/resources/seeds/layouts/';
|
||||
|
||||
foreach (array_diff(scandir($folder), array('..', '.')) as $file) {
|
||||
// Check if the layout file has already been imported
|
||||
if (!in_array($file, $importedLayouts)) {
|
||||
if (stripos($file, '.zip')) {
|
||||
try {
|
||||
$layout = $this->layoutFactory->createFromZip(
|
||||
$folder . '/' . $file,
|
||||
null,
|
||||
$this->userFactory->getSystemUser()->getId(),
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
$this->dataSetFactory,
|
||||
null,
|
||||
$this->mediaService,
|
||||
1
|
||||
);
|
||||
|
||||
$layout->save([
|
||||
'audit' => false,
|
||||
'import' => true
|
||||
]);
|
||||
|
||||
if (!empty($layout->getUnmatchedProperty('thumbnail'))) {
|
||||
rename($layout->getUnmatchedProperty('thumbnail'), $layout->getThumbnailUri());
|
||||
}
|
||||
|
||||
$this->store->commitIfNecessary();
|
||||
// Update Cache
|
||||
$this->layouts[trim($layout->layout)] = $layout->layoutId;
|
||||
} catch (Exception $exception) {
|
||||
$this->log->error('Seed Database: Unable to import layout: ' . $file . '. E = ' . $exception->getMessage());
|
||||
$this->log->debug($exception->getTraceAsString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
* @throws NotFoundException
|
||||
* @throws DuplicateEntityException
|
||||
*/
|
||||
private function createAdCampaigns(): void
|
||||
{
|
||||
$layoutId = $this->layouts['POP Layout 1'];
|
||||
|
||||
// Get All Ad Campaigns
|
||||
$campaigns = $this->campaignFactory->query(null, ['type' => 'ad']);
|
||||
foreach ($campaigns as $campaign) {
|
||||
$this->parentCampaigns[$campaign->campaign] = $campaign->getId();
|
||||
}
|
||||
|
||||
if (!array_key_exists('POP Ad Campaign 1', $this->parentCampaigns)) {
|
||||
$campaign = $this->campaignFactory->create(
|
||||
'ad',
|
||||
'POP Ad Campaign 1',
|
||||
$this->userFactory->getSystemUser()->getId(),
|
||||
1
|
||||
);
|
||||
|
||||
$campaign->targetType = 'plays';
|
||||
$campaign->target = 100;
|
||||
$campaign->listPlayOrder = 'round';
|
||||
|
||||
try {
|
||||
// Assign the layout
|
||||
$campaign->assignLayout($layoutId);
|
||||
$campaign->save(['validate' => false, 'saveTags' => false]);
|
||||
$this->store->commitIfNecessary();
|
||||
// Cache
|
||||
$this->parentCampaigns[$campaign->campaign] = $campaign->getId();
|
||||
} catch (GeneralException $e) {
|
||||
$this->getLogger()->error('Save: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
* @throws NotFoundException
|
||||
* @throws DuplicateEntityException
|
||||
*/
|
||||
private function createListCampaigns(): void
|
||||
{
|
||||
$campaignName = 'Campaign for Schedule 1';
|
||||
|
||||
// Get All List Campaigns
|
||||
$campaigns = $this->campaignFactory->query(null, ['type' => 'list']);
|
||||
foreach ($campaigns as $campaign) {
|
||||
$this->parentCampaigns[$campaign->campaign] = $campaign->getId();
|
||||
}
|
||||
|
||||
if (!array_key_exists($campaignName, $this->parentCampaigns)) {
|
||||
$campaign = $this->campaignFactory->create(
|
||||
'list',
|
||||
$campaignName,
|
||||
$this->userFactory->getSystemUser()->getId(),
|
||||
1
|
||||
);
|
||||
$campaign->listPlayOrder = 'round';
|
||||
|
||||
try {
|
||||
// Assign the layout
|
||||
$campaign->save(['validate' => false, 'saveTags' => false]);
|
||||
$this->store->commitIfNecessary();
|
||||
// Cache
|
||||
$this->parentCampaigns[$campaign->campaign] = $campaign->getId();
|
||||
} catch (GeneralException $e) {
|
||||
$this->getLogger()->error('Save: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
private function createStats(): void
|
||||
{
|
||||
// Delete Stats
|
||||
$this->store->update('DELETE FROM stat WHERE displayId = :displayId', [
|
||||
'displayId' => $this->displays['POP Display 1']
|
||||
]);
|
||||
|
||||
// Get layout campaign Id
|
||||
$campaignId = $this->layoutFactory->getById($this->layouts['POP Layout 1'])->campaignId;
|
||||
$columns = 'type, statDate, scheduleId, displayId, campaignId, parentCampaignId, layoutId, mediaId, widgetId, `start`, `end`, tag, duration, `count`';
|
||||
$values = ':type, :statDate, :scheduleId, :displayId, :campaignId, :parentCampaignId, :layoutId, :mediaId, :widgetId, :start, :end, :tag, :duration, :count';
|
||||
|
||||
// a layout stat for today
|
||||
try {
|
||||
$params = [
|
||||
'type' => 'layout',
|
||||
'statDate' => Carbon::now()->hour(12)->format('U'),
|
||||
'scheduleId' => 0,
|
||||
'displayId' => $this->displays['POP Display 1'],
|
||||
'campaignId' => $campaignId,
|
||||
'parentCampaignId' => $this->parentCampaigns['POP Ad Campaign 1'],
|
||||
'layoutId' => $this->layouts['POP Layout 1'],
|
||||
'mediaId' => null,
|
||||
'widgetId' => 0,
|
||||
'start' => Carbon::now()->hour(12)->format('U'),
|
||||
'end' => Carbon::now()->hour(12)->addSeconds(60)->format('U'),
|
||||
'tag' => null,
|
||||
'duration' => 60,
|
||||
'count' => 1,
|
||||
];
|
||||
$this->store->insert('INSERT INTO `stat` (' . $columns . ') VALUES (' . $values . ')', $params);
|
||||
|
||||
// a layout stat for lastweek
|
||||
$params = [
|
||||
'type' => 'layout',
|
||||
'statDate' => Carbon::now()->subWeek()->hour(12)->format('U'),
|
||||
'scheduleId' => 0,
|
||||
'displayId' => $this->displays['POP Display 1'],
|
||||
'campaignId' => $campaignId,
|
||||
'parentCampaignId' => $this->parentCampaigns['POP Ad Campaign 1'],
|
||||
'layoutId' => $this->layouts['POP Layout 1'],
|
||||
'mediaId' => null,
|
||||
'widgetId' => 0,
|
||||
'start' => Carbon::now()->subWeek()->hour(12)->format('U'),
|
||||
'end' => Carbon::now()->subWeek()->hour(12)->addSeconds(60)->format('U'),
|
||||
'tag' => null,
|
||||
'duration' => 60,
|
||||
'count' => 1,
|
||||
];
|
||||
$this->store->insert('INSERT INTO `stat` (' . $columns . ') VALUES (' . $values . ')', $params);
|
||||
|
||||
// Media stats
|
||||
$columns = 'type, statDate, scheduleId, displayId, campaignId, layoutId, mediaId, widgetId, `start`, `end`, tag, duration, `count`';
|
||||
$values = ':type, :statDate, :scheduleId, :displayId, :campaignId, :layoutId, :mediaId, :widgetId, :start, :end, :tag, :duration, :count';
|
||||
|
||||
// Get Layout
|
||||
$layout = $this->layoutFactory->getById($this->layouts['POP Layout 1']);
|
||||
$layout->load();
|
||||
|
||||
// Take a mediaId and widgetId of the layout
|
||||
foreach ($layout->getAllWidgets() as $widget) {
|
||||
$widgetId = $widget->widgetId;
|
||||
$mediaId = $widget->mediaIds[0];
|
||||
break;
|
||||
}
|
||||
|
||||
// Get Media
|
||||
$media = $this->mediaFactory->getById($mediaId);
|
||||
|
||||
// a media stat for today
|
||||
$params = [
|
||||
'type' => 'media',
|
||||
'statDate' => Carbon::now()->hour(12)->format('U'),
|
||||
'scheduleId' => 0,
|
||||
'displayId' => $this->displays['POP Display 1'],
|
||||
'campaignId' => $campaignId,
|
||||
'layoutId' => $this->layouts['POP Layout 1'],
|
||||
'mediaId' => $media->mediaId,
|
||||
'widgetId' => $widgetId,
|
||||
'start' => Carbon::now()->hour(12)->format('U'),
|
||||
'end' => Carbon::now()->hour(12)->addSeconds(60)->format('U'),
|
||||
'tag' => null,
|
||||
'duration' => 60,
|
||||
'count' => 1,
|
||||
];
|
||||
$this->store->insert('INSERT INTO `stat` (' . $columns . ') VALUES (' . $values . ')', $params);
|
||||
|
||||
// another media stat for today
|
||||
$params = [
|
||||
'type' => 'media',
|
||||
'statDate' => Carbon::now()->hour(12)->addSeconds(60)->format('U'),
|
||||
'scheduleId' => 0,
|
||||
'displayId' => $this->displays['POP Display 1'],
|
||||
'campaignId' => $campaignId,
|
||||
'layoutId' => $this->layouts['POP Layout 1'],
|
||||
'mediaId' => $media->mediaId,
|
||||
'widgetId' => $widgetId,
|
||||
'start' => Carbon::now()->hour(12)->addSeconds(60)->format('U'),
|
||||
'end' => Carbon::now()->hour(12)->addSeconds(120)->format('U'),
|
||||
'tag' => null,
|
||||
'duration' => 60,
|
||||
'count' => 1,
|
||||
];
|
||||
$this->store->insert('INSERT INTO `stat` (' . $columns . ') VALUES (' . $values . ')', $params);
|
||||
|
||||
// a media stat for lastweek
|
||||
// Last week stats -
|
||||
$params = [
|
||||
'type' => 'media',
|
||||
'statDate' => Carbon::now()->subWeek()->addDays(2)->hour(12)->format('U'),
|
||||
'scheduleId' => 0,
|
||||
'displayId' => $this->displays['POP Display 1'],
|
||||
'campaignId' => $campaignId,
|
||||
'layoutId' => $this->layouts['POP Layout 1'],
|
||||
'mediaId' => $media->mediaId,
|
||||
'widgetId' => $widgetId,
|
||||
'start' => Carbon::now()->subWeek()->hour(12)->format('U'),
|
||||
'end' => Carbon::now()->subWeek()->hour(12)->addSeconds(60)->format('U'),
|
||||
'tag' => null,
|
||||
'duration' => 60,
|
||||
'count' => 1,
|
||||
];
|
||||
$this->store->insert('INSERT INTO `stat` (' . $columns . ') VALUES (' . $values . ')', $params);
|
||||
|
||||
// another media stat for lastweek
|
||||
$params = [
|
||||
'type' => 'media',
|
||||
'statDate' => Carbon::now()->subWeek()->addDays(2)->hour(12)->addSeconds(60)->format('U'),
|
||||
'scheduleId' => 0,
|
||||
'displayId' => $this->displays['POP Display 1'],
|
||||
'campaignId' => $campaignId,
|
||||
'layoutId' => $this->layouts['POP Layout 1'],
|
||||
'mediaId' => $media->mediaId,
|
||||
'widgetId' => $widgetId,
|
||||
'start' => Carbon::now()->subWeek()->hour(12)->addSeconds(60)->format('U'),
|
||||
'end' => Carbon::now()->subWeek()->hour(12)->addSeconds(120)->format('U'),
|
||||
'tag' => null,
|
||||
'duration' => 60,
|
||||
'count' => 1,
|
||||
];
|
||||
$this->store->insert('INSERT INTO `stat` (' . $columns . ') VALUES (' . $values . ')', $params);
|
||||
|
||||
// an widget stat for today
|
||||
$params = [
|
||||
'type' => 'widget',
|
||||
'statDate' => Carbon::now()->hour(12)->format('U'),
|
||||
'scheduleId' => 0,
|
||||
'displayId' => $this->displays['POP Display 1'],
|
||||
'campaignId' => $campaignId,
|
||||
'layoutId' => $this->layouts['POP Layout 1'],
|
||||
'mediaId' => $media->mediaId,
|
||||
'widgetId' => $widgetId,
|
||||
'start' => Carbon::now()->hour(12)->format('U'),
|
||||
'end' => Carbon::now()->hour(12)->addSeconds(60)->format('U'),
|
||||
'tag' => null,
|
||||
'duration' => 60,
|
||||
'count' => 1,
|
||||
];
|
||||
$this->store->insert('INSERT INTO `stat` (' . $columns . ') VALUES (' . $values . ')', $params);
|
||||
|
||||
// a event stat for today
|
||||
$params = [
|
||||
'type' => 'event',
|
||||
'statDate' => Carbon::now()->hour(12)->format('U'),
|
||||
'scheduleId' => 0,
|
||||
'displayId' => $this->displays['POP Display 1'],
|
||||
'campaignId' => 0,
|
||||
'layoutId' => 0,
|
||||
'mediaId' => null,
|
||||
'widgetId' => 0,
|
||||
'start' => Carbon::now()->hour(12)->format('U'),
|
||||
'end' => Carbon::now()->hour(12)->addSeconds(60)->format('U'),
|
||||
'tag' => 'Event123',
|
||||
'duration' => 60,
|
||||
'count' => 1,
|
||||
];
|
||||
$this->store->insert('INSERT INTO `stat` (' . $columns . ') VALUES (' . $values . ')', $params);
|
||||
} catch (GeneralException $e) {
|
||||
$this->getLogger()->error('Error inserting stats: '. $e->getMessage());
|
||||
}
|
||||
$this->store->commitIfNecessary();
|
||||
}
|
||||
|
||||
private function createSchedules(): void
|
||||
{
|
||||
// Don't create if the schedule exists
|
||||
$schedules = $this->scheduleFactory->query(null, [
|
||||
'eventTypeId' => Schedule::$LAYOUT_EVENT,
|
||||
'campaignId' => $this->layouts['dataset test']
|
||||
]);
|
||||
|
||||
if (count($schedules) <= 0) {
|
||||
try {
|
||||
$schedule = $this->scheduleFactory->createEmpty();
|
||||
$schedule->userId = $this->userFactory->getSystemUser()->getId();
|
||||
$schedule->eventTypeId = Schedule::$LAYOUT_EVENT;
|
||||
$schedule->dayPartId = 2;
|
||||
$schedule->displayOrder = 0;
|
||||
$schedule->isPriority = 0;
|
||||
// Campaign Id
|
||||
$schedule->campaignId = $this->layouts['dataset test'];
|
||||
$schedule->syncTimezone = 0;
|
||||
$schedule->syncEvent = 0;
|
||||
$schedule->isGeoAware = 0;
|
||||
$schedule->maxPlaysPerHour = 0;
|
||||
|
||||
$displays = $this->displayFactory->query(null, ['display' => 'phpunitv']);
|
||||
foreach ($displays as $display) {
|
||||
$displayGroupId = $display->displayGroupId;
|
||||
$schedule->assignDisplayGroup($this->displayGroupFactory->getById($displayGroupId));
|
||||
}
|
||||
$schedule->save(['notify' => false]);
|
||||
|
||||
$this->store->commitIfNecessary();
|
||||
} catch (GeneralException $e) {
|
||||
$this->log->error('Error creating schedule : '. $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function createSyncGroups(): void
|
||||
{
|
||||
// Don't create if the sync group exists
|
||||
$syncGroups = $this->syncGroupFactory->query(null, [
|
||||
'eventTypeId' => Schedule::$LAYOUT_EVENT,
|
||||
'campaignId' => $this->layouts['dataset test']
|
||||
]);
|
||||
|
||||
if (count($syncGroups) > 0) {
|
||||
foreach ($syncGroups as $syncGroup) {
|
||||
// Cache
|
||||
$this->syncGroups[$syncGroup->name] = $syncGroup->getId();
|
||||
}
|
||||
} else {
|
||||
// Create a SyncGroup - SyncGroup name `Simple Sync Group`
|
||||
try {
|
||||
$syncGroup = $this->syncGroupFactory->createEmpty();
|
||||
$syncGroup->name = 'Simple Sync Group';
|
||||
$syncGroup->ownerId = $this->userFactory->getSystemUser()->getId();
|
||||
$syncGroup->syncPublisherPort = 9590;
|
||||
$syncGroup->folderId = 1;
|
||||
$syncGroup->permissionsFolderId = 1;
|
||||
$syncGroup->save();
|
||||
$this->store->update('UPDATE `display` SET `display`.syncGroupId = :syncGroupId WHERE `display`.displayId = :displayId', [
|
||||
'syncGroupId' => $syncGroup->syncGroupId,
|
||||
'displayId' => $this->displays['phpunitv6']
|
||||
]);
|
||||
|
||||
$this->store->update('UPDATE `display` SET `display`.syncGroupId = :syncGroupId WHERE `display`.displayId = :displayId', [
|
||||
'syncGroupId' => $syncGroup->syncGroupId,
|
||||
'displayId' => $this->displays['phpunitv7']
|
||||
]);
|
||||
|
||||
$syncGroup->leadDisplayId = $this->displays['phpunitv7'];
|
||||
$syncGroup->save();
|
||||
$this->store->commitIfNecessary();
|
||||
// Cache
|
||||
$this->syncGroups[$syncGroup->name] = $syncGroup->getId();
|
||||
} catch (GeneralException $e) {
|
||||
$this->log->error('Error creating sync group: '. $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function createSynchronizedSchedules(): void
|
||||
{
|
||||
// Don't create if the schedule exists
|
||||
$schedules = $this->scheduleFactory->query(null, [
|
||||
'eventTypeId' => Schedule::$SYNC_EVENT,
|
||||
'syncGroupId' => $this->syncGroups['Simple Sync Group']
|
||||
]);
|
||||
|
||||
if (count($schedules) <= 0) {
|
||||
try {
|
||||
$schedule = $this->scheduleFactory->createEmpty();
|
||||
$schedule->userId = $this->userFactory->getSystemUser()->getId();
|
||||
$schedule->eventTypeId = Schedule::$SYNC_EVENT;
|
||||
$schedule->dayPartId = 2;
|
||||
|
||||
$schedule->displayOrder = 0;
|
||||
$schedule->isPriority = 0;
|
||||
|
||||
// Campaign Id
|
||||
$schedule->campaignId = null;
|
||||
$schedule->syncTimezone = 0;
|
||||
$schedule->syncEvent = 1;
|
||||
$schedule->isGeoAware = 0;
|
||||
$schedule->maxPlaysPerHour = 0;
|
||||
$schedule->syncGroupId = $this->syncGroups['Simple Sync Group'];
|
||||
|
||||
$displayV7 = $this->displayFactory->getById($this->displays['phpunitv7']);
|
||||
$schedule->assignDisplayGroup($this->displayGroupFactory->getById($displayV7->displayGroupId));
|
||||
$displayV6 = $this->displayFactory->getById($this->displays['phpunitv6']);
|
||||
$schedule->assignDisplayGroup($this->displayGroupFactory->getById($displayV6->displayGroupId));
|
||||
|
||||
$schedule->save(['notify' => false]);
|
||||
$this->store->commitIfNecessary();
|
||||
// Update Sync Links
|
||||
$this->store->insert('INSERT INTO `schedule_sync` (`eventId`, `displayId`, `layoutId`)
|
||||
VALUES(:eventId, :displayId, :layoutId) ON DUPLICATE KEY UPDATE layoutId = :layoutId', [
|
||||
'eventId' => $schedule->eventId,
|
||||
'displayId' => $this->displays['phpunitv7'],
|
||||
'layoutId' => $this->layouts['Image test']
|
||||
]);
|
||||
|
||||
$this->store->insert('INSERT INTO `schedule_sync` (`eventId`, `displayId`, `layoutId`)
|
||||
VALUES(:eventId, :displayId, :layoutId) ON DUPLICATE KEY UPDATE layoutId = :layoutId', [
|
||||
'eventId' => $schedule->eventId,
|
||||
'displayId' => $this->displays['phpunitv6'],
|
||||
'layoutId' => $this->layouts['Image test']
|
||||
]);
|
||||
$this->store->commitIfNecessary();
|
||||
} catch (GeneralException $e) {
|
||||
$this->log->error('Error creating sync schedule: '. $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function createUsers(): void
|
||||
{
|
||||
// Don't create if exists
|
||||
$users = $this->userFactory->query(null, [
|
||||
'exactUserName' => 'folder_user'
|
||||
]);
|
||||
|
||||
if (count($users) <= 0) {
|
||||
// Create a user - user name `Simple User`
|
||||
try {
|
||||
$user = $this->userFactory->create();
|
||||
$user->setChildAclDependencies($this->userGroupFactory);
|
||||
$user->userName = 'folder_user';
|
||||
$user->email = '';
|
||||
$user->homePageId = 'icondashboard.view';
|
||||
$user->libraryQuota = 20;
|
||||
$user->setNewPassword('password');
|
||||
$user->homeFolderId = 1;
|
||||
$user->userTypeId = 3;
|
||||
$user->isSystemNotification = 0;
|
||||
$user->isDisplayNotification = 0;
|
||||
$user->isPasswordChangeRequired = 0;
|
||||
$user->firstName = 'test';
|
||||
$user->lastName = 'user';
|
||||
$user->save();
|
||||
$this->store->commitIfNecessary();
|
||||
} catch (GeneralException $e) {
|
||||
$this->log->error('Error creating user: '. $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function createFolders(): void
|
||||
{
|
||||
$folders = [
|
||||
'ChildFolder', 'FolderHome', 'EmptyFolder', 'ShareFolder', 'FolderWithContent', 'FolderWithImage', 'MoveToFolder', 'MoveFromFolder'
|
||||
];
|
||||
|
||||
foreach ($folders as $folderName) {
|
||||
try {
|
||||
// Don't create if the folder exists
|
||||
$folds = $this->folderFactory->query(null, ['folderName' => $folderName]);
|
||||
if (count($folds) <= 0) {
|
||||
$folder = $this->folderFactory->createEmpty();
|
||||
$folder->text = $folderName;
|
||||
$folder->parentId = 1;
|
||||
$folder->children = '';
|
||||
|
||||
$folder->save();
|
||||
$this->store->commitIfNecessary();
|
||||
}
|
||||
} catch (GeneralException $e) {
|
||||
$this->log->error('Error creating folder: '. $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Place the media in folders
|
||||
$folderWithImages = [
|
||||
'MoveToFolder' => 'test12',
|
||||
'MoveFromFolder' => 'test34',
|
||||
'FolderWithContent' => 'media_for_not_empty_folder',
|
||||
'FolderWithImage' => 'media_for_search_in_folder'
|
||||
];
|
||||
|
||||
foreach ($folderWithImages as $folderName => $mediaName) {
|
||||
try {
|
||||
$folders = $this->folderFactory->query(null, ['folderName' => $folderName]);
|
||||
if (count($folders) == 1) {
|
||||
$test12 = $this->mediaFactory->getByName($mediaName);
|
||||
$test12->folderId = $folders[0]->getId(); // Get the folder id of FolderHome
|
||||
$test12->save();
|
||||
$this->store->commitIfNecessary();
|
||||
}
|
||||
} catch (GeneralException $e) {
|
||||
$this->log->error('Error moving media ' . $mediaName . ' to the folder: ' . $folderName . ' ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function createBandwidthReportData(): void
|
||||
{
|
||||
// Check if the record exists
|
||||
$monthU = Carbon::now()->startOfDay()->hour(12)->format('U');
|
||||
$record = $this->store->select('SELECT * FROM bandwidth WHERE type = 8 AND displayId = :displayId AND month = :month', [
|
||||
'displayId' => $this->displays['POP Display 1'],
|
||||
'month' => $monthU
|
||||
]);
|
||||
|
||||
if (count($record) <= 0) {
|
||||
$this->store->insert('INSERT INTO `bandwidth` (Month, Type, DisplayID, Size) VALUES (:month, :type, :displayId, :size)', [
|
||||
'month' => $monthU,
|
||||
'type' => 8,
|
||||
'displayId' => $this->displays['POP Display 1'],
|
||||
'size' => 200
|
||||
]);
|
||||
$this->store->commitIfNecessary();
|
||||
}
|
||||
}
|
||||
|
||||
private function createDisconnectedDisplayEvent(): void
|
||||
{
|
||||
// Delete if the record exists
|
||||
$date = Carbon::now()->subDay()->format('U');
|
||||
$this->store->update('DELETE FROM displayevent WHERE displayId = :displayId', [
|
||||
'displayId' => $this->displays['POP Display 1']
|
||||
]);
|
||||
|
||||
$this->store->insert('INSERT INTO `displayevent` (eventDate, start, end, displayID) VALUES (:eventDate, :start, :end, :displayId)', [
|
||||
'eventDate' => $date,
|
||||
'start' => $date,
|
||||
'end' => Carbon::now()->subDay()->addSeconds(600)->format('U'),
|
||||
'displayId' => $this->displays['POP Display 1']
|
||||
]);
|
||||
$this->store->commitIfNecessary();
|
||||
}
|
||||
|
||||
private function createCommands()
|
||||
{
|
||||
$commandName = 'Set Timezone';
|
||||
|
||||
// Don't create if exists
|
||||
$commands = $this->commandFactory->query(null, [
|
||||
'command' => $commandName
|
||||
]);
|
||||
|
||||
if (count($commands) <= 0) {
|
||||
// Create a user - user name `Simple User`
|
||||
try {
|
||||
$command = $this->commandFactory->create();
|
||||
$command->command = $commandName;
|
||||
$command->description = 'a command to test schedule';
|
||||
$command->code = 'TIMEZONE';
|
||||
$command->userId = $this->userFactory->getSystemUser()->getId();
|
||||
$command->createAlertOn = 'never';
|
||||
$command->save();
|
||||
$this->store->commitIfNecessary();
|
||||
} catch (GeneralException $e) {
|
||||
$this->log->error('Error creating command: '. $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
337
lib/XTR/StatsArchiveTask.php
Normal file
337
lib/XTR/StatsArchiveTask.php
Normal file
@@ -0,0 +1,337 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (c) 2022 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - http://www.xibo.org.uk
|
||||
*
|
||||
* 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 Xibo\Entity\User;
|
||||
use Xibo\Factory\MediaFactory;
|
||||
use Xibo\Factory\UserFactory;
|
||||
use Xibo\Helper\DateFormatHelper;
|
||||
use Xibo\Helper\Random;
|
||||
use Xibo\Support\Exception\InvalidArgumentException;
|
||||
use Xibo\Support\Exception\NotFoundException;
|
||||
use Xibo\Support\Exception\TaskRunException;
|
||||
|
||||
/**
|
||||
* Class StatsArchiveTask
|
||||
* @package Xibo\XTR
|
||||
*/
|
||||
class StatsArchiveTask implements TaskInterface
|
||||
{
|
||||
use TaskTrait;
|
||||
|
||||
/** @var User */
|
||||
private $archiveOwner;
|
||||
|
||||
/** @var MediaFactory */
|
||||
private $mediaFactory;
|
||||
|
||||
/** @var UserFactory */
|
||||
private $userFactory;
|
||||
|
||||
/** @var Carbon */
|
||||
private $lastArchiveDate = null;
|
||||
|
||||
/** @var \Xibo\Helper\SanitizerService */
|
||||
private $sanitizerService;
|
||||
|
||||
/** @inheritdoc */
|
||||
public function setFactories($container)
|
||||
{
|
||||
$this->userFactory = $container->get('userFactory');
|
||||
$this->mediaFactory = $container->get('mediaFactory');
|
||||
$this->sanitizerService = $container->get('sanitizerService');
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function run()
|
||||
{
|
||||
$this->archiveStats();
|
||||
$this->tidyStats();
|
||||
}
|
||||
|
||||
public function archiveStats()
|
||||
{
|
||||
$this->log->debug('Archive Stats');
|
||||
|
||||
$this->runMessage = '# ' . __('Stats Archive') . PHP_EOL . PHP_EOL;
|
||||
|
||||
if ($this->getOption('archiveStats', 'Off') == 'On') {
|
||||
$this->log->debug('Archive Enabled');
|
||||
|
||||
// Archive tasks by week.
|
||||
$periodSizeInDays = $this->getOption('periodSizeInDays', 7);
|
||||
$maxPeriods = $this->getOption('maxPeriods', 4);
|
||||
$periodsToKeep = $this->getOption('periodsToKeep', 1);
|
||||
$this->setArchiveOwner();
|
||||
|
||||
// Get the earliest
|
||||
$earliestDate = $this->timeSeriesStore->getEarliestDate();
|
||||
|
||||
if ($earliestDate === null) {
|
||||
$this->log->debug('Earliest date is null, nothing to archive.');
|
||||
|
||||
$this->runMessage = __('Nothing to archive');
|
||||
return;
|
||||
}
|
||||
|
||||
// Wind back to the start of the day
|
||||
$earliestDate = $earliestDate->copy()->setTime(0, 0, 0);
|
||||
|
||||
// Take the earliest date and roll forward until the current time
|
||||
$now = Carbon::now()->subDays($periodSizeInDays * $periodsToKeep)->setTime(0, 0, 0);
|
||||
$i = 0;
|
||||
|
||||
while ($earliestDate < $now && $i < $maxPeriods) {
|
||||
$i++;
|
||||
|
||||
// Push forward
|
||||
$fromDt = $earliestDate->copy();
|
||||
$earliestDate->addDays($periodSizeInDays);
|
||||
|
||||
$this->log->debug('Running archive number ' . $i
|
||||
. 'for ' . $fromDt->toAtomString() . ' - ' . $earliestDate->toAtomString());
|
||||
|
||||
try {
|
||||
$this->exportStatsToLibrary($fromDt, $earliestDate);
|
||||
} catch (\Exception $exception) {
|
||||
$this->log->error('Export error for Archive Number ' . $i . ', e = ' . $exception->getMessage());
|
||||
|
||||
// Throw out to the task handler to record the error.
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
$this->store->commitIfNecessary();
|
||||
|
||||
$this->log->debug('Export success for Archive Number ' . $i);
|
||||
|
||||
// Grab the last from date for use in tidy stats
|
||||
$this->lastArchiveDate = $fromDt;
|
||||
}
|
||||
|
||||
$this->runMessage .= ' - ' . __('Done') . PHP_EOL . PHP_EOL;
|
||||
} else {
|
||||
$this->log->debug('Archive not enabled');
|
||||
$this->runMessage .= ' - ' . __('Disabled') . PHP_EOL . PHP_EOL;
|
||||
}
|
||||
|
||||
$this->log->debug('Finished archive stats, last archive date is '
|
||||
. ($this->lastArchiveDate == null ? 'null' : $this->lastArchiveDate->toAtomString()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Export stats to the library
|
||||
* @param Carbon $fromDt
|
||||
* @param Carbon $toDt
|
||||
* @throws \Xibo\Support\Exception\GeneralException
|
||||
*/
|
||||
private function exportStatsToLibrary($fromDt, $toDt)
|
||||
{
|
||||
$this->log->debug('Export period: ' . $fromDt->toAtomString() . ' - ' . $toDt->toAtomString());
|
||||
$this->runMessage .= ' - ' . $fromDt->format(DateFormatHelper::getSystemFormat()) . ' / ' . $toDt->format(DateFormatHelper::getSystemFormat()) . PHP_EOL;
|
||||
|
||||
$resultSet = $this->timeSeriesStore->getStats([
|
||||
'fromDt'=> $fromDt,
|
||||
'toDt'=> $toDt,
|
||||
]);
|
||||
|
||||
$this->log->debug('Get stats');
|
||||
|
||||
// Create a temporary file for this
|
||||
$fileName = tempnam(sys_get_temp_dir(), 'stats');
|
||||
|
||||
$out = fopen($fileName, 'w');
|
||||
fputcsv($out, ['Stat Date', 'Type', 'FromDT', 'ToDT', 'Layout', 'Display', 'Media', 'Tag', 'Duration', 'Count', 'DisplayId', 'LayoutId', 'WidgetId', 'MediaId', 'Engagements']);
|
||||
|
||||
$hasStatsToArchive = false;
|
||||
while ($row = $resultSet->getNextRow()) {
|
||||
$hasStatsToArchive = true;
|
||||
$sanitizedRow = $this->getSanitizer($row);
|
||||
|
||||
if ($this->timeSeriesStore->getEngine() == 'mongodb') {
|
||||
$statDate = isset($row['statDate']) ? Carbon::createFromTimestamp($row['statDate']->toDateTime()->format('U'))->format(DateFormatHelper::getSystemFormat()) : null;
|
||||
$start = Carbon::createFromTimestamp($row['start']->toDateTime()->format('U'))->format(DateFormatHelper::getSystemFormat());
|
||||
$end = Carbon::createFromTimestamp($row['end']->toDateTime()->format('U'))->format(DateFormatHelper::getSystemFormat());
|
||||
$engagements = isset($row['engagements']) ? json_encode($row['engagements']) : '[]';
|
||||
} else {
|
||||
$statDate = isset($row['statDate']) ? Carbon::createFromTimestamp($row['statDate'])->format(DateFormatHelper::getSystemFormat()) : null;
|
||||
$start = Carbon::createFromTimestamp($row['start'])->format(DateFormatHelper::getSystemFormat());
|
||||
$end = Carbon::createFromTimestamp($row['end'])->format(DateFormatHelper::getSystemFormat());
|
||||
$engagements = isset($row['engagements']) ? $row['engagements'] : '[]';
|
||||
}
|
||||
|
||||
// Read the columns
|
||||
fputcsv($out, [
|
||||
$statDate,
|
||||
$sanitizedRow->getString('type'),
|
||||
$start,
|
||||
$end,
|
||||
isset($row['layout']) ? $sanitizedRow->getString('layout') :'',
|
||||
isset($row['display']) ? $sanitizedRow->getString('display') :'',
|
||||
isset($row['media']) ? $sanitizedRow->getString('media') :'',
|
||||
isset($row['tag']) ? $sanitizedRow->getString('tag') :'',
|
||||
$sanitizedRow->getInt('duration'),
|
||||
$sanitizedRow->getInt('count'),
|
||||
$sanitizedRow->getInt('displayId'),
|
||||
isset($row['layoutId']) ? $sanitizedRow->getInt('layoutId') :'',
|
||||
isset($row['widgetId']) ? $sanitizedRow->getInt('widgetId') :'',
|
||||
isset($row['mediaId']) ? $sanitizedRow->getInt('mediaId') :'',
|
||||
$engagements
|
||||
]);
|
||||
}
|
||||
|
||||
fclose($out);
|
||||
|
||||
if ($hasStatsToArchive) {
|
||||
$this->log->debug('Temporary file written, zipping');
|
||||
|
||||
// Create a ZIP file and add our temporary file
|
||||
$zipName = $this->config->getSetting('LIBRARY_LOCATION') . 'temp/stats.csv.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, 'stats.csv');
|
||||
$zip->close();
|
||||
|
||||
$this->log->debug('Zipped to ' . $zipName);
|
||||
|
||||
// This all might have taken a long time indeed, so lets see if we need to reconnect MySQL
|
||||
$this->store->select('SELECT 1', [], 'default', true);
|
||||
|
||||
$this->log->debug('MySQL connection refreshed if necessary');
|
||||
|
||||
// Upload to the library
|
||||
$media = $this->mediaFactory->create(
|
||||
__('Stats Export %s to %s - %s', $fromDt->format('Y-m-d'), $toDt->format('Y-m-d'), Random::generateString(5)),
|
||||
'stats.csv.zip',
|
||||
'genericfile',
|
||||
$this->archiveOwner->getId()
|
||||
);
|
||||
$media->save();
|
||||
|
||||
$this->log->debug('Media saved as ' . $media->name);
|
||||
|
||||
// Commit before the delete (the delete might take a long time)
|
||||
$this->store->commitIfNecessary();
|
||||
|
||||
// Set max attempts to -1 so that we continue deleting until we've removed all of the stats that we've exported
|
||||
$options = [
|
||||
'maxAttempts' => -1,
|
||||
'statsDeleteSleep' => 1,
|
||||
'limit' => 1000
|
||||
];
|
||||
|
||||
$this->log->debug('Delete stats for period: ' . $fromDt->toAtomString() . ' - ' . $toDt->toAtomString());
|
||||
|
||||
// Delete the stats, incrementally
|
||||
$this->timeSeriesStore->deleteStats($toDt, $fromDt, $options);
|
||||
|
||||
// This all might have taken a long time indeed, so lets see if we need to reconnect MySQL
|
||||
$this->store->select('SELECT 1', [], 'default', true);
|
||||
|
||||
$this->log->debug('MySQL connection refreshed if necessary');
|
||||
|
||||
$this->log->debug('Delete stats completed, export period completed.');
|
||||
} else {
|
||||
$this->log->debug('There are no stats to archive');
|
||||
}
|
||||
|
||||
// Remove the CSV file
|
||||
unlink($fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the archive owner
|
||||
* @throws TaskRunException
|
||||
*/
|
||||
private function setArchiveOwner()
|
||||
{
|
||||
$archiveOwner = $this->getOption('archiveOwner', null);
|
||||
|
||||
if ($archiveOwner == null) {
|
||||
$admins = $this->userFactory->getSuperAdmins();
|
||||
|
||||
if (count($admins) <= 0) {
|
||||
throw new TaskRunException(__('No super admins to use as the archive owner, please set one in the configuration.'));
|
||||
}
|
||||
|
||||
$this->archiveOwner = $admins[0];
|
||||
} else {
|
||||
try {
|
||||
$this->archiveOwner = $this->userFactory->getByName($archiveOwner);
|
||||
} catch (NotFoundException $e) {
|
||||
throw new TaskRunException(__('Archive Owner not found'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tidy Stats
|
||||
*/
|
||||
private function tidyStats()
|
||||
{
|
||||
$this->log->debug('Tidy stats');
|
||||
|
||||
$this->runMessage .= '## ' . __('Tidy Stats') . PHP_EOL;
|
||||
|
||||
$maxAge = intval($this->config->getSetting('MAINTENANCE_STAT_MAXAGE'));
|
||||
|
||||
if ($maxAge != 0) {
|
||||
$this->log->debug('Max Age is ' . $maxAge);
|
||||
|
||||
// Set the max age to maxAgeDays from now, or if we've archived, from the archive date
|
||||
$maxAgeDate = ($this->lastArchiveDate === null)
|
||||
? Carbon::now()->subDays($maxAge)
|
||||
: $this->lastArchiveDate;
|
||||
|
||||
// Control the flow of the deletion
|
||||
$options = [
|
||||
'maxAttempts' => $this->getOption('statsDeleteMaxAttempts', 10),
|
||||
'statsDeleteSleep' => $this->getOption('statsDeleteSleep', 3),
|
||||
'limit' => $this->getOption('limit', 10000) // Note: for mongo we dont use $options['limit'] anymore
|
||||
];
|
||||
|
||||
try {
|
||||
$this->log->debug('Calling delete stats with max age: ' . $maxAgeDate->toAtomString());
|
||||
|
||||
$countDeleted = $this->timeSeriesStore->deleteStats($maxAgeDate, null, $options);
|
||||
|
||||
$this->log->debug('Delete Stats complete');
|
||||
|
||||
$this->runMessage .= ' - ' . sprintf(__('Done - %d deleted.'), $countDeleted) . PHP_EOL . PHP_EOL;
|
||||
} catch (\Exception $exception) {
|
||||
$this->log->error('Unexpected error running stats tidy. e = ' . $exception->getMessage());
|
||||
$this->runMessage .= ' - ' . __('Error.') . PHP_EOL . PHP_EOL;
|
||||
}
|
||||
} else {
|
||||
$this->runMessage .= ' - ' . __('Disabled') . PHP_EOL . PHP_EOL;
|
||||
}
|
||||
|
||||
$this->log->debug('Tidy stats complete');
|
||||
}
|
||||
}
|
||||
679
lib/XTR/StatsMigrationTask.php
Normal file
679
lib/XTR/StatsMigrationTask.php
Normal file
@@ -0,0 +1,679 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2023 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 Xibo\Entity\Task;
|
||||
use Xibo\Factory\DisplayFactory;
|
||||
use Xibo\Factory\LayoutFactory;
|
||||
use Xibo\Factory\TaskFactory;
|
||||
use Xibo\Support\Exception\NotFoundException;
|
||||
|
||||
/**
|
||||
* Class StatsMigrationTask
|
||||
* @package Xibo\XTR
|
||||
*/
|
||||
class StatsMigrationTask implements TaskInterface
|
||||
{
|
||||
use TaskTrait;
|
||||
|
||||
/** @var TaskFactory */
|
||||
private $taskFactory;
|
||||
|
||||
/** @var DisplayFactory */
|
||||
private $displayFactory;
|
||||
|
||||
/** @var LayoutFactory */
|
||||
private $layoutFactory;
|
||||
|
||||
/** @var bool Does the Archive Table exist? */
|
||||
private $archiveExist;
|
||||
|
||||
/** @var array Cache of displayIds found */
|
||||
private $displays = [];
|
||||
|
||||
/** @var array Cache of displayIds not found */
|
||||
private $displaysNotFound = [];
|
||||
|
||||
/** @var Task The Stats Archive Task */
|
||||
private $archiveTask;
|
||||
|
||||
/** @inheritdoc */
|
||||
public function setFactories($container)
|
||||
{
|
||||
$this->taskFactory = $container->get('taskFactory');
|
||||
$this->layoutFactory = $container->get('layoutFactory');
|
||||
$this->displayFactory = $container->get('displayFactory');
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function run()
|
||||
{
|
||||
$this->migrateStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate Stats
|
||||
* @throws \Xibo\Support\Exception\NotFoundException
|
||||
*/
|
||||
public function migrateStats()
|
||||
{
|
||||
// Config options
|
||||
$options = [
|
||||
'killSwitch' => (int)$this->getOption('killSwitch', 0),
|
||||
'numberOfRecords' => (int)$this->getOption('numberOfRecords', 10000),
|
||||
'numberOfLoops' => (int)$this->getOption('numberOfLoops', 1000),
|
||||
'pauseBetweenLoops' => (int)$this->getOption('pauseBetweenLoops', 10),
|
||||
'optimiseOnComplete' => (int)$this->getOption('optimiseOnComplete', 1),
|
||||
];
|
||||
|
||||
// read configOverride
|
||||
$configOverrideFile = $this->getOption('configOverride', '');
|
||||
if (!empty($configOverrideFile) && file_exists($configOverrideFile)) {
|
||||
$options = array_merge($options, json_decode(file_get_contents($configOverrideFile), true));
|
||||
}
|
||||
|
||||
if ($options['killSwitch'] == 0) {
|
||||
// Stat Archive Task
|
||||
$this->archiveTask = $this->taskFactory->getByClass('\Xibo\XTR\\StatsArchiveTask');
|
||||
|
||||
// Check stat_archive table exists
|
||||
$this->archiveExist = $this->store->exists('SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :name', [
|
||||
'name' => 'stat_archive'
|
||||
]);
|
||||
|
||||
// Get timestore engine
|
||||
$timeSeriesStore = $this->timeSeriesStore->getEngine();
|
||||
|
||||
if ($timeSeriesStore == 'mongodb') {
|
||||
// If no records in both the stat and stat_archive then disable the task
|
||||
$statSql = $this->store->getConnection()->prepare('SELECT statId FROM stat LIMIT 1');
|
||||
$statSql->execute();
|
||||
|
||||
$statArchiveSqlCount = 0;
|
||||
if ($this->archiveExist) {
|
||||
/** @noinspection SqlResolve */
|
||||
$statArchiveSql = $this->store->getConnection()->prepare('SELECT statId FROM stat_archive LIMIT 1');
|
||||
$statArchiveSql->execute();
|
||||
$statArchiveSqlCount = $statArchiveSql->rowCount();
|
||||
}
|
||||
|
||||
if (($statSql->rowCount() == 0) && ($statArchiveSqlCount == 0)) {
|
||||
$this->runMessage = '## Stat migration to Mongo' . PHP_EOL ;
|
||||
$this->appendRunMessage('- Both stat_archive and stat is empty. '. PHP_EOL);
|
||||
|
||||
// Disable the task and Enable the StatsArchiver task
|
||||
$this->log->debug('Stats migration task is disabled as stat_archive and stat is empty');
|
||||
$this->disableTask();
|
||||
|
||||
if ($this->archiveTask->isActive == 0) {
|
||||
$this->archiveTask->isActive = 1;
|
||||
$this->archiveTask->save();
|
||||
$this->store->commitIfNecessary();
|
||||
|
||||
$this->appendRunMessage('Enabling Stats Archive Task.');
|
||||
$this->log->debug('Enabling Stats Archive Task.');
|
||||
}
|
||||
} else {
|
||||
// if any of the two tables contain any records
|
||||
$this->quitMigrationTaskOrDisableStatArchiveTask();
|
||||
}
|
||||
|
||||
$this->moveStatsToMongoDb($options);
|
||||
}
|
||||
|
||||
// If when the task runs it finds that MongoDB is disabled,
|
||||
// and there isn't a stat_archive table, then it should disable itself and not run again
|
||||
// (work is considered to be done at that point).
|
||||
else {
|
||||
if ($this->archiveExist) {
|
||||
$this->runMessage = '## Moving from stat_archive to stat (MySQL)' . PHP_EOL;
|
||||
|
||||
$this->quitMigrationTaskOrDisableStatArchiveTask();
|
||||
|
||||
// Start migration
|
||||
$this->moveStatsFromStatArchiveToStatMysql($options);
|
||||
} else {
|
||||
// Disable the task
|
||||
|
||||
$this->runMessage = '## Moving from stat_archive to stat (MySQL)' . PHP_EOL ;
|
||||
$this->appendRunMessage('- Table stat_archive does not exist.' . PHP_EOL);
|
||||
|
||||
$this->log->debug('Table stat_archive does not exist.');
|
||||
$this->disableTask();
|
||||
|
||||
// Enable the StatsArchiver task
|
||||
if ($this->archiveTask->isActive == 0) {
|
||||
$this->archiveTask->isActive = 1;
|
||||
$this->archiveTask->save();
|
||||
$this->store->commitIfNecessary();
|
||||
|
||||
$this->appendRunMessage('Enabling Stats Archive Task.');
|
||||
$this->log->debug('Enabling Stats Archive Task.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function moveStatsFromStatArchiveToStatMysql($options)
|
||||
{
|
||||
|
||||
$fileName = $this->config->getSetting('LIBRARY_LOCATION') . '.watermark_stat_archive_mysql.txt';
|
||||
|
||||
// Get low watermark from file
|
||||
$watermark = $this->getWatermarkFromFile($fileName, 'stat_archive');
|
||||
|
||||
$numberOfLoops = 0;
|
||||
|
||||
while ($watermark > 0) {
|
||||
$count = 0;
|
||||
/** @noinspection SqlResolve */
|
||||
$stats = $this->store->getConnection()->prepare('
|
||||
SELECT statId, type, statDate, scheduleId, displayId, layoutId, mediaId, widgetId, start, `end`, tag
|
||||
FROM stat_archive
|
||||
WHERE statId < :watermark
|
||||
ORDER BY statId DESC LIMIT :limit
|
||||
');
|
||||
$stats->bindParam(':watermark', $watermark, \PDO::PARAM_INT);
|
||||
$stats->bindParam(':limit', $options['numberOfRecords'], \PDO::PARAM_INT);
|
||||
|
||||
// Run the select
|
||||
$stats->execute();
|
||||
|
||||
// Keep count how many stats we've inserted
|
||||
$recordCount = $stats->rowCount();
|
||||
$count+= $recordCount;
|
||||
|
||||
// End of records
|
||||
if ($this->checkEndOfRecords($recordCount, $fileName) === true) {
|
||||
$this->appendRunMessage(PHP_EOL. '# End of records.' . PHP_EOL. '- Dropping stat_archive.');
|
||||
$this->log->debug('End of records in stat_archive (migration to MYSQL). Dropping table.');
|
||||
|
||||
// Drop the stat_archive table
|
||||
/** @noinspection SqlResolve */
|
||||
$this->store->update('DROP TABLE `stat_archive`;', []);
|
||||
|
||||
$this->appendRunMessage(__('Done.'. PHP_EOL));
|
||||
|
||||
// Disable the task
|
||||
$this->disableTask();
|
||||
|
||||
// Enable the StatsArchiver task
|
||||
if ($this->archiveTask->isActive == 0) {
|
||||
$this->archiveTask->isActive = 1;
|
||||
$this->archiveTask->save();
|
||||
$this->store->commitIfNecessary();
|
||||
|
||||
$this->appendRunMessage('Enabling Stats Archive Task.');
|
||||
$this->log->debug('Enabling Stats Archive Task.');
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Loops limit end - task will need to rerun again to start from the saved watermark
|
||||
if ($this->checkLoopLimits($numberOfLoops, $options['numberOfLoops'], $fileName, $watermark) === true) {
|
||||
break;
|
||||
}
|
||||
$numberOfLoops++;
|
||||
|
||||
$temp = [];
|
||||
|
||||
$statIgnoredCount = 0;
|
||||
foreach ($stats->fetchAll() as $stat) {
|
||||
$watermark = $stat['statId'];
|
||||
|
||||
$columns = 'type, statDate, scheduleId, displayId, campaignId, layoutId, mediaId, widgetId, `start`, `end`, tag, duration, `count`';
|
||||
$values = ':type, :statDate, :scheduleId, :displayId, :campaignId, :layoutId, :mediaId, :widgetId, :start, :end, :tag, :duration, :count';
|
||||
|
||||
// Get campaignId
|
||||
if (($stat['type'] != 'event') && ($stat['layoutId'] != null)) {
|
||||
try {
|
||||
// Search the campaignId in the temp array first to reduce query in layouthistory
|
||||
if (array_key_exists($stat['layoutId'], $temp)) {
|
||||
$campaignId = $temp[$stat['layoutId']];
|
||||
} else {
|
||||
$campaignId = $this->layoutFactory->getCampaignIdFromLayoutHistory($stat['layoutId']);
|
||||
$temp[$stat['layoutId']] = $campaignId;
|
||||
}
|
||||
} catch (NotFoundException $error) {
|
||||
$statIgnoredCount+= 1;
|
||||
$count = $count - 1;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
$campaignId = 0;
|
||||
}
|
||||
|
||||
$params = [
|
||||
'type' => $stat['type'],
|
||||
'statDate' => Carbon::createFromTimestamp($stat['statDate'])->format('U'),
|
||||
'scheduleId' => (int) $stat['scheduleId'],
|
||||
'displayId' => (int) $stat['displayId'],
|
||||
'campaignId' => $campaignId,
|
||||
'layoutId' => (int) $stat['layoutId'],
|
||||
'mediaId' => (int) $stat['mediaId'],
|
||||
'widgetId' => (int) $stat['widgetId'],
|
||||
'start' => Carbon::createFromTimestamp($stat['start'])->format('U'),
|
||||
'end' => Carbon::createFromTimestamp($stat['end'])->format('U'),
|
||||
'tag' => $stat['tag'],
|
||||
'duration' => isset($stat['duration']) ? (int) $stat['duration'] : Carbon::createFromTimestamp($stat['end'])->format('U') - Carbon::createFromTimestamp($stat['start'])->format('U'),
|
||||
'count' => isset($stat['count']) ? (int) $stat['count'] : 1,
|
||||
];
|
||||
|
||||
// Do the insert
|
||||
$this->store->insert('INSERT INTO `stat` (' . $columns . ') VALUES (' . $values . ')', $params);
|
||||
$this->store->commitIfNecessary();
|
||||
}
|
||||
|
||||
if ($statIgnoredCount > 0) {
|
||||
$this->appendRunMessage($statIgnoredCount. ' stat(s) were ignored while migrating');
|
||||
}
|
||||
|
||||
// Give SQL time to recover
|
||||
if ($watermark > 0) {
|
||||
$this->appendRunMessage('- '. $count. ' rows migrated.');
|
||||
|
||||
$this->log->debug('MYSQL stats migration from stat_archive to stat. '.$count.' rows effected, sleeping.');
|
||||
sleep($options['pauseBetweenLoops']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function moveStatsToMongoDb($options)
|
||||
{
|
||||
|
||||
// Migration from stat table to Mongo
|
||||
$this->migrationStatToMongo($options);
|
||||
|
||||
// Migration from stat_archive table to Mongo
|
||||
// After migration delete only stat_archive
|
||||
if ($this->archiveExist) {
|
||||
$this->migrationStatArchiveToMongo($options);
|
||||
}
|
||||
}
|
||||
|
||||
function migrationStatToMongo($options)
|
||||
{
|
||||
|
||||
$this->appendRunMessage('## Moving from stat to Mongo');
|
||||
|
||||
$fileName = $this->config->getSetting('LIBRARY_LOCATION') . '.watermark_stat_mongo.txt';
|
||||
|
||||
// Get low watermark from file
|
||||
$watermark = $this->getWatermarkFromFile($fileName, 'stat');
|
||||
|
||||
$numberOfLoops = 0;
|
||||
|
||||
while ($watermark > 0) {
|
||||
$count = 0;
|
||||
$stats = $this->store->getConnection()
|
||||
->prepare('SELECT * FROM stat WHERE statId < :watermark ORDER BY statId DESC LIMIT :limit');
|
||||
$stats->bindParam(':watermark', $watermark, \PDO::PARAM_INT);
|
||||
$stats->bindParam(':limit', $options['numberOfRecords'], \PDO::PARAM_INT);
|
||||
|
||||
// Run the select
|
||||
$stats->execute();
|
||||
|
||||
// Keep count how many stats we've inserted
|
||||
$recordCount = $stats->rowCount();
|
||||
$count+= $recordCount;
|
||||
|
||||
// End of records
|
||||
if ($this->checkEndOfRecords($recordCount, $fileName) === true) {
|
||||
$this->appendRunMessage(PHP_EOL. '# End of records.' . PHP_EOL. '- Truncating and Optimising stat.');
|
||||
$this->log->debug('End of records in stat table. Truncate and Optimise.');
|
||||
|
||||
// Truncate stat table
|
||||
$this->store->update('TRUNCATE TABLE stat', []);
|
||||
|
||||
// Optimize stat table
|
||||
if ($options['optimiseOnComplete'] == 1) {
|
||||
$this->store->update('OPTIMIZE TABLE stat', []);
|
||||
}
|
||||
|
||||
$this->appendRunMessage(__('Done.'. PHP_EOL));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Loops limit end - task will need to rerun again to start from the saved watermark
|
||||
if ($this->checkLoopLimits($numberOfLoops, $options['numberOfLoops'], $fileName, $watermark) === true) {
|
||||
break;
|
||||
}
|
||||
$numberOfLoops++;
|
||||
|
||||
$statCount = 0;
|
||||
foreach ($stats->fetchAll() as $stat) {
|
||||
// We get the watermark now because if we skip the records our watermark will never reach 0
|
||||
$watermark = $stat['statId'];
|
||||
|
||||
// Get display
|
||||
$display = $this->getDisplay((int) $stat['displayId']);
|
||||
|
||||
if (empty($display)) {
|
||||
$this->log->error('Display not found. Display Id: '. $stat['displayId']);
|
||||
continue;
|
||||
}
|
||||
|
||||
$entry = [];
|
||||
|
||||
$entry['statDate'] = Carbon::createFromTimestamp($stat['statDate']);
|
||||
$entry['type'] = $stat['type'];
|
||||
$entry['fromDt'] = Carbon::createFromTimestamp($stat['start']);
|
||||
$entry['toDt'] = Carbon::createFromTimestamp($stat['end']);
|
||||
$entry['scheduleId'] = (int) $stat['scheduleId'];
|
||||
$entry['mediaId'] = (int) $stat['mediaId'];
|
||||
$entry['layoutId'] = (int) $stat['layoutId'];
|
||||
$entry['display'] = $display;
|
||||
$entry['campaignId'] = (int) $stat['campaignId'];
|
||||
$entry['tag'] = $stat['tag'];
|
||||
$entry['widgetId'] = (int) $stat['widgetId'];
|
||||
$entry['duration'] = (int) $stat['duration'];
|
||||
$entry['count'] = (int) $stat['count'];
|
||||
|
||||
// Add stats in store $this->stats
|
||||
$this->timeSeriesStore->addStat($entry);
|
||||
$statCount++;
|
||||
}
|
||||
|
||||
// Write stats
|
||||
if ($statCount > 0) {
|
||||
$this->timeSeriesStore->addStatFinalize();
|
||||
} else {
|
||||
$this->appendRunMessage('No stat to migrate from stat to mongo');
|
||||
$this->log->debug('No stat to migrate from stat to mongo');
|
||||
}
|
||||
|
||||
// Give Mongo time to recover
|
||||
if ($watermark > 0) {
|
||||
$this->appendRunMessage('- '. $count. ' rows migrated.');
|
||||
$this->log->debug('Mongo stats migration from stat. '.$count.' rows effected, sleeping.');
|
||||
sleep($options['pauseBetweenLoops']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function migrationStatArchiveToMongo($options)
|
||||
{
|
||||
|
||||
$this->appendRunMessage(PHP_EOL. '## Moving from stat_archive to Mongo');
|
||||
$fileName = $this->config->getSetting('LIBRARY_LOCATION') . '.watermark_stat_archive_mongo.txt';
|
||||
|
||||
// Get low watermark from file
|
||||
$watermark = $this->getWatermarkFromFile($fileName, 'stat_archive');
|
||||
|
||||
$numberOfLoops = 0;
|
||||
|
||||
while ($watermark > 0) {
|
||||
$count = 0;
|
||||
/** @noinspection SqlResolve */
|
||||
$stats = $this->store->getConnection()->prepare('
|
||||
SELECT statId, type, statDate, scheduleId, displayId, layoutId, mediaId, widgetId, start, `end`, tag
|
||||
FROM stat_archive
|
||||
WHERE statId < :watermark
|
||||
ORDER BY statId DESC LIMIT :limit
|
||||
');
|
||||
$stats->bindParam(':watermark', $watermark, \PDO::PARAM_INT);
|
||||
$stats->bindParam(':limit', $options['numberOfRecords'], \PDO::PARAM_INT);
|
||||
|
||||
// Run the select
|
||||
$stats->execute();
|
||||
|
||||
// Keep count how many stats we've processed
|
||||
$recordCount = $stats->rowCount();
|
||||
$count+= $recordCount;
|
||||
|
||||
// End of records
|
||||
if ($this->checkEndOfRecords($recordCount, $fileName) === true) {
|
||||
$this->appendRunMessage(PHP_EOL. '# End of records.' . PHP_EOL. '- Dropping stat_archive.');
|
||||
$this->log->debug('End of records in stat_archive (migration to Mongo). Dropping table.');
|
||||
|
||||
// Drop the stat_archive table
|
||||
/** @noinspection SqlResolve */
|
||||
$this->store->update('DROP TABLE `stat_archive`;', []);
|
||||
|
||||
$this->appendRunMessage(__('Done.'. PHP_EOL));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Loops limit end - task will need to rerun again to start from the saved watermark
|
||||
if ($this->checkLoopLimits($numberOfLoops, $options['numberOfLoops'], $fileName, $watermark) === true) {
|
||||
break;
|
||||
}
|
||||
$numberOfLoops++;
|
||||
$temp = [];
|
||||
|
||||
$statIgnoredCount = 0;
|
||||
$statCount = 0;
|
||||
foreach ($stats->fetchAll() as $stat) {
|
||||
// We get the watermark now because if we skip the records our watermark will never reach 0
|
||||
$watermark = $stat['statId'];
|
||||
|
||||
// Get display
|
||||
$display = $this->getDisplay((int) $stat['displayId']);
|
||||
|
||||
if (empty($display)) {
|
||||
$this->log->error('Display not found. Display Id: '. $stat['displayId']);
|
||||
$statIgnoredCount+= 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
$entry = [];
|
||||
|
||||
// Get campaignId
|
||||
if (($stat['type'] != 'event') && ($stat['layoutId'] != null)) {
|
||||
try {
|
||||
// Search the campaignId in the temp array first to reduce query in layouthistory
|
||||
if (array_key_exists($stat['layoutId'], $temp)) {
|
||||
$campaignId = $temp[$stat['layoutId']];
|
||||
} else {
|
||||
$campaignId = $this->layoutFactory->getCampaignIdFromLayoutHistory($stat['layoutId']);
|
||||
$temp[$stat['layoutId']] = $campaignId;
|
||||
}
|
||||
} catch (NotFoundException $error) {
|
||||
$statIgnoredCount+= 1;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
$campaignId = 0;
|
||||
}
|
||||
|
||||
$statDate = Carbon::createFromTimestamp($stat['statDate']);
|
||||
$start = Carbon::createFromTimestamp($stat['start']);
|
||||
$end = Carbon::createFromTimestamp($stat['end']);
|
||||
|
||||
$entry['statDate'] = $statDate;
|
||||
$entry['type'] = $stat['type'];
|
||||
$entry['fromDt'] = $start;
|
||||
$entry['toDt'] = $end;
|
||||
$entry['scheduleId'] = (int) $stat['scheduleId'];
|
||||
$entry['display'] = $display;
|
||||
$entry['campaignId'] = (int) $campaignId;
|
||||
$entry['layoutId'] = (int) $stat['layoutId'];
|
||||
$entry['mediaId'] = (int) $stat['mediaId'];
|
||||
$entry['tag'] = $stat['tag'];
|
||||
$entry['widgetId'] = (int) $stat['widgetId'];
|
||||
$entry['duration'] = (int) $end->diffInSeconds($start);
|
||||
$entry['count'] = isset($stat['count']) ? (int) $stat['count'] : 1;
|
||||
|
||||
// Add stats in store $this->stats
|
||||
$this->timeSeriesStore->addStat($entry);
|
||||
$statCount++;
|
||||
}
|
||||
|
||||
if ($statIgnoredCount > 0) {
|
||||
$this->appendRunMessage($statIgnoredCount. ' stat(s) were ignored while migrating');
|
||||
}
|
||||
|
||||
// Write stats
|
||||
if ($statCount > 0) {
|
||||
$this->timeSeriesStore->addStatFinalize();
|
||||
} else {
|
||||
$this->appendRunMessage('No stat to migrate from stat archive to mongo');
|
||||
$this->log->debug('No stat to migrate from stat archive to mongo');
|
||||
}
|
||||
|
||||
// Give Mongo time to recover
|
||||
if ($watermark > 0) {
|
||||
if ($statCount > 0) {
|
||||
$this->appendRunMessage('- '. $count. ' rows processed. ' . $statCount. ' rows migrated');
|
||||
$this->log->debug('Mongo stats migration from stat_archive. '.$count.' rows effected, sleeping.');
|
||||
}
|
||||
sleep($options['pauseBetweenLoops']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get low watermark from file
|
||||
function getWatermarkFromFile($fileName, $tableName)
|
||||
{
|
||||
|
||||
if (file_exists($fileName)) {
|
||||
$file = fopen($fileName, 'r');
|
||||
$line = fgets($file);
|
||||
fclose($file);
|
||||
$watermark = (int) $line;
|
||||
} else {
|
||||
// Save mysql low watermark in file if .watermark.txt file is not found
|
||||
/** @noinspection SqlResolve */
|
||||
$statId = $this->store->select('SELECT MAX(statId) as statId FROM '.$tableName, []);
|
||||
$watermark = (int) $statId[0]['statId'];
|
||||
|
||||
$out = fopen($fileName, 'w');
|
||||
fwrite($out, $watermark);
|
||||
fclose($out);
|
||||
}
|
||||
|
||||
// We need to increase it
|
||||
$watermark+= 1;
|
||||
$this->appendRunMessage('- Initial watermark is '.$watermark);
|
||||
|
||||
return $watermark;
|
||||
}
|
||||
|
||||
// Check if end of records
|
||||
function checkEndOfRecords($recordCount, $fileName)
|
||||
{
|
||||
|
||||
if ($recordCount == 0) {
|
||||
// No records in stat, save watermark in file
|
||||
$watermark = -1;
|
||||
|
||||
$out = fopen($fileName, 'w');
|
||||
fwrite($out, $watermark);
|
||||
fclose($out);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check loop limits
|
||||
function checkLoopLimits($numberOfLoops, $optionsNumberOfLoops, $fileName, $watermark)
|
||||
{
|
||||
|
||||
if ($numberOfLoops == $optionsNumberOfLoops) {
|
||||
// Save watermark in file
|
||||
$watermark = $watermark - 1;
|
||||
$this->log->debug(' Loop reached limit. Watermark is now '.$watermark);
|
||||
|
||||
$out = fopen($fileName, 'w');
|
||||
fwrite($out, $watermark);
|
||||
fclose($out);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Disable the task
|
||||
function disableTask()
|
||||
{
|
||||
|
||||
$this->appendRunMessage('# Disabling task.');
|
||||
$this->log->debug('Disabling task.');
|
||||
|
||||
$this->getTask()->isActive = 0;
|
||||
$this->getTask()->save();
|
||||
|
||||
$this->appendRunMessage(__('Done.'. PHP_EOL));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable the task
|
||||
function quitMigrationTaskOrDisableStatArchiveTask()
|
||||
{
|
||||
|
||||
// Quit the migration task if stat archive task is running
|
||||
if ($this->archiveTask->status == Task::$STATUS_RUNNING) {
|
||||
$this->appendRunMessage('Quitting the stat migration task as stat archive task is running');
|
||||
$this->log->debug('Quitting the stat migration task as stat archive task is running.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark the Stats Archiver as disabled if it is active
|
||||
if ($this->archiveTask->isActive == 1) {
|
||||
$this->archiveTask->isActive = 0;
|
||||
$this->archiveTask->save();
|
||||
$this->store->commitIfNecessary();
|
||||
|
||||
$this->appendRunMessage('Disabling Stats Archive Task.');
|
||||
$this->log->debug('Disabling Stats Archive Task.');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Cahce/Get display
|
||||
function getDisplay($displayId)
|
||||
{
|
||||
|
||||
// Get display if in memory
|
||||
if (array_key_exists($displayId, $this->displays)) {
|
||||
$display = $this->displays[$displayId];
|
||||
} elseif (array_key_exists($displayId, $this->displaysNotFound)) {
|
||||
// Display not found
|
||||
return false;
|
||||
} else {
|
||||
try {
|
||||
$display = $this->displayFactory->getById($displayId);
|
||||
|
||||
// Cache display
|
||||
$this->displays[$displayId] = $display;
|
||||
} catch (NotFoundException $error) {
|
||||
// Cache display not found
|
||||
$this->displaysNotFound[$displayId] = $displayId;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return $display;
|
||||
}
|
||||
}
|
||||
119
lib/XTR/TaskInterface.php
Normal file
119
lib/XTR/TaskInterface.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
/**
|
||||
* Copyright (C) 2020 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - http://www.xibo.org.uk
|
||||
*
|
||||
* 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 Psr\Container\ContainerInterface;
|
||||
use Stash\Interfaces\PoolInterface;
|
||||
use Xibo\Entity\Task;
|
||||
use Xibo\Entity\User;
|
||||
use Xibo\Service\ConfigServiceInterface;
|
||||
use Xibo\Service\LogServiceInterface;
|
||||
use Xibo\Storage\StorageServiceInterface;
|
||||
use Xibo\Storage\TimeSeriesStoreInterface;
|
||||
use Xibo\Support\Sanitizer\SanitizerInterface;
|
||||
|
||||
/**
|
||||
* Interface TaskInterface
|
||||
* @package Xibo\XTR
|
||||
*/
|
||||
interface TaskInterface
|
||||
{
|
||||
/**
|
||||
* Set the app options
|
||||
* @param ConfigServiceInterface $config
|
||||
* @return $this
|
||||
*/
|
||||
public function setConfig($config);
|
||||
|
||||
/**
|
||||
* @param LogServiceInterface $logger
|
||||
* @return $this
|
||||
*/
|
||||
public function setLogger($logger);
|
||||
|
||||
/**
|
||||
* @param SanitizerInterface $sanitizer
|
||||
* @return $this
|
||||
*/
|
||||
public function setSanitizer($sanitizer);
|
||||
|
||||
/**
|
||||
* @param $array
|
||||
* @return SanitizerInterface
|
||||
*/
|
||||
public function getSanitizer($array);
|
||||
|
||||
/**
|
||||
* Set the task
|
||||
* @param Task $task
|
||||
* @return $this
|
||||
*/
|
||||
public function setTask($task);
|
||||
|
||||
/**
|
||||
* @param StorageServiceInterface $store
|
||||
* @return $this
|
||||
*/
|
||||
public function setStore($store);
|
||||
|
||||
/**
|
||||
* @param TimeSeriesStoreInterface $timeSeriesStore
|
||||
* @return $this
|
||||
*/
|
||||
public function setTimeSeriesStore($timeSeriesStore);
|
||||
|
||||
/**
|
||||
* @param PoolInterface $pool
|
||||
* @return $this
|
||||
*/
|
||||
public function setPool($pool);
|
||||
|
||||
/**
|
||||
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
|
||||
* @return $this
|
||||
*/
|
||||
public function setDispatcher($dispatcher);
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @return $this
|
||||
*/
|
||||
public function setUser($user);
|
||||
|
||||
/**
|
||||
* @param ContainerInterface $container
|
||||
* @return $this
|
||||
*/
|
||||
public function setFactories($container);
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function run();
|
||||
|
||||
/**
|
||||
* Get the run message
|
||||
* @return string
|
||||
*/
|
||||
public function getRunMessage();
|
||||
}
|
||||
216
lib/XTR/TaskTrait.php
Normal file
216
lib/XTR/TaskTrait.php
Normal file
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (c) 2022 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - http://www.xibo.org.uk
|
||||
*
|
||||
* 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 Psr\Log\LoggerInterface;
|
||||
use Stash\Interfaces\PoolInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Xibo\Entity\Task;
|
||||
use Xibo\Entity\User;
|
||||
use Xibo\Helper\SanitizerService;
|
||||
use Xibo\Service\ConfigServiceInterface;
|
||||
use Xibo\Service\LogServiceInterface;
|
||||
use Xibo\Storage\StorageServiceInterface;
|
||||
use Xibo\Storage\TimeSeriesStoreInterface;
|
||||
|
||||
/**
|
||||
* Class TaskTrait
|
||||
* @package Xibo\XTR
|
||||
*/
|
||||
trait TaskTrait
|
||||
{
|
||||
/** @var LogServiceInterface */
|
||||
private $log;
|
||||
|
||||
/** @var ConfigServiceInterface */
|
||||
private $config;
|
||||
|
||||
/** @var SanitizerService */
|
||||
private $sanitizerService;
|
||||
|
||||
/** @var StorageServiceInterface */
|
||||
private $store;
|
||||
|
||||
/** @var TimeSeriesStoreInterface */
|
||||
private $timeSeriesStore;
|
||||
|
||||
/** @var PoolInterface */
|
||||
private $pool;
|
||||
|
||||
/** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface */
|
||||
private $dispatcher;
|
||||
|
||||
/** @var User */
|
||||
private $user;
|
||||
|
||||
/** @var Task */
|
||||
private $task;
|
||||
|
||||
/** @var array */
|
||||
private $options;
|
||||
|
||||
/** @var string */
|
||||
private $runMessage;
|
||||
|
||||
/** @inheritdoc */
|
||||
public function setConfig($config)
|
||||
{
|
||||
$this->config = $config;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Xibo\Service\ConfigServiceInterface
|
||||
*/
|
||||
protected function getConfig(): ConfigServiceInterface
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function setLogger($logger)
|
||||
{
|
||||
$this->log = $logger;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Psr\Log\LoggerInterface
|
||||
*/
|
||||
private function getLogger(): LoggerInterface
|
||||
{
|
||||
return $this->log->getLoggerInterface();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $array
|
||||
* @return \Xibo\Support\Sanitizer\SanitizerInterface
|
||||
*/
|
||||
public function getSanitizer($array)
|
||||
{
|
||||
return $this->sanitizerService->getSanitizer($array);
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function setSanitizer($sanitizer)
|
||||
{
|
||||
$this->sanitizerService = $sanitizer;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function setTask($task)
|
||||
{
|
||||
$options = $task->options;
|
||||
|
||||
if (property_exists($this, 'defaultConfig'))
|
||||
$options = array_merge($this->defaultConfig, $options);
|
||||
|
||||
$this->task = $task;
|
||||
$this->options = $options;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function setStore($store)
|
||||
{
|
||||
$this->store = $store;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function setTimeSeriesStore($timeSeriesStore)
|
||||
{
|
||||
$this->timeSeriesStore = $timeSeriesStore;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function setPool($pool)
|
||||
{
|
||||
$this->pool = $pool;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
|
||||
* @return $this
|
||||
*/
|
||||
public function setDispatcher($dispatcher)
|
||||
{
|
||||
$this->dispatcher = $dispatcher;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Symfony\Component\EventDispatcher\EventDispatcherInterface
|
||||
*/
|
||||
protected function getDispatcher(): EventDispatcherInterface
|
||||
{
|
||||
return $this->dispatcher;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function setUser($user)
|
||||
{
|
||||
$this->user = $user;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function getRunMessage()
|
||||
{
|
||||
return $this->runMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get task
|
||||
* @return Task
|
||||
*/
|
||||
private function getTask()
|
||||
{
|
||||
return $this->task;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $option
|
||||
* @param $default
|
||||
* @return mixed
|
||||
*/
|
||||
private function getOption($option, $default)
|
||||
{
|
||||
return $this->options[$option] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append Run Message
|
||||
* @param $message
|
||||
*/
|
||||
private function appendRunMessage($message)
|
||||
{
|
||||
if ($this->runMessage === null)
|
||||
$this->runMessage = '';
|
||||
|
||||
$this->runMessage .= $message . PHP_EOL;
|
||||
}
|
||||
}
|
||||
52
lib/XTR/UpdateEmptyVideoDurations.php
Normal file
52
lib/XTR/UpdateEmptyVideoDurations.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
/*
|
||||
* Spring Signage Ltd - http://www.springsignage.com
|
||||
* Copyright (C) 2018 Spring Signage Ltd
|
||||
* (UpdateEmptyVideoDurations.php)
|
||||
*/
|
||||
|
||||
namespace Xibo\XTR;
|
||||
|
||||
use Xibo\Factory\MediaFactory;
|
||||
use Xibo\Factory\ModuleFactory;
|
||||
|
||||
/**
|
||||
* Class UpdateEmptyVideoDurations
|
||||
* @package Xibo\XTR
|
||||
*
|
||||
* update video durations
|
||||
*/
|
||||
class UpdateEmptyVideoDurations implements TaskInterface
|
||||
{
|
||||
use TaskTrait;
|
||||
|
||||
/** @var MediaFactory */
|
||||
private $mediaFactory;
|
||||
|
||||
/** @var ModuleFactory */
|
||||
private $moduleFactory;
|
||||
|
||||
/** @inheritdoc */
|
||||
public function setFactories($container)
|
||||
{
|
||||
$this->mediaFactory = $container->get('mediaFactory');
|
||||
$this->moduleFactory = $container->get('moduleFactory');
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function run()
|
||||
{
|
||||
$libraryLocation = $this->config->getSetting('LIBRARY_LOCATION');
|
||||
$module = $this->moduleFactory->getByType('video');
|
||||
$videos = $this->mediaFactory->getByMediaType($module->type);
|
||||
|
||||
foreach ($videos as $video) {
|
||||
if ($video->duration == 0) {
|
||||
// Update
|
||||
$video->duration = $module->fetchDurationOrDefaultFromFile($libraryLocation . $video->storedAs);
|
||||
$video->save(['validate' => false]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
248
lib/XTR/WidgetCompatibilityTask.php
Normal file
248
lib/XTR/WidgetCompatibilityTask.php
Normal file
@@ -0,0 +1,248 @@
|
||||
<?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 Xibo\Support\Exception\GeneralException;
|
||||
use Xibo\Support\Exception\NotFoundException;
|
||||
|
||||
/**
|
||||
* Class WidgetCompatibilityTask
|
||||
* Run only once when upgrading widget from v3 to v4
|
||||
* @package Xibo\XTR
|
||||
*/
|
||||
class WidgetCompatibilityTask implements TaskInterface
|
||||
{
|
||||
use TaskTrait;
|
||||
|
||||
/** @var \Xibo\Factory\ModuleFactory */
|
||||
private $moduleFactory;
|
||||
|
||||
/** @var \Xibo\Factory\WidgetFactory */
|
||||
private $widgetFactory;
|
||||
|
||||
/** @var \Xibo\Factory\LayoutFactory */
|
||||
private $layoutFactory;
|
||||
|
||||
/** @var \Xibo\Factory\playlistFactory */
|
||||
private $playlistFactory;
|
||||
|
||||
/** @var \Xibo\Factory\TaskFactory */
|
||||
private $taskFactory;
|
||||
/** @var \Xibo\Factory\RegionFactory */
|
||||
private $regionFactory;
|
||||
|
||||
/** @var array The cache for layout */
|
||||
private $layoutCache = [];
|
||||
|
||||
/** @inheritdoc */
|
||||
public function setFactories($container)
|
||||
{
|
||||
$this->moduleFactory = $container->get('moduleFactory');
|
||||
$this->widgetFactory = $container->get('widgetFactory');
|
||||
$this->layoutFactory = $container->get('layoutFactory');
|
||||
$this->playlistFactory = $container->get('playlistFactory');
|
||||
$this->taskFactory = $container->get('taskFactory');
|
||||
$this->regionFactory = $container->get('regionFactory');
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @inheritdoc
|
||||
*/
|
||||
public function run()
|
||||
{
|
||||
// This task should only be run once when upgrading for the first time from v3 to v4.
|
||||
// If the Widget Compatibility class is defined, it needs to be executed to upgrade the widgets.
|
||||
$this->runMessage = '# ' . __('Widget Compatibility') . PHP_EOL . PHP_EOL;
|
||||
|
||||
// Get all modules
|
||||
$modules = $this->moduleFactory->getAll();
|
||||
|
||||
// For each module we should get all widgets which are < the schema version of the module installed, and
|
||||
// upgrade them to the schema version of the module installed
|
||||
foreach ($modules as $module) {
|
||||
// Run upgrade - Part 1
|
||||
// Upgrade a widget having the same module type
|
||||
$this->getLogger()->debug('run: finding widgets for ' . $module->type
|
||||
. ' with schema version less than ' . $module->schemaVersion);
|
||||
|
||||
$statement = $this->executeStatement($module->type, $module->schemaVersion);
|
||||
$this->upgradeWidget($statement);
|
||||
|
||||
// Run upgrade - Part 2
|
||||
// Upgrade a widget having the old style module type/legacy type
|
||||
$legacyTypes = [];
|
||||
if (count($module->legacyTypes) > 0) {
|
||||
// Get the name of the module legacy types
|
||||
$legacyTypes = array_column($module->legacyTypes, 'name'); // TODO Make this efficient
|
||||
}
|
||||
|
||||
// Get module legacy type and update matched widgets
|
||||
foreach ($legacyTypes as $legacyType) {
|
||||
$statement = $this->executeStatement($legacyType, $module->schemaVersion);
|
||||
$this->upgradeWidget($statement);
|
||||
}
|
||||
}
|
||||
|
||||
// Get Widget Compatibility Task
|
||||
$compatibilityTask = $this->taskFactory->getByClass('\Xibo\XTR\\WidgetCompatibilityTask');
|
||||
|
||||
// Mark the task as disabled if it is active
|
||||
if ($compatibilityTask->isActive == 1) {
|
||||
$compatibilityTask->isActive = 0;
|
||||
$compatibilityTask->save();
|
||||
$this->store->commitIfNecessary();
|
||||
|
||||
$this->appendRunMessage('Disabling widget compatibility task.');
|
||||
$this->log->debug('Disabling widget compatibility task.');
|
||||
}
|
||||
$this->appendRunMessage(__('Done.'. PHP_EOL));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string $type
|
||||
* @param int $version
|
||||
* @return false|\PDOStatement
|
||||
*/
|
||||
private function executeStatement(string $type, int $version): bool|\PDOStatement
|
||||
{
|
||||
$sql = '
|
||||
SELECT widget.widgetId
|
||||
FROM `widget`
|
||||
WHERE `widget`.`type` = :type
|
||||
and `widget`.schemaVersion < :version
|
||||
';
|
||||
$connection = $this->store->getConnection();
|
||||
$connection->setAttribute(\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true);
|
||||
|
||||
// Prepare the statement
|
||||
$statement = $connection->prepare($sql);
|
||||
|
||||
// Execute
|
||||
$statement->execute([
|
||||
'type' => $type,
|
||||
'version' => $version
|
||||
]);
|
||||
|
||||
return $statement;
|
||||
}
|
||||
|
||||
private function upgradeWidget(\PDOStatement $statement): void
|
||||
{
|
||||
// Load each widget and its options
|
||||
// Then run upgrade
|
||||
while ($row = $statement->fetch(\PDO::FETCH_ASSOC)) {
|
||||
try {
|
||||
$widget = $this->widgetFactory->getById((int) $row['widgetId']);
|
||||
$widget->loadMinimum();
|
||||
|
||||
$this->log->debug('WidgetCompatibilityTask: Get Widget: ' . $row['widgetId']);
|
||||
|
||||
// Form conditions from the widget's option and value, e.g, templateId==worldclock1
|
||||
$widgetConditionMatch = [];
|
||||
foreach ($widget->widgetOptions as $option) {
|
||||
$widgetConditionMatch[] = $option->option . '==' . $option->value;
|
||||
}
|
||||
|
||||
// Get module
|
||||
try {
|
||||
$module = $this->moduleFactory->getByType($widget->type, $widgetConditionMatch);
|
||||
} catch (NotFoundException $e) {
|
||||
$this->log->error('Module not found for widget: ' . $widget->type);
|
||||
$this->appendRunMessage('Module not found for widget: '. $widget->widgetId);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Run upgrade
|
||||
if ($module->isWidgetCompatibilityAvailable()) {
|
||||
// Grab a widget compatibility interface, if there is one
|
||||
$widgetCompatibilityInterface = $module->getWidgetCompatibilityOrNull();
|
||||
if ($widgetCompatibilityInterface !== null) {
|
||||
try {
|
||||
// Pass the widget through the compatibility interface.
|
||||
$upgraded = $widgetCompatibilityInterface->upgradeWidget(
|
||||
$widget,
|
||||
$widget->schemaVersion,
|
||||
$module->schemaVersion
|
||||
);
|
||||
|
||||
// Save widget version
|
||||
if ($upgraded) {
|
||||
$widget->schemaVersion = $module->schemaVersion;
|
||||
|
||||
// Assert the module type, unless the widget has already changed it.
|
||||
if (!$widget->hasPropertyChanged('type')) {
|
||||
$widget->type = $module->type;
|
||||
}
|
||||
|
||||
$widget->save(['alwaysUpdate' => true, 'upgrade' => true]);
|
||||
$this->log->debug('WidgetCompatibilityTask: Upgraded');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->log->error('Failed to upgrade for widgetId: ' . $widget->widgetId .
|
||||
', message: ' . $e->getMessage());
|
||||
$this->appendRunMessage('Failed to upgrade for widgetId: : '. $widget->widgetId);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the layout of the widget and set it to rebuild.
|
||||
$playlist = $this->playlistFactory->getById($widget->playlistId);
|
||||
|
||||
// check if the Widget was assigned to a region playlist
|
||||
if ($playlist->isRegionPlaylist()) {
|
||||
$playlist->load();
|
||||
$region = $this->regionFactory->getById($playlist->regionId);
|
||||
|
||||
// set the region type accordingly
|
||||
if ($region->isDrawer === 1) {
|
||||
$regionType = 'drawer';
|
||||
} else if (count($playlist->widgets) === 1 && $widget->type !== 'subplaylist') {
|
||||
$regionType = 'frame';
|
||||
} else if (count($playlist->widgets) === 0) {
|
||||
$regionType = 'zone';
|
||||
} else {
|
||||
$regionType = 'playlist';
|
||||
}
|
||||
|
||||
$region->type = $regionType;
|
||||
$region->save(['notify' => false]);
|
||||
}
|
||||
$playlist->notifyLayouts();
|
||||
} catch (\Exception $e) {
|
||||
$this->log->error('Failed to set layout rebuild for widgetId: ' . $widget->widgetId .
|
||||
', message: ' . $e->getMessage());
|
||||
$this->appendRunMessage('Layout rebuild error for widgetId: : '. $widget->widgetId);
|
||||
}
|
||||
} else {
|
||||
$this->getLogger()->debug('upgradeWidget: no compatibility task available for ' . $widget->type);
|
||||
}
|
||||
} catch (GeneralException $e) {
|
||||
$this->log->debug($e->getTraceAsString());
|
||||
$this->log->error('WidgetCompatibilityTask: Cannot process widget');
|
||||
}
|
||||
}
|
||||
|
||||
$this->store->commitIfNecessary();
|
||||
}
|
||||
}
|
||||
493
lib/XTR/WidgetSyncTask.php
Normal file
493
lib/XTR/WidgetSyncTask.php
Normal file
@@ -0,0 +1,493 @@
|
||||
<?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 Xibo\Entity\Display;
|
||||
use Xibo\Entity\Module;
|
||||
use Xibo\Entity\Widget;
|
||||
use Xibo\Event\WidgetDataRequestEvent;
|
||||
use Xibo\Factory\WidgetDataFactory;
|
||||
use Xibo\Helper\DateFormatHelper;
|
||||
use Xibo\Support\Exception\GeneralException;
|
||||
use Xibo\Support\Exception\NotFoundException;
|
||||
use Xibo\Widget\Provider\WidgetProviderInterface;
|
||||
|
||||
/**
|
||||
* Class WidgetSyncTask
|
||||
* Keep all widgets which have data up to date
|
||||
* @package Xibo\XTR
|
||||
*/
|
||||
class WidgetSyncTask implements TaskInterface
|
||||
{
|
||||
use TaskTrait;
|
||||
|
||||
/** @var \Xibo\Factory\ModuleFactory */
|
||||
private $moduleFactory;
|
||||
|
||||
/** @var \Xibo\Factory\WidgetFactory */
|
||||
private $widgetFactory;
|
||||
|
||||
/** @var \Xibo\Factory\WidgetDataFactory */
|
||||
private WidgetDataFactory $widgetDataFactory;
|
||||
|
||||
/** @var \Xibo\Factory\MediaFactory */
|
||||
private $mediaFactory;
|
||||
|
||||
/** @var \Xibo\Factory\DisplayFactory */
|
||||
private $displayFactory;
|
||||
|
||||
/** @var \Symfony\Component\EventDispatcher\EventDispatcher */
|
||||
private $eventDispatcher;
|
||||
|
||||
/** @inheritdoc */
|
||||
public function setFactories($container)
|
||||
{
|
||||
$this->moduleFactory = $container->get('moduleFactory');
|
||||
$this->widgetFactory = $container->get('widgetFactory');
|
||||
$this->widgetDataFactory = $container->get('widgetDataFactory');
|
||||
$this->mediaFactory = $container->get('mediaFactory');
|
||||
$this->displayFactory = $container->get('displayFactory');
|
||||
$this->eventDispatcher = $container->get('dispatcher');
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function run()
|
||||
{
|
||||
// Track the total time we've spent caching
|
||||
$timeCaching = 0.0;
|
||||
$countWidgets = 0;
|
||||
$cutOff = Carbon::now()->subHours(2);
|
||||
|
||||
// Update for widgets which are active on displays, or for displays which have been active recently.
|
||||
$sql = '
|
||||
SELECT DISTINCT `requiredfile`.itemId, `requiredfile`.complete
|
||||
FROM `requiredfile`
|
||||
INNER JOIN `display`
|
||||
ON `display`.displayId = `requiredfile`.displayId
|
||||
WHERE `requiredfile`.type = \'D\'
|
||||
AND (`display`.loggedIn = 1 OR `display`.lastAccessed > :lastAccessed)
|
||||
ORDER BY `requiredfile`.complete DESC, `requiredfile`.itemId
|
||||
';
|
||||
|
||||
$smt = $this->store->getConnection()->prepare($sql);
|
||||
$smt->execute(['lastAccessed' => $cutOff->unix()]);
|
||||
|
||||
$row = true;
|
||||
while ($row) {
|
||||
$row = $smt->fetch(\PDO::FETCH_ASSOC);
|
||||
try {
|
||||
if ($row !== false) {
|
||||
$widgetId = (int)$row['itemId'];
|
||||
|
||||
$this->getLogger()->debug('widgetSyncTask: processing itemId ' . $widgetId);
|
||||
|
||||
// What type of widget do we have here.
|
||||
$widget = $this->widgetFactory->getById($widgetId)->load();
|
||||
|
||||
// Get the module
|
||||
$module = $this->moduleFactory->getByType($widget->type);
|
||||
|
||||
// If this widget's module expects data to be provided (i.e. has a datatype) then make sure that
|
||||
// data is cached ahead of time here.
|
||||
// This also refreshes any library or external images referenced by the data so that they aren't
|
||||
// considered for removal.
|
||||
if ($module->isDataProviderExpected()) {
|
||||
$this->getLogger()->debug('widgetSyncTask: data provider expected.');
|
||||
|
||||
// Record start time
|
||||
$countWidgets++;
|
||||
$startTime = microtime(true);
|
||||
|
||||
// Grab a widget interface, if there is one
|
||||
$widgetInterface = $module->getWidgetProviderOrNull();
|
||||
|
||||
// Is the cache key display specific?
|
||||
$dataProvider = $module->createDataProvider($widget);
|
||||
$cacheKey = $widgetInterface?->getDataCacheKey($dataProvider);
|
||||
|
||||
if ($cacheKey === null) {
|
||||
$cacheKey = $module->dataCacheKey;
|
||||
}
|
||||
|
||||
// Refresh the cache if needed.
|
||||
$isDisplaySpecific = str_contains($cacheKey, '%displayId%')
|
||||
|| (str_contains($cacheKey, '%useDisplayLocation%')
|
||||
&& $dataProvider->getProperty('useDisplayLocation') == 1);
|
||||
|
||||
// We're either assigning all media to all displays, or we're assigning them one by one
|
||||
if ($isDisplaySpecific) {
|
||||
$this->getLogger()->debug('widgetSyncTask: cache is display specific');
|
||||
|
||||
// We need to run the cache for every display this widget is assigned to.
|
||||
foreach ($this->getDisplays($widget) as $display) {
|
||||
$cacheData = $this->cache(
|
||||
$module,
|
||||
$widget,
|
||||
$widgetInterface,
|
||||
$display,
|
||||
);
|
||||
|
||||
$this->linkDisplays(
|
||||
$widget->widgetId,
|
||||
[$display],
|
||||
$cacheData['mediaIds'],
|
||||
$cacheData['fromCache']
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$this->getLogger()->debug('widgetSyncTask: cache is not display specific');
|
||||
|
||||
// Just a single run will do it.
|
||||
$cacheData = $this->cache($module, $widget, $widgetInterface, null);
|
||||
|
||||
$this->linkDisplays(
|
||||
$widget->widgetId,
|
||||
$this->getDisplays($widget),
|
||||
$cacheData['mediaIds'],
|
||||
$cacheData['fromCache']
|
||||
);
|
||||
}
|
||||
|
||||
// Record end time and aggregate for final total
|
||||
$duration = (microtime(true) - $startTime);
|
||||
$timeCaching = $timeCaching + $duration;
|
||||
$this->log->debug('widgetSyncTask: Took ' . $duration
|
||||
. ' seconds to check and/or cache widgetId ' . $widget->widgetId);
|
||||
|
||||
// Commit so that any images we've downloaded have their cache times updated for the
|
||||
// next request, this makes sense because we've got a file cache that is already written
|
||||
// out.
|
||||
$this->store->commitIfNecessary();
|
||||
}
|
||||
}
|
||||
} catch (GeneralException $xiboException) {
|
||||
$this->log->debug($xiboException->getTraceAsString());
|
||||
$this->log->error('widgetSyncTask: Cannot process widget ' . $widgetId
|
||||
. ', E = ' . $xiboException->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Remove display_media records which have not been touched for a defined period of time.
|
||||
$this->removeOldDisplayLinks($cutOff);
|
||||
|
||||
$this->log->info('Total time spent caching is ' . $timeCaching . ', synced ' . $countWidgets . ' widgets');
|
||||
|
||||
$this->appendRunMessage('Synced ' . $countWidgets . ' widgets');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Module $module
|
||||
* @param Widget $widget
|
||||
* @param WidgetProviderInterface|null $widgetInterface
|
||||
* @param Display|null $display
|
||||
* @return array
|
||||
* @throws GeneralException
|
||||
*/
|
||||
private function cache(
|
||||
Module $module,
|
||||
Widget $widget,
|
||||
?WidgetProviderInterface $widgetInterface,
|
||||
?Display $display
|
||||
): array {
|
||||
$this->getLogger()->debug('cache: ' . $widget->widgetId . ' for display: ' . ($display?->displayId ?? 0));
|
||||
|
||||
// Each time we call this we use a new provider
|
||||
$dataProvider = $module->createDataProvider($widget);
|
||||
$dataProvider->setMediaFactory($this->mediaFactory);
|
||||
|
||||
// Set our provider up for the display
|
||||
$dataProvider->setDisplayProperties(
|
||||
$display?->latitude ?: $this->getConfig()->getSetting('DEFAULT_LAT'),
|
||||
$display?->longitude ?: $this->getConfig()->getSetting('DEFAULT_LONG'),
|
||||
$display?->displayId ?? 0
|
||||
);
|
||||
|
||||
$widgetDataProviderCache = $this->moduleFactory->createWidgetDataProviderCache();
|
||||
|
||||
// Get the cache key
|
||||
$cacheKey = $this->moduleFactory->determineCacheKey(
|
||||
$module,
|
||||
$widget,
|
||||
$display?->displayId ?? 0,
|
||||
$dataProvider,
|
||||
$widgetInterface
|
||||
);
|
||||
|
||||
// Get the data modified date
|
||||
$dataModifiedDt = null;
|
||||
if ($widgetInterface !== null) {
|
||||
$dataModifiedDt = $widgetInterface->getDataModifiedDt($dataProvider);
|
||||
|
||||
if ($dataModifiedDt !== null) {
|
||||
$this->getLogger()->debug('cache: data modifiedDt is ' . $dataModifiedDt->toAtomString());
|
||||
}
|
||||
}
|
||||
|
||||
// Will we use fallback data if available?
|
||||
$showFallback = $widget->getOptionValue('showFallback', 'never');
|
||||
if ($showFallback !== 'never') {
|
||||
// What data type are we dealing with?
|
||||
try {
|
||||
$dataTypeFields = [];
|
||||
foreach ($this->moduleFactory->getDataTypeById($module->dataType)->fields as $field) {
|
||||
$dataTypeFields[$field->id] = $field->type;
|
||||
}
|
||||
|
||||
// Potentially we will, so get the modifiedDt of this fallback data.
|
||||
$fallbackModifiedDt = $this->widgetDataFactory->getModifiedDtForWidget($widget->widgetId);
|
||||
|
||||
if ($fallbackModifiedDt !== null) {
|
||||
$this->getLogger()->debug('cache: fallback modifiedDt is ' . $fallbackModifiedDt->toAtomString());
|
||||
|
||||
$dataModifiedDt = max($dataModifiedDt, $fallbackModifiedDt);
|
||||
}
|
||||
} catch (NotFoundException) {
|
||||
$this->getLogger()->info('cache: widget will fallback set where the module does not support it');
|
||||
$dataTypeFields = null;
|
||||
}
|
||||
} else {
|
||||
$dataTypeFields = null;
|
||||
}
|
||||
|
||||
// Is this data from cache?
|
||||
$fromCache = false;
|
||||
|
||||
if (!$widgetDataProviderCache->decorateWithCache($dataProvider, $cacheKey, $dataModifiedDt)
|
||||
|| $widgetDataProviderCache->isCacheMissOrOld()
|
||||
) {
|
||||
$this->getLogger()->debug('cache: Cache expired, pulling fresh: key: ' . $cacheKey);
|
||||
|
||||
$dataProvider->clearData();
|
||||
$dataProvider->clearMeta();
|
||||
$dataProvider->addOrUpdateMeta('showFallback', $showFallback);
|
||||
|
||||
try {
|
||||
if ($widgetInterface !== null) {
|
||||
$widgetInterface->fetchData($dataProvider);
|
||||
} else {
|
||||
$dataProvider->setIsUseEvent();
|
||||
}
|
||||
|
||||
if ($dataProvider->isUseEvent()) {
|
||||
$this->getDispatcher()->dispatch(
|
||||
new WidgetDataRequestEvent($dataProvider),
|
||||
WidgetDataRequestEvent::$NAME
|
||||
);
|
||||
}
|
||||
|
||||
// Before caching images, check to see if the data provider is handled
|
||||
$isFallback = false;
|
||||
if ($showFallback !== 'never'
|
||||
&& $dataTypeFields !== null
|
||||
&& (
|
||||
count($dataProvider->getErrors()) > 0
|
||||
|| count($dataProvider->getData()) <= 0
|
||||
|| $showFallback === 'always'
|
||||
)
|
||||
) {
|
||||
// Error or no data.
|
||||
// Pull in the fallback data
|
||||
foreach ($this->widgetDataFactory->getByWidgetId($dataProvider->getWidgetId()) as $item) {
|
||||
// Handle any special data types in the fallback data
|
||||
foreach ($item->data as $itemId => $itemData) {
|
||||
if (!empty($itemData)
|
||||
&& array_key_exists($itemId, $dataTypeFields)
|
||||
&& $dataTypeFields[$itemId] === 'image'
|
||||
) {
|
||||
$item->data[$itemId] = $dataProvider->addLibraryFile($itemData);
|
||||
}
|
||||
}
|
||||
|
||||
$dataProvider->addItem($item->data);
|
||||
|
||||
// Indicate we've been handled by fallback data
|
||||
$isFallback = true;
|
||||
}
|
||||
|
||||
if ($isFallback) {
|
||||
$dataProvider->addOrUpdateMeta('includesFallback', true);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove fallback data from the cache if no-longer needed
|
||||
if (!$isFallback) {
|
||||
$dataProvider->addOrUpdateMeta('includesFallback', false);
|
||||
}
|
||||
|
||||
// Do we have images?
|
||||
// They could be library images (i.e. they already exist) or downloads
|
||||
$mediaIds = $dataProvider->getImageIds();
|
||||
if (count($mediaIds) > 0) {
|
||||
// Process the downloads.
|
||||
$this->mediaFactory->processDownloads(function ($media) {
|
||||
/** @var \Xibo\Entity\Media $media */
|
||||
// Success
|
||||
// We don't need to do anything else, references to mediaId will be built when we decorate
|
||||
// the HTML.
|
||||
$this->getLogger()->debug('cache: Successfully downloaded ' . $media->mediaId);
|
||||
}, function ($media) use (&$mediaIds) {
|
||||
/** @var \Xibo\Entity\Media $media */
|
||||
// Error
|
||||
// Remove it
|
||||
unset($mediaIds[$media->mediaId]);
|
||||
});
|
||||
}
|
||||
|
||||
// Save to cache
|
||||
if ($dataProvider->isHandled() || $isFallback) {
|
||||
$widgetDataProviderCache->saveToCache($dataProvider);
|
||||
}
|
||||
} finally {
|
||||
$widgetDataProviderCache->finaliseCache();
|
||||
}
|
||||
} else {
|
||||
$this->getLogger()->debug('cache: Cache still valid, key: ' . $cacheKey);
|
||||
|
||||
$fromCache = true;
|
||||
|
||||
// Get the existing mediaIds so that we can maintain the links to displays.
|
||||
$mediaIds = $widgetDataProviderCache->getCachedMediaIds();
|
||||
}
|
||||
|
||||
return [
|
||||
'mediaIds' => $mediaIds,
|
||||
'fromCache' => $fromCache
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Xibo\Entity\Widget $widget
|
||||
* @return Display[]
|
||||
*/
|
||||
private function getDisplays(Widget $widget): array
|
||||
{
|
||||
$sql = '
|
||||
SELECT DISTINCT `requiredfile`.`displayId`
|
||||
FROM `requiredfile`
|
||||
WHERE itemId = :widgetId
|
||||
AND type = \'D\'
|
||||
';
|
||||
|
||||
$displayIds = [];
|
||||
foreach ($this->store->select($sql, ['widgetId' => $widget->widgetId]) as $record) {
|
||||
$displayId = intval($record['displayId']);
|
||||
try {
|
||||
$displayIds[] = $this->displayFactory->getById($displayId);
|
||||
} catch (NotFoundException) {
|
||||
$this->getLogger()->error('getDisplayIds: unknown displayId: ' . $displayId);
|
||||
}
|
||||
}
|
||||
|
||||
return $displayIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Link an array of displays with an array of media
|
||||
* @param int $widgetId
|
||||
* @param Display[] $displays
|
||||
* @param int[] $mediaIds
|
||||
* @param bool $fromCache
|
||||
* @return void
|
||||
*/
|
||||
private function linkDisplays(int $widgetId, array $displays, array $mediaIds, bool $fromCache): void
|
||||
{
|
||||
$this->getLogger()->debug('linkDisplays: ' . count($displays) . ' displays, ' . count($mediaIds) . ' media');
|
||||
|
||||
$sql = '
|
||||
INSERT INTO `display_media` (`displayId`, `mediaId`, `modifiedAt`)
|
||||
VALUES (:displayId, :mediaId, CURRENT_TIMESTAMP)
|
||||
ON DUPLICATE KEY UPDATE `modifiedAt` = CURRENT_TIMESTAMP
|
||||
';
|
||||
|
||||
// Run invididual updates so that we can see if we've made a change.
|
||||
// With ON DUPLICATE KEY UPDATE, the affected-rows value per row is
|
||||
// 1 if the row is inserted as a new row,
|
||||
// 2 if an existing row is updated and
|
||||
// 0 if the existing row is set to its current values.
|
||||
foreach ($displays as $display) {
|
||||
$shouldNotify = false;
|
||||
foreach ($mediaIds as $mediaId) {
|
||||
try {
|
||||
$affected = $this->store->update($sql, [
|
||||
'displayId' => $display->displayId,
|
||||
'mediaId' => $mediaId
|
||||
]);
|
||||
|
||||
if ($affected == 1) {
|
||||
$shouldNotify = true;
|
||||
}
|
||||
} catch (\PDOException) {
|
||||
// We link what we can, and log any failures.
|
||||
$this->getLogger()->error('linkDisplays: unable to link displayId: ' . $display->displayId
|
||||
. ' to mediaId: ' . $mediaId . ', most likely the media has since gone');
|
||||
}
|
||||
}
|
||||
|
||||
// When should we notify?
|
||||
// ----------------------
|
||||
// Newer displays (>= v4) should clear their cache only if linked media has changed
|
||||
// Older displays (< v4) should check in immediately on change
|
||||
if ($display->clientCode >= 400) {
|
||||
if ($shouldNotify) {
|
||||
$this->displayFactory->getDisplayNotifyService()->collectLater()
|
||||
->notifyByDisplayId($display->displayId);
|
||||
}
|
||||
|
||||
if (!$fromCache) {
|
||||
$this->displayFactory->getDisplayNotifyService()
|
||||
->notifyDataUpdate($display, $widgetId);
|
||||
}
|
||||
} else {
|
||||
$this->displayFactory->getDisplayNotifyService()->collectNow()
|
||||
->notifyByDisplayId($display->displayId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any display/media links which have expired.
|
||||
* @param Carbon $cutOff
|
||||
* @return void
|
||||
*/
|
||||
private function removeOldDisplayLinks(Carbon $cutOff): void
|
||||
{
|
||||
$sql = '
|
||||
DELETE
|
||||
FROM `display_media`
|
||||
WHERE `modifiedAt` < :modifiedAt
|
||||
AND `display_media`.`displayId` IN (
|
||||
SELECT `displayId`
|
||||
FROM `display`
|
||||
WHERE `display`.`loggedIn` = 1
|
||||
OR `display`.`lastAccessed` > :lastAccessed
|
||||
)
|
||||
';
|
||||
|
||||
$this->store->update($sql, [
|
||||
'modifiedAt' => Carbon::now()->subDay()->format(DateFormatHelper::getSystemFormat()),
|
||||
'lastAccessed' => $cutOff->unix(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user