From 6b8c87a1d8818f4d322ad7fef4f641e7f68a498e Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Fri, 14 Jun 2024 09:35:11 +0100 Subject: [PATCH 01/22] 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 02/22] 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 03/22] 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 04/22] 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 05/22] 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 7f42e3f6f69714c7dd2c42e4afed82c1e25143f0 Mon Sep 17 00:00:00 2001 From: Israt Jahan Farzana Date: Wed, 26 Jun 2024 15:12:13 +0100 Subject: [PATCH 06/22] securing github workflow input https://github.com/xibosignageltd/xibo-private/issues/758 (#2587) --- .github/workflows/test-suite.yaml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-suite.yaml b/.github/workflows/test-suite.yaml index 00e71c7597..71a2f66d5e 100644 --- a/.github/workflows/test-suite.yaml +++ b/.github/workflows/test-suite.yaml @@ -32,11 +32,13 @@ jobs: # Step 4: The Rest - name: Check Comment id: check_comment + env: + COMMENT_BODY: ${{ github.event.comment.body }} run: | - comment_body="${{ github.event.comment.body }}" - if [[ "$comment_body" =~ TEST:\ (all|\*) ]]; then + # Sanitize and validate comment body for TEST command + if [[ "$COMMENT_BODY" =~ ^TEST:\ (all|\*)$ ]]; then test_all=true - elif [[ "$comment_body" =~ TEST:(.*?)END ]]; then + elif [[ "$COMMENT_BODY" =~ ^TEST:(.*?)END$ ]]; then specs="${BASH_REMATCH[1]}" # Split the content by commas and remove spaces IFS=',' read -ra content_array <<< "$specs" @@ -46,7 +48,7 @@ jobs: content_array=("${content_array[@]%/}") # Add "/app/cypress/e2e/" prefix to each element for ((i=0; i<${#content_array[@]}; i++)); do - content_array[$i]="/app/cypress/e2e/${content_array[$i]}" + content_array[$i]="/app/cypress/e2e/${content_array[$i]}" done # Join the content array elements with commas content_no_spaces="$(IFS=','; echo "${content_array[*]}")" From 2a569ed46171c94a657b3f37f695579f18c72a67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauro=20Ferr=C3=A3o?= Date: Fri, 28 Jun 2024 09:02:08 +0100 Subject: [PATCH 07/22] /schedule/view shows no events when mobile layout is used. (#2584) relates to xibosignage/xibo#3442 --- ui/src/core/xibo-cms.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/src/core/xibo-cms.js b/ui/src/core/xibo-cms.js index acd5b91421..35ad7e6ff6 100644 --- a/ui/src/core/xibo-cms.js +++ b/ui/src/core/xibo-cms.js @@ -3484,6 +3484,7 @@ function initDatePicker($element, baseFormat, displayFormat, options, onChangeCa allowInput: false, defaultDate: ((initialValue != undefined) ? initialValue : null), altInputClass: 'datePickerHelper ' + $element.attr('class'), + disableMobile: true, altFormat: displayFormat, dateFormat: baseFormat, locale: (language != 'en-GB') ? language : 'default', From 219b5e4240843f0ecda988d790592159f020101c Mon Sep 17 00:00:00 2001 From: Mae Grace Baybay <87640602+mgbaybay@users.noreply.github.com> Date: Tue, 2 Jul 2024 16:45:55 +0800 Subject: [PATCH 08/22] Bandwidth Report: Fixed Table/Chart is not showing any data (#2596) --- lib/Report/Bandwidth.php | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/Report/Bandwidth.php b/lib/Report/Bandwidth.php index 5d106a6de2..3fb2bc333f 100644 --- a/lib/Report/Bandwidth.php +++ b/lib/Report/Bandwidth.php @@ -1,4 +1,24 @@ . + */ namespace Xibo\Report; @@ -256,7 +276,8 @@ public function getResults(SanitizerInterface $sanitizedParams) } // Decide what our units are going to be, based on the size - $base = floor(log($maxSize) / log(1024)); + // We need to put a fallback value in case it returns an infinite value + $base = !is_infinite(floor(log($maxSize) / log(1024))) ? floor(log($maxSize) / log(1024)) : 0; $labels = []; $data = []; From f8c64ac88a93e2a577909cb601b50f9fb67ce1a6 Mon Sep 17 00:00:00 2001 From: Mae Grace Baybay <87640602+mgbaybay@users.noreply.github.com> Date: Tue, 2 Jul 2024 17:25:02 +0800 Subject: [PATCH 09/22] Connector: Fixed file based settings should override database settings (#2597) --- lib/Factory/ConnectorFactory.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Factory/ConnectorFactory.php b/lib/Factory/ConnectorFactory.php index f7c5b9932e..e00d2a0874 100644 --- a/lib/Factory/ConnectorFactory.php +++ b/lib/Factory/ConnectorFactory.php @@ -1,8 +1,8 @@ setFactories($this->container) ->useLogger($this->getLog()->getLoggerInterface()) - ->useSettings($this->config->getConnectorSettings($out->getSourceName()), true) ->useSettings($connector->settings) + ->useSettings($this->config->getConnectorSettings($out->getSourceName()), true) ->useHttpOptions($this->config->getGuzzleProxy()) ->useJwtService($this->jwtService) ->usePool($this->pool); From 12a8152ce55db2a4f0a7e50ca4d38ec394b1f517 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Tue, 2 Jul 2024 17:13:32 +0100 Subject: [PATCH 10/22] Dashboard: remove unused code to improve page load performance. (#2571) fixes xibosignage/xibo#3440 --- lib/Controller/StatusDashboard.php | 14 +--------- lib/Factory/UserFactory.php | 44 +++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/lib/Controller/StatusDashboard.php b/lib/Controller/StatusDashboard.php index 45b2689adc..3a6f7c5a03 100644 --- a/lib/Controller/StatusDashboard.php +++ b/lib/Controller/StatusDashboard.php @@ -310,16 +310,9 @@ public function displayPage(Request $request, Response $response) $data['libraryWidgetData'] = json_encode($libraryUsage); // Get a count of users - $data['countUsers'] = count($this->userFactory->query()); + $data['countUsers'] = $this->userFactory->count(); // Get a count of active layouts, only for display groups we have permission for - $displayGroups = $this->displayGroupFactory->query(null, ['isDisplaySpecific' => -1]); - $displayGroupIds = array_map(function ($element) { - return $element->displayGroupId; - }, $displayGroups); - // Add an empty one - $displayGroupIds[] = -1; - $params = ['now' => Carbon::now()->format('U')]; $sql = ' @@ -422,9 +415,7 @@ public function displayPage(Request $request, Response $response) // Display Status and Media Inventory data - Level one $displays = $this->displayFactory->query(); - $displayIds = []; $displayLoggedIn = []; - $displayNames = []; $displayMediaStatus = []; $displaysOnline = 0; $displaysOffline = 0; @@ -432,8 +423,6 @@ public function displayPage(Request $request, Response $response) $displaysMediaNotUpToDate = 0; foreach ($displays as $display) { - $displayIds[] = $display->displayId; - $displayNames[] = $display->display; $displayLoggedIn[] = $display->loggedIn; $displayMediaStatus[] = $display->mediaInventoryStatus; } @@ -456,7 +445,6 @@ public function displayPage(Request $request, Response $response) $data['displayStatus'] = json_encode([$displaysOnline, $displaysOffline]); $data['displayMediaStatus'] = json_encode([$displaysMediaUpToDate, $displaysMediaNotUpToDate]); - $data['displayLabels'] = json_encode($displayNames); } catch (Exception $e) { $this->getLog()->error($e->getMessage()); $this->getLog()->debug($e->getTraceAsString()); diff --git a/lib/Factory/UserFactory.php b/lib/Factory/UserFactory.php index 89c1c49915..83da9d197d 100644 --- a/lib/Factory/UserFactory.php +++ b/lib/Factory/UserFactory.php @@ -1,6 +1,6 @@ getUser()->isSuperAdmin()) { + // Non-super admins should only get a count of users in their group + $sql .= ' + WHERE `user`.userId IN ( + SELECT `otherUserLinks`.userId + FROM `lkusergroup` + INNER JOIN `group` + ON `group`.groupId = `lkusergroup`.groupId + AND `group`.isUserSpecific = 0 + INNER JOIN `lkusergroup` `otherUserLinks` + ON `otherUserLinks`.groupId = `group`.groupId + WHERE `lkusergroup`.userId = :currentUserId + ) + '; + $params['currentUserId'] = $this->getUser()->userId; + } + + // Run the query + $results = $this->getStore()->select($sql, $params); + return intval($results[0]['countOf'] ?? 0); + } } \ No newline at end of file From 3389483a33efe6400ef07b1b4bf68ff799c13b78 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Wed, 3 Jul 2024 14:37:50 +0100 Subject: [PATCH 11/22] Data Widget: increasing/decreasing the number of slots does not update the duration when durationPerItem is checked (#2599) * Widget: saving elements doesn't recalculate widget duration. * Refresh Layout duration on widget elements save relates to xibosignage/xibo#3445 --------- Co-authored-by: maurofmferrao --- lib/Controller/Widget.php | 11 +++++++++++ lib/Widget/DataSetProvider.php | 9 +++++++-- ui/src/editor-core/widget.js | 10 ++++------ ui/src/layout-editor/main.js | 1 + 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/lib/Controller/Widget.php b/lib/Controller/Widget.php index 3d1a5224e3..bd2afe7904 100644 --- a/lib/Controller/Widget.php +++ b/lib/Controller/Widget.php @@ -1692,7 +1692,18 @@ public function saveElements(Request $request, Response $response, $id) } if ($uniqueSlots > 0) { + $currentItemsPerPage = $widget->getOptionValue('itemsPerPage', null); + $widget->setOptionValue('itemsPerPage', 'attrib', $uniqueSlots); + + // We should calculate the widget duration as it might have changed + if ($currentItemsPerPage != $uniqueSlots) { + $this->getLog()->debug('saveElements: updating unique slots to ' . $uniqueSlots + . ', currentItemsPerPage: ' . $currentItemsPerPage); + + $module = $this->moduleFactory->getByType($widget->type); + $widget->calculateDuration($module); + } } // Save elements diff --git a/lib/Widget/DataSetProvider.php b/lib/Widget/DataSetProvider.php index 60070843de..b85ea01c93 100644 --- a/lib/Widget/DataSetProvider.php +++ b/lib/Widget/DataSetProvider.php @@ -50,8 +50,6 @@ public function fetchData(DataProviderInterface $dataProvider): WidgetProviderIn public function fetchDuration(DurationProviderInterface $durationProvider): WidgetProviderInterface { if ($durationProvider->getWidget()->getOptionValue('durationIsPerItem', 0) == 1) { - $this->getLog()->debug('fetchDuration: duration is per item'); - // Count of rows $numItems = $durationProvider->getWidget()->getOptionValue('numItems', 0); @@ -66,6 +64,13 @@ public function fetchDuration(DurationProviderInterface $durationProvider): Widg } $durationProvider->setDuration($durationProvider->getWidget()->calculatedDuration * $numItems); + + $this->getLog()->debug(sprintf( + 'fetchDuration: duration is per item, numItems: %s, rowsPerPage: %s, itemsPerPage: %s', + $numItems, + $rowsPerPage, + $itemsPerPage + )); } return $this; } diff --git a/ui/src/editor-core/widget.js b/ui/src/editor-core/widget.js index c817a00608..d67f92e8b3 100644 --- a/ui/src/editor-core/widget.js +++ b/ui/src/editor-core/widget.js @@ -685,12 +685,10 @@ Widget.prototype.saveElements = function( let savePending; const reloadLayout = function(forceReload = false) { - if (reload || forceReload) { - app.reloadData(app.layout, - { - refreshEditor: true, - }); - } + app.reloadData(app.layout, + { + refreshEditor: (reload || forceReload), + }); }; // If there's no more elements in widget, remove it diff --git a/ui/src/layout-editor/main.js b/ui/src/layout-editor/main.js index 1125158b1f..b4075a91fe 100644 --- a/ui/src/layout-editor/main.js +++ b/ui/src/layout-editor/main.js @@ -2951,6 +2951,7 @@ lD.checkLayoutStatus = function() { res.extra.status, res.html, res.extra.statusMessage, + res.extra.duration, ); if ( From 1c9524f4d0e79962dfd239f0843cbd113a4765f6 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Wed, 3 Jul 2024 14:39:31 +0100 Subject: [PATCH 12/22] Schedule: fix monthly repeat and from/to dates for timezone. (#2600) xibosignage/xibo#3429 --- lib/Controller/Schedule.php | 29 +++++++++++++++++++++++++---- views/schedule-page.twig | 4 ++-- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/lib/Controller/Schedule.php b/lib/Controller/Schedule.php index efdf7c0099..7988ec9b79 100644 --- a/lib/Controller/Schedule.php +++ b/lib/Controller/Schedule.php @@ -2284,8 +2284,9 @@ public function grid(Request $request, Response $response) ], $params) ); - // Setting for whether we show Layouts with out permissions + // Grab some settings which determine how events are displayed. $showLayoutName = ($this->getConfig()->getSetting('SCHEDULE_SHOW_LAYOUT_NAME') == 1); + $defaultTimezone = $this->getConfig()->getSetting('defaultTimezone'); foreach ($events as $event) { $event->load(); @@ -2348,15 +2349,25 @@ public function grid(Request $request, Response $response) $i++; } } else if ($event->recurrenceType === 'Month') { + // Force the timezone for this date (schedule from/to dates are timezone agnostic, but this + // date still has timezone information, which could lead to use formatting as the wrong day) + $date = Carbon::parse($event->fromDt)->tz($defaultTimezone); + $this->getLog()->debug('grid: setting description for monthly event with date: ' + . $date->toAtomString()); + if ($event->recurrenceMonthlyRepeatsOn === 0) { - $repeatsOn = 'the ' . Carbon::parse($event->fromDt)->format('jS') . ' day of the month'; + $repeatsOn = 'the ' . $date->format('jS') . ' day of the month'; } else { - $date = Carbon::parse($event->fromDt); + // Which day of the month is this? $firstDay = Carbon::parse('first ' . $date->format('l') . ' of ' . $date->format('F')); + + $this->getLog()->debug('grid: the first day of the month for this date is: ' + . $firstDay->toAtomString()); + $nth = $firstDay->diffInDays($date) / 7 + 1; $repeatWeekDayDate = $date->copy()->setDay($nth)->format('jS'); $repeatsOn = 'the ' . $repeatWeekDayDate . ' ' - . Carbon::parse($event->fromDt)->format('l') + . $date->format('l') . ' of the month'; } } @@ -2391,6 +2402,16 @@ public function grid(Request $request, Response $response) $event->toDt = $event->fromDt; } + // Set the row from/to date to be an ISO date for display (no timezone) + $event->setUnmatchedProperty( + 'displayFromDt', + Carbon::createFromTimestamp($event->fromDt)->format(DateFormatHelper::getSystemFormat()) + ); + $event->setUnmatchedProperty( + 'displayToDt', + Carbon::createFromTimestamp($event->toDt)->format(DateFormatHelper::getSystemFormat()) + ); + if ($this->isApi($request)) { continue; } diff --git a/views/schedule-page.twig b/views/schedule-page.twig index a045bbdec4..bf616b54ac 100644 --- a/views/schedule-page.twig +++ b/views/schedule-page.twig @@ -1374,7 +1374,7 @@ if (data.isAlways === 1) { return '{{ "Always"|trans }}' } else { - return moment(data.fromDt, 'X').format(jsDateFormat) + return moment(data.displayFromDt, systemDateFormat).format(jsDateFormat) } } }, @@ -1386,7 +1386,7 @@ if (data.isAlways === 1) { return '{{ "Always"|trans }}' } else { - return moment(data.toDt, 'X').format(jsDateFormat) + return moment(data.displayToDt, systemDateFormat).format(jsDateFormat) } } }, From 040159d67a71e737a4fbe7958dea6064a7d3c4d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauro=20Ferr=C3=A3o?= Date: Thu, 4 Jul 2024 08:51:15 +0100 Subject: [PATCH 13/22] Stocks: Incorrect calculation of Total Duration for elements with Duration Per Item (#2586) * Stocks: Incorrect calculation of Total Duration for elements with Duration Per Item relates to xibosignage/xibo#3441 * Fix for template rendering and hide num items fields relates to xibosignage/xibo#3441 * Widget: stock static templates not pulling itemsPerPage from XML. xibosignage/xibo#3441 --------- Co-authored-by: Dan Garner --- lib/Factory/ModuleXmlTrait.php | 2 +- lib/Widget/CurrenciesAndStocksProvider.php | 2 +- lib/Widget/Definition/Property.php | 9 ++++++-- modules/src/xibo-finance-render.js | 20 ++++++++++------- modules/templates/currency-static.xml | 22 ++++++++++--------- modules/templates/stock-static.xml | 16 ++++++++------ .../Widget/CurrenciesWidgetTest.php | 4 ++-- 7 files changed, 44 insertions(+), 31 deletions(-) diff --git a/lib/Factory/ModuleXmlTrait.php b/lib/Factory/ModuleXmlTrait.php index ec223f5f9d..d1b4743eb3 100644 --- a/lib/Factory/ModuleXmlTrait.php +++ b/lib/Factory/ModuleXmlTrait.php @@ -122,9 +122,9 @@ private function parseProperties($propertyNodes, ?Module $module = null): array $property->allowLibraryRefs = $node->getAttribute('allowLibraryRefs') === 'true'; $property->allowAssetRefs = $node->getAttribute('allowAssetRefs') === 'true'; $property->parseTranslations = $node->getAttribute('parseTranslations') === 'true'; + $property->saveDefault = $node->getAttribute('saveDefault') === 'true'; $property->title = __($this->getFirstValueOrDefaultFromXmlNode($node, 'title')); $property->helpText = __($this->getFirstValueOrDefaultFromXmlNode($node, 'helpText')); - $property->value = $this->getFirstValueOrDefaultFromXmlNode($node, 'value'); $property->dependsOn = $this->getFirstValueOrDefaultFromXmlNode($node, 'dependsOn'); // How should we default includeInXlf? diff --git a/lib/Widget/CurrenciesAndStocksProvider.php b/lib/Widget/CurrenciesAndStocksProvider.php index 706458e8d4..b20e5f5257 100644 --- a/lib/Widget/CurrenciesAndStocksProvider.php +++ b/lib/Widget/CurrenciesAndStocksProvider.php @@ -74,7 +74,7 @@ public function fetchDuration(DurationProviderInterface $durationProvider): Widg if ($numItems > 1) { // If we have paging involved then work out the page count. - $itemsPerPage = $durationProvider->getWidget()->getOptionValue('maxItemsPerPage', 0); + $itemsPerPage = $durationProvider->getWidget()->getOptionValue('itemsPerPage', 0); if ($itemsPerPage > 0) { $numItems = ceil($numItems / $itemsPerPage); } diff --git a/lib/Widget/Definition/Property.php b/lib/Widget/Definition/Property.php index ab6fe02cd8..40ff8c7ca0 100644 --- a/lib/Widget/Definition/Property.php +++ b/lib/Widget/Definition/Property.php @@ -90,9 +90,12 @@ class Property implements \JsonSerializable /** @var bool Should translations be parsed in the value? */ public $parseTranslations = false; - /** @var bool Should the prooperty be included in the XLF? */ + /** @var bool Should the property be included in the XLF? */ public $includeInXlf = false; + /** @var bool Should the default value be written out to widget options */ + public $saveDefault = false; + /** @var \Xibo\Widget\Definition\PlayerCompatibility */ public $playerCompatibility; @@ -111,6 +114,7 @@ class Property implements \JsonSerializable /** @var string The group ID of the property */ public $propertyGroupId; + /** @var mixed The value assigned to this property. This is set from widget options, or settings, never via XML */ public $value; /** @inheritDoc */ @@ -136,6 +140,7 @@ public function jsonSerialize(): array 'allowLibraryRefs' => $this->allowLibraryRefs, 'allowAssetRefs' => $this->allowAssetRefs, 'parseTranslations' => $this->parseTranslations, + 'saveDefault' => $this->saveDefault, 'dependsOn' => $this->dependsOn, ]; } @@ -197,7 +202,7 @@ public function setValueByType( bool $ignoreDefault = false ): Property { $value = $this->getByType($params, $key); - if ($value !== $this->default || $ignoreDefault) { + if ($value !== $this->default || $ignoreDefault || $this->saveDefault) { $this->value = $value; } return $this; diff --git a/modules/src/xibo-finance-render.js b/modules/src/xibo-finance-render.js index 5d190b0707..43247d1b45 100644 --- a/modules/src/xibo-finance-render.js +++ b/modules/src/xibo-finance-render.js @@ -1,7 +1,7 @@ /* - * Copyright (C) 2023 Xibo Signage Ltd + * Copyright (C) 2024 Xibo Signage Ltd * - * Xibo - Digital Signage - http://www.xibo.org.uk + * Xibo - Digital Signage - https://xibosignage.com * * This file is part of Xibo. * @@ -28,7 +28,7 @@ jQuery.fn.extend({ duration: '30', durationIsPerItem: false, numItems: items.length, - maxItemsPerPage: 5, + itemsPerPage: 5, previewWidth: 0, previewHeight: 0, scaleOverride: 0, @@ -36,6 +36,10 @@ jQuery.fn.extend({ options = $.extend({}, defaults, options); + if (!options.itemsPerPage) { + options.itemsPerPage = 1; + } + // Calculate the dimensions of this itemoptions.numItems // based on the preview/original dimensions let width = height = 0; @@ -65,8 +69,8 @@ jQuery.fn.extend({ this.each(function(_idx, _elem) { // How many pages to we need? const numberOfPages = - (options.numItems > options.maxItemsPerPage) ? - Math.ceil(options.numItems / options.maxItemsPerPage) : 1; + (options.numItems > options.itemsPerPage) ? + Math.ceil(options.numItems / options.itemsPerPage) : 1; const $mainContainer = $(_elem); // Destroy any existing cycle @@ -91,9 +95,9 @@ jQuery.fn.extend({ for (let i = 0; i < numberOfPages; i++) { // Create a page const $itemsHTML = $('
').addClass('page'); - for (let j = 0; j < options.maxItemsPerPage; j++) { - if (((i * options.maxItemsPerPage) + j) < options.numItems) { - const $item = $(items[(i * options.maxItemsPerPage) + j]); + for (let j = 0; j < options.itemsPerPage; j++) { + if (((i * options.itemsPerPage) + j) < options.numItems) { + const $item = $(items[(i * options.itemsPerPage) + j]); // Clone and append the item to the page // and remove template-item class when isEditor = true (isEditor ? $item.clone() : $item).appendTo($itemsHTML) diff --git a/modules/templates/currency-static.xml b/modules/templates/currency-static.xml index 2b0aabefcf..ddca85c2e6 100644 --- a/modules/templates/currency-static.xml +++ b/modules/templates/currency-static.xml @@ -1,5 +1,5 @@