From 6b8c87a1d8818f4d322ad7fef4f641e7f68a498e Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Fri, 14 Jun 2024 09:35:11 +0100 Subject: [PATCH 1/6] Module: webpage widget should use original width/height (which is region width/height) rather than determining it client side. (#2565) fixes xibosignage/xibo#3424 --- modules/webpage.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/webpage.xml b/modules/webpage.xml index 453cee844f..5eb157020e 100644 --- a/modules/webpage.xml +++ b/modules/webpage.xml @@ -146,8 +146,8 @@ $(target).xiboLayoutScaler(properties); // Set dimensions based on the properties -properties.iframeWidth = properties.pageWidth ? properties.pageWidth : $(target).width(); -properties.iframeHeight = properties.pageHeight ? properties.pageHeight : $(target).height(); +properties.iframeWidth = properties.pageWidth ? properties.pageWidth : globalOptions.originalWidth; +properties.iframeHeight = properties.pageHeight ? properties.pageHeight : globalOptions.originalHeight; properties.iframeTop = properties.offsetTop ? properties.offsetTop : 0; properties.iframeLeft = properties.offsetLeft ? properties.offsetLeft : 0; properties.scale = properties.scaling ? (properties.scaling/ 100) : 1; From 846f61ab9bba0033be63893d8908bdaae9dc172a Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Fri, 14 Jun 2024 14:02:04 +0100 Subject: [PATCH 2/6] Module: HTML cache can be generated via XMDS which fails for HLS. (#2561) xibosignage/xibo#3433 --- lib/Middleware/State.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/Middleware/State.php b/lib/Middleware/State.php index 0a4b241000..4e856b157c 100644 --- a/lib/Middleware/State.php +++ b/lib/Middleware/State.php @@ -205,12 +205,13 @@ public static function setState(App $app, Request $request): Request $mode = $container->get('configService')->getSetting('SERVER_MODE'); $container->get('logService')->setMode($mode); - - if ($container->get('name') == 'web' || $container->get('name') == 'xtr') { + // Inject some additional changes on a per-container basis + $containerName = $container->get('name'); + if ($containerName == 'web' || $containerName == 'xtr' || $containerName == 'xmds') { /** @var Twig $view */ $view = $container->get('view'); - if ($container->get('name') == 'web') { + if ($containerName == 'web') { $container->set('flash', function () { return new \Slim\Flash\Messages(); }); @@ -223,8 +224,11 @@ public static function setState(App $app, Request $request): Request $filter = new \Twig\TwigFilter('url_decode', 'urldecode'); $twigEnvironment->addFilter($filter); - // set Twig auto reload - $twigEnvironment->enableAutoReload(); + // Set Twig auto reload if needed + // XMDS only renders widget html cache, and shouldn't need auto reload. + if ($containerName !== 'xmds') { + $twigEnvironment->enableAutoReload(); + } } // Configure logging From 3a2c2f9639826d0bb8690ab5345215bb44c6018e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 16 Jun 2024 09:54:24 +0000 Subject: [PATCH 3/6] Bump braces from 3.0.2 to 3.0.3 Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3. - [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3) --- updated-dependencies: - dependency-name: braces dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index fcb3bf87b2..0186d7d742 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5103,12 +5103,23 @@ } }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" + }, + "dependencies": { + "fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + } } }, "browserslist": { @@ -7342,15 +7353,6 @@ "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-1.3.8.tgz", "integrity": "sha512-spKHSBQIxxS81N/O21WmuXA2F6wppUCsutpzenOeZzOCCJ5gEfcbqJP983IrpLXzYmXnMUa6J03SubcNPdKrlg==" }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, "find-cache-dir": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", From 0649b83de83b486416ffaaa759505c98080413c5 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Mon, 17 Jun 2024 08:22:46 +0100 Subject: [PATCH 4/6] Module: improve the way widget data is cached and returned (#2566) * Module: improvements to the way we return old widget data which is cached, so that it is cached for 15 minute or 50% over the cache period fixes xibosignage/xibo#3434 --- lib/Widget/Render/WidgetDataProviderCache.php | 47 +++++++++++++------ 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/lib/Widget/Render/WidgetDataProviderCache.php b/lib/Widget/Render/WidgetDataProviderCache.php index c8f0235cdb..7986202677 100644 --- a/lib/Widget/Render/WidgetDataProviderCache.php +++ b/lib/Widget/Render/WidgetDataProviderCache.php @@ -115,6 +115,9 @@ public function decorateWithCache( // Get the cache $this->cache = $this->pool->getItem($this->key); + + // Invalidation method old means that if this cache key is being regenerated concurrently to this request + // we return the old data we have stored already. $this->cache->setInvalidationMethod(Invalidation::OLD); // Get the data (this might be OLD data) @@ -122,32 +125,44 @@ public function decorateWithCache( $cacheCreationDt = $this->cache->getCreation(); // Does the cache have data? + // we keep data 50% longer than we need to, so that it has a chance to be regenerated out of band if ($data === null) { $this->getLog()->debug('decorateWithCache: miss, no data'); $hasData = false; } else { - // Determine if the cache returned is a miss or older than the modified date - $this->isMissOrOld = $this->cache->isMiss() || ( - $dataModifiedDt !== null && $cacheCreationDt !== false && $dataModifiedDt->isAfter($cacheCreationDt) - ); - - $this->getLog()->debug('decorateWithCache: cache has data, is miss or old: ' - . var_export($this->isMissOrOld, true)); + $hasData = true; // Clear the data provider and add the cached items back to it. $dataProvider->clearData(); $dataProvider->clearMeta(); $dataProvider->addItems($data->data ?? []); + // Record any cached mediaIds + $this->cachedMediaIds = $data->media ?? []; + + // Update any meta foreach (($data->meta ?? []) as $key => $item) { $dataProvider->addOrUpdateMeta($key, $item); } - $dataProvider->addOrUpdateMeta('cacheDt', $this->cache->getCreation()->format('c')); - $dataProvider->addOrUpdateMeta('expireDt', $this->cache->getExpiration()->format('c')); - // Record any cached mediaIds - $this->cachedMediaIds = $data->media ?? []; - $hasData = true; + // Determine whether this cache is a miss (i.e. expired and being regenerated, expired, out of date) + // We use our own expireDt here because Stash will only return expired data with invalidation method OLD + // if the data is currently being regenerated and another process has called lock() on it + $expireDt = $dataProvider->getMeta()['expireDt'] ?? null; + if ($expireDt !== null) { + $expireDt = Carbon::createFromFormat('c', $expireDt); + } else { + $expireDt = $this->cache->getExpiration(); + } + + // Determine if the cache returned is a miss or older than the modified/expired dates + $this->isMissOrOld = $this->cache->isMiss() + || ($dataModifiedDt !== null && $cacheCreationDt !== false && $dataModifiedDt->isAfter($cacheCreationDt) + || ($expireDt->isBefore(Carbon::now())) + ); + + $this->getLog()->debug('decorateWithCache: cache has data, is miss or old: ' + . var_export($this->isMissOrOld, true)); } // If we do not have data/we're old/missed cache, and we have requested a lock, then we will be refreshing @@ -200,13 +215,14 @@ public function saveToCache(DataProviderInterface $dataProvider): void throw new GeneralException('No cache to save'); } - // Set our cache from the data provider. + // Set some cache dates so that we can track when this data provider was cached and when it should expire. $dataProvider->addOrUpdateMeta('cacheDt', Carbon::now()->format('c')); $dataProvider->addOrUpdateMeta( 'expireDt', Carbon::now()->addSeconds($dataProvider->getCacheTtl())->format('c') ); + // Set our cache from the data provider. $object = new \stdClass(); $object->data = $dataProvider->getData(); $object->meta = $dataProvider->getMeta(); @@ -216,7 +232,10 @@ public function saveToCache(DataProviderInterface $dataProvider): void if (!$cached) { throw new GeneralException('Cache failure'); } - $this->cache->expiresAfter($dataProvider->getCacheTtl()); + + // Keep the cache 50% longer than necessary + // The expireDt must always be 15 minutes to allow plenty of time for the WidgetSyncTask to regenerate. + $this->cache->expiresAfter(ceil(max($dataProvider->getCacheTtl() * 1.5, 900))); // Save to the pool $this->pool->save($this->cache); From df21d36774aee47487b882e219275b12cdd52e5e Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Thu, 20 Jun 2024 08:14:59 +0100 Subject: [PATCH 5/6] XMDS: deadlocks occurring when a lot of displays make concurrent connections (#2567) * XMDS: optimise the way notifications are inserted, refactor and document alertDisplayUp (no functionality changes). Only assess operating hours if we're likely to send an email. * DisplayGroup: refactor dynamic display group assessment to use the cache instead of the DB. * Tags: ensure we set the original value to any entity we decorate with tag links. --- lib/Controller/Display.php | 1 - lib/Controller/Tag.php | 6 +- lib/Entity/Display.php | 6 +- lib/Entity/Notification.php | 38 ++++--- lib/Event/TriggerTaskEvent.php | 44 +++----- lib/Factory/TagTrait.php | 16 ++- lib/Listener/DisplayGroupListener.php | 10 +- lib/Listener/TaskListener.php | 27 ++--- lib/Middleware/ListenersMiddleware.php | 3 +- lib/XTR/MaintenanceRegularTask.php | 14 ++- lib/Xmds/Soap.php | 150 +++++++++++++++---------- 11 files changed, 185 insertions(+), 130 deletions(-) diff --git a/lib/Controller/Display.php b/lib/Controller/Display.php index d447a7863e..3ba8d1ec52 100644 --- a/lib/Controller/Display.php +++ b/lib/Controller/Display.php @@ -1867,7 +1867,6 @@ function edit(Request $request, Response $response, $id) // Tags are stored on the displaygroup, we're just passing through here if ($this->getUser()->featureEnabled('tag.tagging')) { - $display->setOriginalValue('tags', $display->tags); if (is_array($sanitizedParams->getParam('tags'))) { $tags = $this->tagFactory->tagsFromJson($sanitizedParams->getArray('tags')); } else { diff --git a/lib/Controller/Tag.php b/lib/Controller/Tag.php index f4fb47a657..6332e27dd2 100644 --- a/lib/Controller/Tag.php +++ b/lib/Controller/Tag.php @@ -27,6 +27,7 @@ use Xibo\Event\DisplayGroupLoadEvent; use Xibo\Event\TagDeleteEvent; use Xibo\Event\TagEditEvent; +use Xibo\Event\TriggerTaskEvent; use Xibo\Factory\CampaignFactory; use Xibo\Factory\DisplayFactory; use Xibo\Factory\DisplayGroupFactory; @@ -743,7 +744,10 @@ public function editMultiple(Request $request, Response $response) // Once we're done, and if we're a Display entity, we need to calculate the dynamic display groups if ($targetType === 'display') { // Background update. - $this->getConfig()->changeSetting('DYNAMIC_DISPLAY_GROUP_ASSESS', 1); + $this->getDispatcher()->dispatch( + new TriggerTaskEvent('\Xibo\XTR\MaintenanceRegularTask', 'DYNAMIC_DISPLAY_GROUP_ASSESSED'), + TriggerTaskEvent::$NAME + ); } } else { $this->getLog()->debug('Tags were not changed'); diff --git a/lib/Entity/Display.php b/lib/Entity/Display.php index d4a09ebd2e..8c04c7abc0 100644 --- a/lib/Entity/Display.php +++ b/lib/Entity/Display.php @@ -25,6 +25,7 @@ use Respect\Validation\Validator as v; use Stash\Interfaces\PoolInterface; use Xibo\Event\DisplayGroupLoadEvent; +use Xibo\Event\TriggerTaskEvent; use Xibo\Factory\DisplayFactory; use Xibo\Factory\DisplayGroupFactory; use Xibo\Factory\DisplayProfileFactory; @@ -884,7 +885,10 @@ public function save($options = []) // Trigger an update of all dynamic DisplayGroups? if ($this->hasPropertyChanged('display') || $this->hasPropertyChanged('tags')) { // Background update. - $this->config->changeSetting('DYNAMIC_DISPLAY_GROUP_ASSESS', 1); + $this->getDispatcher()->dispatch( + new TriggerTaskEvent('\Xibo\XTR\MaintenanceRegularTask', 'DYNAMIC_DISPLAY_GROUP_ASSESSED'), + TriggerTaskEvent::$NAME + ); } } diff --git a/lib/Entity/Notification.php b/lib/Entity/Notification.php index 4dd3961848..b142e36eef 100644 --- a/lib/Entity/Notification.php +++ b/lib/Entity/Notification.php @@ -257,16 +257,19 @@ public function load($options = []) * Save Notification * @throws InvalidArgumentException */ - public function save() + public function save(): void { $this->validate(); - if ($this->notificationId == null) + $isNewRecord = false; + if ($this->notificationId == null) { + $isNewRecord = true; $this->add(); - else + } else { $this->edit(); + } - $this->manageAssignments(); + $this->manageAssignments($isNewRecord); } /** @@ -346,13 +349,20 @@ private function edit() /** * Manage assignements in DB */ - private function manageAssignments() + private function manageAssignments(bool $isNewRecord): void { $this->linkUserGroups(); - $this->unlinkUserGroups(); + + // Only unlink if we're not new (otherwise there is no point as we can't have any links yet) + if (!$isNewRecord) { + $this->unlinkUserGroups(); + } $this->linkDisplayGroups(); - $this->unlinkDisplayGroups(); + + if (!$isNewRecord) { + $this->unlinkDisplayGroups(); + } $this->manageRealisedUserLinks(); } @@ -360,10 +370,11 @@ private function manageAssignments() /** * Manage the links in the User notification table */ - private function manageRealisedUserLinks() + private function manageRealisedUserLinks(bool $isNewRecord = false): void { - // Delete links that no longer exist - $this->getStore()->update(' + if (!$isNewRecord) { + // Delete links that no longer exist + $this->getStore()->update(' DELETE FROM `lknotificationuser` WHERE `notificationId` = :notificationId AND `userId` NOT IN ( SELECT `userId` @@ -373,9 +384,10 @@ private function manageRealisedUserLinks() WHERE `lknotificationgroup`.notificationId = :notificationId2 ) AND userId <> 0 ', [ - 'notificationId' => $this->notificationId, - 'notificationId2' => $this->notificationId - ]); + 'notificationId' => $this->notificationId, + 'notificationId2' => $this->notificationId + ]); + } // Pop in new links following from this adjustment $this->getStore()->update(' diff --git a/lib/Event/TriggerTaskEvent.php b/lib/Event/TriggerTaskEvent.php index 444c499ff9..18d0965b32 100644 --- a/lib/Event/TriggerTaskEvent.php +++ b/lib/Event/TriggerTaskEvent.php @@ -22,33 +22,26 @@ namespace Xibo\Event; +/** + * An event which triggers the provided task to Run Now (at the next XTR poll) + * optionally clears a cache key to provide further instructions to the task that's running + */ class TriggerTaskEvent extends Event { - public static $NAME = 'trigger.task.event'; - /** - * @var string - */ - private $className; - /** - * @var string - */ - private $setting; - /** - * @var mixed|null - */ - private $settingValue; + public static string $NAME = 'trigger.task.event'; /** - * @param string $className + * @param string $className Class name of the task to be run + * @param string $key Cache Key to be dropped */ - public function __construct(string $className, string $setting = '', $value = null) - { - $this->className = $className; - $this->setting = $setting; - $this->settingValue = $value; + public function __construct( + private readonly string $className, + private readonly string $key = '' + ) { } /** + * Returns the class name for the task to be run * @return string */ public function getClassName(): string @@ -56,13 +49,12 @@ public function getClassName(): string return $this->className; } - public function getSetting(): string - { - return $this->setting; - } - - public function getSettingValue() + /** + * Returns the cache key to be dropped + * @return string + */ + public function getKey(): string { - return $this->settingValue; + return $this->key; } } diff --git a/lib/Factory/TagTrait.php b/lib/Factory/TagTrait.php index 6110886fdc..11a4aefd0c 100644 --- a/lib/Factory/TagTrait.php +++ b/lib/Factory/TagTrait.php @@ -58,13 +58,18 @@ public function loadTagsByEntityId(string $table, string $column, int $entityId) * @param string $table * @param string $column * @param array $entityIds - * @param array $entries + * @param \Xibo\Entity\EntityTrait[] $entries */ - public function decorateWithTagLinks(string $table, string $column, array $entityIds, array $entries) + public function decorateWithTagLinks(string $table, string $column, array $entityIds, array $entries): void { - $sql = 'SELECT tag.tagId, tag.tag, `'. $table .'`.value, `'.$table.'`.'.$column.' FROM `tag` INNER JOIN `'.$table.'` ON `'.$table.'`.tagId = tag.tagId WHERE `'.$table.'`.'.$column.' IN('. implode(',', $entityIds).')'; + // Query to get all tags from a tag link table for a set of entityIds + $sql = 'SELECT `tag`.`tagId`, `tag`.`tag`, `' . $table . '`.`value`, `' . $table . '`.`' . $column . '`' + . ' FROM `tag` ' + . ' INNER JOIN `' . $table . '` ON `' . $table . '`.`tagId` = `tag`.`tagId` ' + . ' WHERE `' . $table . '`.`' . $column . '` IN(' . implode(',', $entityIds) .')'; foreach ($this->getStore()->select($sql, []) as $row) { + // Add each tag returned above to its respective entity $sanitizedRow = $this->getSanitizer($row); $tagLink = new TagLink($this->getStore(), $this->getLog(), $this->getDispatcher()); @@ -78,6 +83,11 @@ public function decorateWithTagLinks(string $table, string $column, array $entit } } } + + // Set the original value on the entity. + foreach ($entries as $entry) { + $entry->setOriginalValue('tags', $entry->tags); + } } public function getTagUsageByEntity(string $tagLinkTable, string $idColumn, string $nameColumn, string $entity, int $tagId, &$entries) diff --git a/lib/Listener/DisplayGroupListener.php b/lib/Listener/DisplayGroupListener.php index 6e6af43c52..5eacb3d96f 100644 --- a/lib/Listener/DisplayGroupListener.php +++ b/lib/Listener/DisplayGroupListener.php @@ -197,12 +197,14 @@ public function onFolderMoving(FolderMovingEvent $event) * @param EventDispatcherInterface $dispatcher * @return void */ - public function onTagDelete(TagDeleteEvent $event, $eventName, EventDispatcherInterface $dispatcher) + public function onTagDelete(TagDeleteEvent $event, $eventName, EventDispatcherInterface $dispatcher): void { $displays = $this->storageService->select(' SELECT lktagdisplaygroup.displayGroupId - FROM `lktagdisplaygroup` INNER JOIN `displaygroup` - ON `lktagdisplaygroup`.displayGroupId = `displaygroup`.displayGroupId AND `displaygroup`.isDisplaySpecific = 1 + FROM `lktagdisplaygroup` + INNER JOIN `displaygroup` + ON `lktagdisplaygroup`.displayGroupId = `displaygroup`.displayGroupId + AND `displaygroup`.isDisplaySpecific = 1 WHERE `lktagdisplaygroup`.tagId = :tagId', [ 'tagId' => $event->getTagId() ]); @@ -214,7 +216,7 @@ public function onTagDelete(TagDeleteEvent $event, $eventName, EventDispatcherIn if (count($displays) > 0) { $dispatcher->dispatch( - new TriggerTaskEvent('\Xibo\XTR\MaintenanceRegularTask', 'DYNAMIC_DISPLAY_GROUP_ASSESS', 1), + new TriggerTaskEvent('\Xibo\XTR\MaintenanceRegularTask', 'DYNAMIC_DISPLAY_GROUP_ASSESSED'), TriggerTaskEvent::$NAME ); } diff --git a/lib/Listener/TaskListener.php b/lib/Listener/TaskListener.php index 5e901dcf85..b4eefadda0 100644 --- a/lib/Listener/TaskListener.php +++ b/lib/Listener/TaskListener.php @@ -22,33 +22,24 @@ namespace Xibo\Listener; +use Stash\Interfaces\PoolInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Xibo\Event\TriggerTaskEvent; use Xibo\Factory\TaskFactory; use Xibo\Service\ConfigServiceInterface; /** - * Task events + * A listener for events related to tasks */ class TaskListener { use ListenerLoggerTrait; - /** - * @var TaskFactory - */ - private $taskFactory; - /** - * @var ConfigServiceInterface - */ - private $configService; - public function __construct( - TaskFactory $taskFactory, - ConfigServiceInterface $configService + private readonly TaskFactory $taskFactory, + private readonly ConfigServiceInterface $configService, + private readonly PoolInterface $pool ) { - $this->taskFactory = $taskFactory; - $this->configService = $configService; } /** @@ -68,12 +59,14 @@ public function registerWithDispatcher(EventDispatcherInterface $dispatcher) : T * @throws \Xibo\Support\Exception\InvalidArgumentException * @throws \Xibo\Support\Exception\NotFoundException */ - public function onTriggerTask(TriggerTaskEvent $event) + public function onTriggerTask(TriggerTaskEvent $event): void { - if (!empty($event->getSetting()) && !empty($event->getSettingValue())) { - $this->configService->changeSetting($event->getSetting(), $event->getSettingValue()); + if (!empty($event->getKey())) { + // Drop this setting from the cache + $this->pool->deleteItem($event->getKey()); } + // Mark the task to run now $task = $this->taskFactory->getByClass($event->getClassName()); $task->runNow = 1; $task->save(); diff --git a/lib/Middleware/ListenersMiddleware.php b/lib/Middleware/ListenersMiddleware.php index a33ac95d8a..8aaf677d5a 100644 --- a/lib/Middleware/ListenersMiddleware.php +++ b/lib/Middleware/ListenersMiddleware.php @@ -146,7 +146,8 @@ public static function setListeners(App $app) // Listen for event that affect Task (new TaskListener( $c->get('taskFactory'), - $c->get('configService') + $c->get('configService'), + $c->get('pool') )) ->useLogger($c->get('logger')) ->registerWithDispatcher($dispatcher); diff --git a/lib/XTR/MaintenanceRegularTask.php b/lib/XTR/MaintenanceRegularTask.php index 66a19f5660..511e5e1823 100644 --- a/lib/XTR/MaintenanceRegularTask.php +++ b/lib/XTR/MaintenanceRegularTask.php @@ -493,15 +493,23 @@ private function publishLayouts() /** * Assess any eligible dynamic display groups if necessary * @return void + * @throws \Xibo\Support\Exception\NotFoundException */ - private function assessDynamicDisplayGroups() + private function assessDynamicDisplayGroups(): void { $this->runMessage .= '## ' . __('Assess Dynamic Display Groups') . PHP_EOL; - if ($this->config->getSetting('DYNAMIC_DISPLAY_GROUP_ASSESS', 0) == 1) { + // 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); - $this->config->changeSetting('DYNAMIC_DISPLAY_GROUP_ASSESS', 0); + // 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) { diff --git a/lib/Xmds/Soap.php b/lib/Xmds/Soap.php index 489e4c1cfa..410d7c8711 100644 --- a/lib/Xmds/Soap.php +++ b/lib/Xmds/Soap.php @@ -2551,94 +2551,124 @@ protected function authDisplay($hardwareKey) /** * Alert Display Up - * @throws \phpmailerException + * assesses whether a notification is required to be sent for this display, and only does something if the + * display is currently marked as offline (i.e. it is coming back online again) + * this is only called in Register * @throws NotFoundException */ - protected function alertDisplayUp() + protected function alertDisplayUp(): void { $maintenanceEnabled = $this->getConfig()->getSetting('MAINTENANCE_ENABLED'); if ($this->display->loggedIn == 0) { - $this->getLog()->info(sprintf('Display %s was down, now its up.', $this->display->display)); // Log display up $this->displayEventFactory->createEmpty()->displayUp($this->display->displayId); - $dayPartId = $this->display->getSetting('dayPartId', null,['displayOverride' => true]); - - $operatingHours = true; - - if ($dayPartId !== null) { - try { - $dayPart = $this->dayPartFactory->getById($dayPartId); - - $startTimeArray = explode(':', $dayPart->startTime); - $startTime = Carbon::now()->setTime(intval($startTimeArray[0]), intval($startTimeArray[1])); - - $endTimeArray = explode(':', $dayPart->endTime); - $endTime = Carbon::now()->setTime(intval($endTimeArray[0]), intval($endTimeArray[1])); - - $now = Carbon::now(); - - // exceptions - foreach ($dayPart->exceptions as $exception) { + // Do we need to email? + if ($this->display->emailAlert == 1 + && ($maintenanceEnabled == 'On' || $maintenanceEnabled == 'Protected') + && $this->getConfig()->getSetting('MAINTENANCE_EMAIL_ALERTS') == 1 + ) { + // Only send alerts during operating hours. + if ($this->isInsideOperatingHours()) { + $subject = sprintf(__('Recovery for Display %s'), $this->display->display); + $body = sprintf( + __('Display ID %d is now back online %s'), + $this->display->displayId, + Carbon::now()->format(DateFormatHelper::getSystemFormat()) + ); - // check if we are on exception day and if so override the startTime and endTime accordingly - if ($exception['day'] == Carbon::now()->format('D')) { - $exceptionsStartTime = explode(':', $exception['start']); - $startTime = Carbon::now()->setTime(intval($exceptionsStartTime[0]), intval($exceptionsStartTime[1])); + // Create a notification assigned to system-wide user groups + try { + $notification = $this->notificationFactory->createSystemNotification( + $subject, + $body, + Carbon::now() + ); - $exceptionsEndTime = explode(':', $exception['end']); - $endTime = Carbon::now()->setTime(intval($exceptionsEndTime[0]), intval($exceptionsEndTime[1])); + // Get groups which have been configured to receive notifications + foreach ($this->userGroupFactory + ->getDisplayNotificationGroups($this->display->displayGroupId) as $group) { + $notification->assignUserGroup($group); } - } - // check if we are inside the operating hours for this display - we use that flag to decide if we need to create a notification and send an email. - if (($now >= $startTime && $now <= $endTime)) { - $operatingHours = true; - } else { - $operatingHours = false; + // Save the notification and insert the links, etc. + $notification->save(); + } catch (\Exception) { + $this->getLog()->error(sprintf( + 'Unable to send email alert for display %s with subject %s and body %s', + $this->display->display, + $subject, + $body + )); } - - } catch (NotFoundException $e) { - $this->getLog()->debug('Unknown dayPartId set on Display Profile for displayId ' . $this->display->displayId); + } else { + $this->getLog()->info('Not sending recovery email for Display - ' + . $this->display->display . ' we are outside of its operating hours'); } + } else { + $this->getLog()->debug(sprintf( + 'No email required. Email Alert: %d, Enabled: %s, Email Enabled: %s.', + $this->display->emailAlert, + $maintenanceEnabled, + $this->getConfig()->getSetting('MAINTENANCE_EMAIL_ALERTS') + )); } + } + } - // Do we need to email? - if ($this->display->emailAlert == 1 && ($maintenanceEnabled == 'On' || $maintenanceEnabled == 'Protected') - && $this->getConfig()->getSetting('MAINTENANCE_EMAIL_ALERTS') == 1) { + /** + * Is the display currently inside operating hours? + * @return bool + * @throws \Xibo\Support\Exception\NotFoundException + */ + private function isInsideOperatingHours(): bool + { + $dayPartId = $this->display->getSetting('dayPartId', null, ['displayOverride' => true]); + if ($dayPartId !== null) { + try { + $dayPart = $this->dayPartFactory->getById($dayPartId); - // for displays without dayPartId set, this is always true, otherwise we check if we are inside the operating hours set for this display - if ($operatingHours) { - $subject = sprintf(__("Recovery for Display %s"), $this->display->display); - $body = sprintf(__("Display ID %d is now back online %s"), $this->display->displayId, - Carbon::now()->format(DateFormatHelper::getSystemFormat())); + $startTimeArray = explode(':', $dayPart->startTime); + $startTime = Carbon::now()->setTime(intval($startTimeArray[0]), intval($startTimeArray[1])); - // Create a notification assigned to system wide user groups - try { - $notification = $this->notificationFactory->createSystemNotification($subject, $body, - Carbon::now()); + $endTimeArray = explode(':', $dayPart->endTime); + $endTime = Carbon::now()->setTime(intval($endTimeArray[0]), intval($endTimeArray[1])); - // Add in any displayNotificationGroups, with permissions - foreach ($this->userGroupFactory->getDisplayNotificationGroups($this->display->displayGroupId) as $group) { - $notification->assignUserGroup($group); - } + $now = Carbon::now(); - $notification->save(); + // handle exceptions + foreach ($dayPart->exceptions as $exception) { + // check if we are on exception day and if so override the startTime and endTime accordingly + if ($exception['day'] == Carbon::now()->format('D')) { + // Parse the start/end times into the current day. + $exceptionsStartTime = explode(':', $exception['start']); + $startTime = Carbon::now()->setTime( + intval($exceptionsStartTime[0]), + intval($exceptionsStartTime[1]) + ); - } catch (\Exception $e) { - $this->getLog()->error(sprintf('Unable to send email alert for display %s with subject %s and body %s', - $this->display->display, $subject, $body)); + $exceptionsEndTime = explode(':', $exception['end']); + $endTime = Carbon::now()->setTime( + intval($exceptionsEndTime[0]), + intval($exceptionsEndTime[1]) + ); } - } else { - $this->getLog()->info('Not sending recovery email for Display - ' . $this->display->display . ' we are outside of its operating hours'); } - } else { - $this->getLog()->debug(sprintf('No email required. Email Alert: %d, Enabled: %s, Email Enabled: %s.', $this->display->emailAlert, $maintenanceEnabled, $this->getConfig()->getSetting('MAINTENANCE_EMAIL_ALERTS'))); + + // check if we are inside the operating hours for this display - we use that flag to decide + // if we need to create a notification and send an email. + if (($now >= $startTime && $now <= $endTime)) { + return true; + } + } catch (NotFoundException) { + $this->getLog()->debug('Unknown dayPartId set on Display Profile for displayId ' + . $this->display->displayId); } } + return false; } /** From 5bd5e2f47ebb7b1badfb863bf72a46dae2690340 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Thu, 20 Jun 2024 08:35:50 +0100 Subject: [PATCH 6/6] Release: bump version to 4.1.0-alpha2, update swagger. --- lib/Helper/Environment.php | 2 +- web/swagger.json | 407 ++++++++++++++++++++++++++++++++++--- 2 files changed, 384 insertions(+), 25 deletions(-) diff --git a/lib/Helper/Environment.php b/lib/Helper/Environment.php index 21d6aca093..436b8a7547 100644 --- a/lib/Helper/Environment.php +++ b/lib/Helper/Environment.php @@ -30,7 +30,7 @@ */ class Environment { - public static $WEBSITE_VERSION_NAME = '4.1.0-alpha'; + public static $WEBSITE_VERSION_NAME = '4.1.0-alpha2'; public static $XMDS_VERSION = '7'; public static $XLF_VERSION = 4; public static $VERSION_REQUIRED = '8.1.0'; diff --git a/web/swagger.json b/web/swagger.json index feb6431536..f04aa7eaf4 100755 --- a/web/swagger.json +++ b/web/swagger.json @@ -804,6 +804,13 @@ "description": "An array of Player types this Command is available on, empty for all.", "required": false, "type": "string" + }, + { + "name": "createAlertOn", + "in": "formData", + "description": "On command execution, when should a Display alert be created?\r\n * success, failure, always or never", + "required": false, + "type": "string" } ], "responses": { @@ -872,6 +879,13 @@ "description": "An array of Player types this Command is available on, empty for all.", "required": false, "type": "string" + }, + { + "name": "createAlertOn", + "in": "formData", + "description": "On command execution, when should a Display alert be created?\r\n * success, failure, always or never", + "required": false, + "type": "string" } ], "responses": { @@ -936,6 +950,13 @@ "required": false, "type": "string" }, + { + "name": "isRealTime", + "in": "query", + "description": "Filter by real time", + "required": false, + "type": "integer" + }, { "name": "userId", "in": "query", @@ -1006,6 +1027,13 @@ "required": true, "type": "integer" }, + { + "name": "isRealTime", + "in": "formData", + "description": "Is this a real time DataSet?", + "required": true, + "type": "integer" + }, { "name": "method", "in": "formData", @@ -1146,6 +1174,13 @@ "required": false, "type": "string" }, + { + "name": "dataConnectorScript", + "in": "formData", + "description": "If isRealTime then provide a script to connect to the data source", + "required": false, + "type": "string" + }, { "name": "folderId", "in": "formData", @@ -1214,6 +1249,13 @@ "required": true, "type": "integer" }, + { + "name": "isRealTime", + "in": "formData", + "description": "Is this a real time DataSet?", + "required": true, + "type": "integer" + }, { "name": "method", "in": "formData", @@ -1354,6 +1396,13 @@ "required": false, "type": "string" }, + { + "name": "dataConnectorScript", + "in": "formData", + "description": "If isRealTime then provide a script to connect to the data source", + "required": false, + "type": "string" + }, { "name": "folderId", "in": "formData", @@ -1394,6 +1443,40 @@ } } }, + "/dataset/dataConnector/{dataSetId}": { + "put": { + "tags": [ + "dataset" + ], + "summary": "Edit DataSet Data Connector", + "description": "Edit a DataSet Data Connector", + "operationId": "dataSetDataConnectorEdit", + "parameters": [ + { + "name": "dataSetId", + "in": "path", + "description": "The DataSet ID", + "required": true, + "type": "integer" + }, + { + "name": "dataConnectorScript", + "in": "formData", + "description": "If isRealTime then provide a script to connect to the data source", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/DataSet" + } + } + } + } + }, "/dataset/copy/{dataSetId}": { "post": { "tags": [ @@ -6960,6 +7043,13 @@ "description": "DataType to return templates for", "required": true, "type": "string" + }, + { + "name": "type", + "in": "query", + "description": "Type to return templates for", + "required": false, + "type": "string" } ], "responses": { @@ -7085,13 +7175,6 @@ "required": false, "type": "string" }, - { - "name": "isEmail", - "in": "formData", - "description": "Flag indicating whether this notification should be emailed.", - "required": true, - "type": "integer" - }, { "name": "isInterrupt", "in": "formData", @@ -7173,13 +7256,6 @@ "required": true, "type": "string" }, - { - "name": "isEmail", - "in": "formData", - "description": "Flag indicating whether this notification should be emailed.", - "required": true, - "type": "integer" - }, { "name": "isInterrupt", "in": "formData", @@ -7922,6 +7998,37 @@ } } }, + "/playlist/{id}/convert": { + "post": { + "tags": [ + "playlist" + ], + "summary": "Playlist Convert", + "description": "Create a global playlist from inline editor Playlist.\r\n * Assign created Playlist via sub-playlist Widget to region Playlist.", + "operationId": "convert", + "parameters": [ + { + "name": "playlistId", + "in": "path", + "description": "The Playlist ID", + "required": true, + "type": "integer" + }, + { + "name": "name", + "in": "formData", + "description": "Optional name for the global Playlist.", + "required": false, + "type": "string" + } + ], + "responses": { + "201": { + "description": "successful operation" + } + } + } + }, "/region/{id}": { "put": { "tags": [ @@ -8726,6 +8833,20 @@ "description": "For Action eventTypeId and navLayout actionType, the Layout Code identifier", "required": false, "type": "string" + }, + { + "name": "dataSetId", + "in": "formData", + "description": "For Data Connector eventTypeId, the DataSet ID", + "required": false, + "type": "integer" + }, + { + "name": "dataSetParams", + "in": "formData", + "description": "For Data Connector eventTypeId, the DataSet params", + "required": false, + "type": "string" } ], "responses": { @@ -8955,6 +9076,20 @@ "description": "For Action eventTypeId and navLayout actionType, the Layout Code identifier", "required": false, "type": "string" + }, + { + "name": "dataSetId", + "in": "formData", + "description": "For Data Connector eventTypeId, the DataSet ID", + "required": false, + "type": "integer" + }, + { + "name": "dataSetParams", + "in": "formData", + "description": "For Data Connector eventTypeId, the DataSet params", + "required": false, + "type": "string" } ], "responses": { @@ -9400,6 +9535,20 @@ "required": false, "type": "integer" }, + { + "name": "syncSwitchDelay", + "in": "formData", + "description": "The delay (in ms) when displaying the changes in content - default 750", + "required": false, + "type": "integer" + }, + { + "name": "syncVideoPauseDelay", + "in": "formData", + "description": "The delay (in ms) before unpausing the video on start - default 100", + "required": false, + "type": "integer" + }, { "name": "leadDisplayId", "in": "formData", @@ -10584,7 +10733,49 @@ { "name": "isDisplayNotification", "in": "formData", - "description": "Flag (0, 1), should members of this Group receive Display notifications for Displays they have permissions to see", + "description": "Flag (0, 1), should members of this Group receive Display notifications\r\n * for Displays they have permissions to see", + "required": false, + "type": "integer" + }, + { + "name": "isDataSetNotification", + "in": "formData", + "description": "Flag (0, 1), should members of this Group receive DataSet notification emails?", + "required": false, + "type": "integer" + }, + { + "name": "isLayoutNotification", + "in": "formData", + "description": "Flag (0, 1), should members of this Group receive Layout notification emails?", + "required": false, + "type": "integer" + }, + { + "name": "isLibraryNotification", + "in": "formData", + "description": "Flag (0, 1), should members of this Group receive Library notification emails?", + "required": false, + "type": "integer" + }, + { + "name": "isReportNotification", + "in": "formData", + "description": "Flag (0, 1), should members of this Group receive Report notification emails?", + "required": false, + "type": "integer" + }, + { + "name": "isScheduleNotification", + "in": "formData", + "description": "Flag (0, 1), should members of this Group receive Schedule notification emails?", + "required": false, + "type": "integer" + }, + { + "name": "isCustomNotification", + "in": "formData", + "description": "Flag (0, 1), should members of this Group receive Custom notification emails?", "required": false, "type": "integer" }, @@ -10663,7 +10854,49 @@ { "name": "isDisplayNotification", "in": "formData", - "description": "Flag (0, 1), should members of this Group receive Display notifications for Displays they have permissions to see", + "description": "Flag (0, 1), should members of this Group receive Display notifications\r\n * for Displays they have permissions to see", + "required": false, + "type": "integer" + }, + { + "name": "isDataSetNotification", + "in": "formData", + "description": "Flag (0, 1), should members of this Group receive DataSet notification emails?", + "required": false, + "type": "integer" + }, + { + "name": "isLayoutNotification", + "in": "formData", + "description": "Flag (0, 1), should members of this Group receive Layout notification emails?", + "required": false, + "type": "integer" + }, + { + "name": "isLibraryNotification", + "in": "formData", + "description": "Flag (0, 1), should members of this Group receive Library notification emails?", + "required": false, + "type": "integer" + }, + { + "name": "isReportNotification", + "in": "formData", + "description": "Flag (0, 1), should members of this Group receive Report notification emails?", + "required": false, + "type": "integer" + }, + { + "name": "isScheduleNotification", + "in": "formData", + "description": "Flag (0, 1), should members of this Group receive Schedule notification emails?", + "required": false, + "type": "integer" + }, + { + "name": "isCustomNotification", + "in": "formData", + "description": "Flag (0, 1), should members of this Group receive Custom notification emails?", "required": false, "type": "integer" }, @@ -11535,6 +11768,10 @@ "ipAddress": { "description": "The IP Address of the User that took this action", "type": "string" + }, + "sessionHistoryId": { + "description": "Session history id.", + "type": "integer" } } }, @@ -11697,6 +11934,13 @@ "description": "A comma separated list of player types this command is available on", "type": "string" }, + "createAlertOn": { + "description": "Define if execution of this command should create an alert on success, failure, always or never.", + "type": "string" + }, + "createAlertOnDisplayProfile": { + "description": "Create Alert On specific to the provided DisplayProfile." + }, "groupsWithPermissions": { "description": "A comma separated list of groups/users with permissions to this Command", "type": "string" @@ -11746,6 +11990,10 @@ "description": "Flag to indicate whether this DataSet is Remote", "type": "integer" }, + "isRealTime": { + "description": "Flag to indicate whether this DataSet is Real time", + "type": "integer" + }, "method": { "description": "Method to fetch the Data, can be GET or POST", "type": "string" @@ -12267,6 +12515,26 @@ "syncGroupId": { "description": "The Display Group ID this Display is synced to", "type": "integer" + }, + "osVersion": { + "description": "The OS version of the Display", + "type": "string" + }, + "osSdk": { + "description": "The SDK version of the Display", + "type": "string" + }, + "manufacturer": { + "description": "The manufacturer of the Display", + "type": "string" + }, + "brand": { + "description": "The brand of the Display", + "type": "string" + }, + "model": { + "description": "The model of the Display", + "type": "string" } } }, @@ -12688,6 +12956,14 @@ "display": { "description": "The display this message relates to or CMS for CMS.", "type": "string" + }, + "sessionHistoryId": { + "description": "Session history id.", + "type": "integer" + }, + "userId": { + "description": "User id.", + "type": "integer" } } }, @@ -13171,6 +13447,10 @@ "description": "Is Visible?", "type": "boolean" }, + "isEnabled": { + "description": "Is Enabled?", + "type": "boolean" + }, "propertyGroups": { "description": "An array of additional module specific group properties", "type": "array", @@ -13186,6 +13466,10 @@ "description": "", "type": "array", "items": {} + }, + "groupsWithPermissions": { + "description": "A comma separated list of groups/users with permissions to this template", + "type": "string" } } }, @@ -13207,14 +13491,14 @@ "description": "The subject line", "type": "string" }, + "type": { + "description": "The Notification type", + "type": "string" + }, "body": { "description": "The HTML body of the notification", "type": "string" }, - "isEmail": { - "description": "Should the notification be emailed", - "type": "integer" - }, "isInterrupt": { "description": "Should the notification interrupt the CMS UI on navigate/login", "type": "integer" @@ -13742,6 +14026,13 @@ "$ref": "#/definitions/ScheduleReminder" } }, + "criteria": { + "description": "Schedule Criteria assigned to this Scheduled Event.", + "type": "array", + "items": { + "$ref": "#/definitions/ScheduleCriteria" + } + }, "userId": { "description": "The userId that owns this event.", "type": "integer" @@ -13857,6 +14148,14 @@ "description": "For sync events, the id the the sync group", "type": "integer" }, + "dataSetId": { + "description": "For data connector events, the dataSetId", + "type": "integer" + }, + "dataSetParams": { + "description": "For data connector events, the data set parameters", + "type": "integer" + }, "modifiedBy": { "description": "The userId of the user that last modified this Schedule", "type": "integer" @@ -13875,6 +14174,7 @@ } } }, + "ScheduleCriteria": {}, "ScheduleExclusion": { "properties": { "scheduleExclusionId": { @@ -13975,7 +14275,7 @@ "type": "integer" }, "syncVideoPauseDelay": { - "description": "The delay (in ms) before unpausing the video on start", + "description": "The delay (in ms) before unpausing the video on start.", "type": "integer" }, "leadDisplayId": { @@ -14214,11 +14514,35 @@ } }, "isSystemNotification": { - "description": "Does this Group receive system notifications.", + "description": "Does this User receive system notifications.", "type": "integer" }, "isDisplayNotification": { - "description": "Does this Group receive system notifications.", + "description": "Does this User receive system notifications.", + "type": "integer" + }, + "isDataSetNotification": { + "description": "Does this User receive DataSet notifications.", + "type": "integer" + }, + "isLayoutNotification": { + "description": "Does this User receive Layout notifications.", + "type": "integer" + }, + "isLibraryNotification": { + "description": "Does this User receive Library notifications.", + "type": "integer" + }, + "isReportNotification": { + "description": "Does this User receive Report notifications.", + "type": "integer" + }, + "isScheduleNotification": { + "description": "Does this User receive Schedule notifications.", + "type": "integer" + }, + "isCustomNotification": { + "description": "Does this User receive Custom notifications.", "type": "integer" }, "twoFactorTypeId": { @@ -14272,6 +14596,30 @@ "description": "Does this Group receive display notifications.", "type": "integer" }, + "isDataSetNotification": { + "description": "Does this Group receive DataSet notifications.", + "type": "integer" + }, + "isLayoutNotification": { + "description": "Does this Group receive Layout notifications.", + "type": "integer" + }, + "isLibraryNotification": { + "description": "Does this Group receive Library notifications.", + "type": "integer" + }, + "isReportNotification": { + "description": "Does this Group receive Report notifications.", + "type": "integer" + }, + "isScheduleNotification": { + "description": "Does this Group receive Schedule notifications.", + "type": "integer" + }, + "isCustomNotification": { + "description": "Does this Group receive Custom notifications.", + "type": "integer" + }, "isShownForAddUser": { "description": "Is this Group shown in the list of choices when onboarding a new user", "type": "integer" @@ -14494,6 +14842,7 @@ } }, "Element": {}, + "ElementGroup": {}, "Extend": {}, "LegacyType": {}, "Option": { @@ -14545,7 +14894,17 @@ }, "PropertyGroup": {}, "Rule": {}, - "Stencil": {}, + "Stencil": { + "properties": { + "elementGroups": { + "description": "An array of element groups", + "type": "array", + "items": { + "$ref": "#/definitions/ElementGroup" + } + } + } + }, "Test": {} }, "parameters": {