From 9179f9d999f108160c8807b0da344898d7c0d866 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Thu, 3 Aug 2023 12:19:13 +0100 Subject: [PATCH 01/13] Middleware: IDE help for getContainer --- lib/Middleware/CustomMiddlewareTrait.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Middleware/CustomMiddlewareTrait.php b/lib/Middleware/CustomMiddlewareTrait.php index c6c8d49dc4..4dd9112b94 100644 --- a/lib/Middleware/CustomMiddlewareTrait.php +++ b/lib/Middleware/CustomMiddlewareTrait.php @@ -1,8 +1,8 @@ Date: Fri, 4 Aug 2023 17:20:56 +0100 Subject: [PATCH 02/13] SAML support for OKTA (#1963) relates to xibosignage/xibo#3082 --- lib/Middleware/SAMLAuthentication.php | 70 +++++++++++++++++---------- 1 file changed, 45 insertions(+), 25 deletions(-) diff --git a/lib/Middleware/SAMLAuthentication.php b/lib/Middleware/SAMLAuthentication.php index e2d3ff1408..5165456536 100644 --- a/lib/Middleware/SAMLAuthentication.php +++ b/lib/Middleware/SAMLAuthentication.php @@ -1,8 +1,8 @@ getContainer()->logoutRoute = 'saml.logout'; // Route providing SAML metadata - $app->get('/saml/metadata', function (\Slim\Http\ServerRequest $request, \Slim\Http\Response $response) { + $app->get('/saml/metadata', function (Request $request, Response $response) { $settings = new Settings($this->getConfig()->samlSettings, true); $metadata = $settings->getSPMetadata(); $errors = $settings->validateMetadata($metadata); @@ -72,19 +71,19 @@ public function addRoutes() }); // SAML Login - $app->get('/saml/login', function (\Slim\Http\ServerRequest $request, \Slim\Http\Response $response) { + $app->get('/saml/login', function (Request $request, Response $response) { // Initiate SAML SSO $auth = new Auth($this->getConfig()->samlSettings); return $auth->login(); }); // SAML Logout - $app->get('/saml/logout', function (\Slim\Http\ServerRequest $request, \Slim\Http\Response $response) { + $app->get('/saml/logout', function (Request $request, Response $response) { return $this->samlLogout($request, $response); })->setName('saml.logout'); // SAML Assertion Consumer Endpoint - $app->post('/saml/acs', function (\Slim\Http\ServerRequest $request, \Slim\Http\Response $response) { + $app->post('/saml/acs', function (Request $request, Response $response) { // Log some interesting things $this->getLog()->debug('Arrived at the ACS route with own URL: ' . Utils::getSelfRoutedURLNoQuery()); @@ -278,30 +277,40 @@ public function addRoutes() ]); } - // Redirect back to the originally-requested url + // Redirect back to the originally-requested url, if provided + // it is not clear why basename is used here, it seems to be something to do with a logout loop $params = $request->getParams(); - $redirect = !isset($params['RelayState']) || (isset($params['RelayState']) && basename($params['RelayState']) == 'login') + $relayState = $params['RelayState'] ?? null; + $redirect = empty($relayState) || basename($relayState) === 'login' ? $this->getRouteParser()->urlFor('home') - : $params['RelayState']; + : $relayState; return $response->withRedirect($redirect); } }); // Single Logout Service - $app->get('/saml/sls', function (\Slim\Http\ServerRequest $request, \Slim\Http\Response $response) use ($app) { - + $app->map(['GET', 'POST'], '/saml/sls', function (Request $request, Response $response) use ($app) { + // Make request to IDP $auth = new Auth($app->getContainer()->get('configService')->samlSettings); - $auth->processSLO(false, null, false, function () use ($request) { - $this->completeLogoutFlow($this->getUser($_SESSION['userid'], $request->getAttribute('ip_address')), $this->getSession(), $this->getLog(), $request); - }); + try { + $auth->processSLO(false, null, false, function () use ($request) { + // Audit that the IDP has completed this request. + $this->getLog()->setIpAddress($request->getAttribute('ip_address')); + $this->getLog()->audit('User', 0, 'Idp SLO completed', [ + 'UserAgent' => $request->getHeader('User-Agent') + ]); + }); + } catch (\Exception $e) { + // Ignored - get with getErrors() + } $errors = $auth->getErrors(); if (empty($errors)) { return $response->withRedirect($this->getRouteParser()->urlFor('home')); } else { - throw new AccessDeniedException("SLO failed. " . implode(', ', $errors)); + throw new AccessDeniedException('SLO failed. ' . implode(', ', $errors)); } }); @@ -309,8 +318,8 @@ public function addRoutes() } /** - * @param \Psr\Http\Message\ServerRequestInterface $request - * @param \Psr\Http\Message\ResponseInterface $response + * @param \Slim\Http\ServerRequest $request + * @param \Slim\Http\Response $response * @return \Psr\Http\Message\ResponseInterface|\Slim\Http\Response * @throws \OneLogin\Saml2\Error */ @@ -322,6 +331,14 @@ public function samlLogout(Request $request, Response $response) && isset($samlSettings['workflow']['slo']) && $samlSettings['workflow']['slo'] == true ) { + // Complete our own logout flow + $this->completeLogoutFlow( + $this->getUser($_SESSION['userid'], $request->getAttribute('ip_address')), + $this->getSession(), + $this->getLog(), + $request + ); + // Initiate SAML SLO $auth = new Auth($samlSettings); return $response->withRedirect($auth->logout()); @@ -335,7 +352,7 @@ public function samlLogout(Request $request, Response $response) * @return Response * @throws \OneLogin\Saml2\Error */ - public function redirectToLogin(Request $request) + public function redirectToLogin(\Psr\Http\Message\ServerRequestInterface $request) { if ($this->isAjax($request)) { return $this->createResponse($request)->withJson(ApplicationState::asRequiresLogin()); @@ -347,7 +364,7 @@ public function redirectToLogin(Request $request) } /** @inheritDoc */ - public function getPublicRoutes(Request $request) + public function getPublicRoutes(\Psr\Http\Message\ServerRequestInterface $request) { return array_merge($request->getAttribute('publicRoutes', []), [ '/saml/metadata', @@ -367,8 +384,11 @@ public function shouldRedirectPublicRoute($route) } /** @inheritDoc */ - public function addToRequest(Request $request) + public function addToRequest(\Psr\Http\Message\ServerRequestInterface $request) { - return $request->withAttribute('excludedCsrfRoutes', array_merge($request->getAttribute('excludedCsrfRoutes', []), ['/saml/acs'])); + return $request->withAttribute( + 'excludedCsrfRoutes', + array_merge($request->getAttribute('excludedCsrfRoutes', []), ['/saml/acs', '/saml/sls']) + ); } } From 269ee2f1ce8387e8b79802eeb8c15764c52ea434 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Fri, 4 Aug 2023 18:47:13 +0100 Subject: [PATCH 03/13] Widget: duration not calculated correctly for dataset/menuboard due to upper/lower limits. Refactor calculation duration. --- lib/Entity/Module.php | 42 +++++++++++-- lib/Entity/Widget.php | 43 +++++-------- lib/Event/MenuBoardModifiedDtRequest.php | 60 +++++++++++++++++++ lib/Factory/ModuleFactory.php | 8 +-- lib/Listener/MenuBoardProviderListener.php | 17 +++++- lib/Middleware/ListenersMiddleware.php | 3 +- lib/Widget/AudioProvider.php | 20 +++---- lib/Widget/DataSetProvider.php | 8 ++- lib/Widget/MenuBoardProductProvider.php | 18 +++++- lib/Widget/Provider/DurationProvider.php | 44 +++++++++----- .../Provider/DurationProviderInterface.php | 15 ++++- lib/Widget/VideoProvider.php | 20 +++---- modules/menuboard-product.xml | 6 +- 13 files changed, 221 insertions(+), 83 deletions(-) create mode 100644 lib/Event/MenuBoardModifiedDtRequest.php diff --git a/lib/Entity/Module.php b/lib/Entity/Module.php index a5f32cce4b..172cb34ccb 100644 --- a/lib/Entity/Module.php +++ b/lib/Entity/Module.php @@ -354,13 +354,47 @@ public function createDataProvider(Widget $widget): DataProvider */ public function fetchDurationOrDefaultFromFile(string $file): int { - if ($this->widgetProvider === null) { - return $this->defaultDuration; + $this->getLog()->debug('fetchDurationOrDefaultFromFile: fetchDuration with file: ' . $file); + + // If we don't have a file name, then we use the default duration of 0 (end-detect) + if (empty($file)) { + return 0; + } else { + $info = new \getID3(); + $file = $info->analyze($file); + return intval($file['playtime_seconds'] ?? 0); + } + } + + /** + * Calculate the duration of this Widget. + * @param Widget $widget + * @return int|null + */ + public function calculateDuration(Widget $widget): ?int + { + if ($this->widgetProvider === null && $this->regionSpecific === 1) { + // Take some default action to cover the majourity of region specific widgets + // Duration can depend on the number of items per page for some widgets + // this is a legacy way of working, and our preference is to use elements + $numItems = $widget->getOptionValue('numItems', 15); + + if ($widget->getOptionValue('durationIsPerItem', 0) == 1 && $numItems > 1) { + // If we have paging involved then work out the page count. + $itemsPerPage = $widget->getOptionValue('itemsPerPage', 0); + if ($itemsPerPage > 0) { + $numItems = ceil($numItems / $itemsPerPage); + } + + return $widget->calculatedDuration * $numItems; + } + } else if ($this->widgetProvider === null) { + return null; } - $durationProvider = $this->moduleFactory->createDurationProvider($file, null); + $durationProvider = $this->moduleFactory->createDurationProvider($this, $widget); $this->widgetProvider->fetchDuration($durationProvider); - return $durationProvider->getDuration(); + return $durationProvider->isDurationSet() ? $durationProvider->getDuration() : null; } /** diff --git a/lib/Entity/Widget.php b/lib/Entity/Widget.php index 4ab683b457..4fefdcddfb 100644 --- a/lib/Entity/Widget.php +++ b/lib/Entity/Widget.php @@ -765,33 +765,13 @@ public function calculateDuration( $event = new SubPlaylistDurationEvent($this); $this->getDispatcher()->dispatch($event, SubPlaylistDurationEvent::$NAME); $this->calculatedDuration = $event->getDuration(); - } else if (($module->type === 'video' || $module->type === 'audio') && $this->useDuration === 0) { - // Video/Audio needs handling for the default duration being 0. - $this->getLog()->debug('calculateDuration: ' . $module->type . ' without specified duration'); - - try { - $mediaId = $this->getPrimaryMediaId(); - $this->calculatedDuration = $this->widgetMediaFactory->getDurationForMediaId($mediaId); - } catch (NotFoundException $notFoundException) { - $this->getLog()->error('calculateDuration: video/audio without primaryMediaId. widgetId: ' - . $this->widgetId); - } - } else if ($module->regionSpecific === 1) { - // Non-file based module - $this->getLog()->debug('calculateDuration: ' . $module->type . ', non-file based module.'); - - // Duration can depend on the number of items per page for some widgets - // this is a legacy way of working, and our preference is to use elements - $numItems = $this->getOptionValue('numItems', 15); - - if ($this->getOptionValue('durationIsPerItem', 0) == 1 && $numItems > 1) { - // If we have paging involved then work out the page count. - $itemsPerPage = $this->getOptionValue('itemsPerPage', 0); - if ($itemsPerPage > 0) { - $numItems = ceil($numItems / $itemsPerPage); - } - - $this->calculatedDuration = $this->calculatedDuration * $numItems; + } else { + // Our module will calculate the duration for us. + $duration = $module->calculateDuration($this); + if ($duration !== null) { + $this->calculatedDuration = $duration; + } else { + $this->getLog()->debug('calculateDuration: Duration not set by module'); } } @@ -799,6 +779,15 @@ public function calculateDuration( return $this; } + /** + * @return int + * @throws NotFoundException + */ + public function getDurationForMedia(): int + { + return $this->widgetMediaFactory->getDurationForMediaId($this->getPrimaryMediaId()); + } + /** * Load the Widget * @param bool $loadActions diff --git a/lib/Event/MenuBoardModifiedDtRequest.php b/lib/Event/MenuBoardModifiedDtRequest.php new file mode 100644 index 0000000000..c945517407 --- /dev/null +++ b/lib/Event/MenuBoardModifiedDtRequest.php @@ -0,0 +1,60 @@ +. + */ + +namespace Xibo\Event; + +use Carbon\Carbon; + +/** + * Menu Board Product Request. + */ +class MenuBoardModifiedDtRequest extends Event +{ + public static $NAME = 'menuboard.modifiedDt.request.event'; + + /** @var int */ + private $menuId; + + /** @var Carbon */ + private $modifiedDt; + + public function __construct(int $menuId) + { + $this->menuId = $menuId; + } + + public function getDataSetId(): int + { + return $this->menuId; + } + + public function setModifiedDt(Carbon $modifiedDt): MenuBoardModifiedDtRequest + { + $this->modifiedDt = $modifiedDt; + return $this; + } + + public function getModifiedDt(): ?Carbon + { + return $this->modifiedDt; + } +} diff --git a/lib/Factory/ModuleFactory.php b/lib/Factory/ModuleFactory.php index f6284a89e0..0ab166583d 100644 --- a/lib/Factory/ModuleFactory.php +++ b/lib/Factory/ModuleFactory.php @@ -93,13 +93,13 @@ public function createDataProvider(Module $module, Widget $widget): DataProvider } /** - * @param string $path - * @param int|null $duration + * @param Module $module + * @param Widget $widget * @return DurationProviderInterface */ - public function createDurationProvider(string $path, ?int $duration): DurationProviderInterface + public function createDurationProvider(Module $module, Widget $widget): DurationProviderInterface { - return new DurationProvider($path, $duration); + return new DurationProvider($module, $widget); } /** diff --git a/lib/Listener/MenuBoardProviderListener.php b/lib/Listener/MenuBoardProviderListener.php index aed34045e7..3f88f024a0 100644 --- a/lib/Listener/MenuBoardProviderListener.php +++ b/lib/Listener/MenuBoardProviderListener.php @@ -22,10 +22,13 @@ namespace Xibo\Listener; +use Carbon\Carbon; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Xibo\Event\MenuBoardCategoryRequest; +use Xibo\Event\MenuBoardModifiedDtRequest; use Xibo\Event\MenuBoardProductRequest; use Xibo\Factory\MenuBoardCategoryFactory; +use Xibo\Factory\MenuBoardFactory; use Xibo\Support\Exception\NotFoundException; /** @@ -35,10 +38,13 @@ class MenuBoardProviderListener { use ListenerLoggerTrait; + private MenuBoardFactory $menuBoardFactory; + private MenuBoardCategoryFactory $menuBoardCategoryFactory; - public function __construct(MenuBoardCategoryFactory $menuBoardCategoryFactory) + public function __construct(MenuBoardFactory $menuBoardFactory, MenuBoardCategoryFactory $menuBoardCategoryFactory) { + $this->menuBoardFactory = $menuBoardFactory; $this->menuBoardCategoryFactory = $menuBoardCategoryFactory; } @@ -46,6 +52,7 @@ public function registerWithDispatcher(EventDispatcherInterface $dispatcher): Me { $dispatcher->addListener(MenuBoardProductRequest::$NAME, [$this, 'onProductRequest']); $dispatcher->addListener(MenuBoardCategoryRequest::$NAME, [$this, 'onCategoryRequest']); + $dispatcher->addListener(MenuBoardModifiedDtRequest::$NAME, [$this, 'onModifiedDtRequest']); return $this; } @@ -76,7 +83,7 @@ public function onProductRequest(MenuBoardProductRequest $event): void $categoryId = $dataProvider->getProperty('categoryId'); $this->getLogger()->debug('onProductRequest: $categoryId: ' . $categoryId); - if ($categoryId !== null && $categoryId !== "") { + if ($categoryId !== null && $categoryId !== '') { $filter['menuCategoryId'] = intval($categoryId); } @@ -151,4 +158,10 @@ public function onCategoryRequest(MenuBoardCategoryRequest $event): void $dataProvider->setIsHandled(); } + + public function onModifiedDtRequest(MenuBoardModifiedDtRequest $event): void + { + $menu = $this->menuBoardFactory->getById($event->getDataSetId()); + $event->setModifiedDt(Carbon::createFromTimestamp($menu->modifiedDt)); + } } diff --git a/lib/Middleware/ListenersMiddleware.php b/lib/Middleware/ListenersMiddleware.php index 03569a55f6..e98633d2fa 100644 --- a/lib/Middleware/ListenersMiddleware.php +++ b/lib/Middleware/ListenersMiddleware.php @@ -357,7 +357,8 @@ public static function setListeners(App $app) ->registerWithDispatcher($dispatcher); (new MenuBoardProviderListener( - $c->get('menuBoardCategoryFactory') + $c->get('menuBoardFactory'), + $c->get('menuBoardCategoryFactory'), )) ->useLogger($c->get('logger')) ->registerWithDispatcher($dispatcher); diff --git a/lib/Widget/AudioProvider.php b/lib/Widget/AudioProvider.php index 6693edfd83..37731f5d83 100644 --- a/lib/Widget/AudioProvider.php +++ b/lib/Widget/AudioProvider.php @@ -23,6 +23,7 @@ namespace Xibo\Widget; use Carbon\Carbon; +use Xibo\Support\Exception\NotFoundException; use Xibo\Widget\Provider\DataProviderInterface; use Xibo\Widget\Provider\DurationProviderInterface; use Xibo\Widget\Provider\WidgetProviderInterface; @@ -42,17 +43,14 @@ public function fetchData(DataProviderInterface $dataProvider): WidgetProviderIn public function fetchDuration(DurationProviderInterface $durationProvider): WidgetProviderInterface { - $fileName = $durationProvider->getFile(); - - $this->getLog()->debug('AudioProvider: fetchDuration with file: ' . $fileName); - - // If we don't have a file name, then we use the default duration of 0 (end-detect) - if (empty($fileName)) { - $durationProvider->setDuration(0); - } else { - $info = new \getID3(); - $file = $info->analyze($fileName); - $durationProvider->setDuration(intval($file['playtime_seconds'] ?? 0)); + // If we have not been provided a specific duration, we should use the duration stored in the library + try { + if ($durationProvider->getWidget()->useDuration === 0) { + $durationProvider->setDuration($durationProvider->getWidget()->getDurationForMedia()); + } + } catch (NotFoundException) { + $this->getLog()->error('fetchDuration: video/audio without primaryMediaId. widgetId: ' + . $durationProvider->getWidget()->getId()); } return $this; } diff --git a/lib/Widget/DataSetProvider.php b/lib/Widget/DataSetProvider.php index d0dec66011..07ca2dd697 100644 --- a/lib/Widget/DataSetProvider.php +++ b/lib/Widget/DataSetProvider.php @@ -2,7 +2,7 @@ /* * Copyright (C) 2023 Xibo Signage Ltd * - * Xibo - Digital Signage - http://www.xibo.org.uk + * Xibo - Digital Signage - https://xibosignage.com * * This file is part of Xibo. * @@ -49,6 +49,12 @@ public function fetchData(DataProviderInterface $dataProvider): WidgetProviderIn public function fetchDuration(DurationProviderInterface $durationProvider): WidgetProviderInterface { + $this->getLog()->debug('fetchDuration'); + + $lowerLimit = $durationProvider->getWidget()->getOptionValue('lowerLimit', 0); + $upperLimit = $durationProvider->getWidget()->getOptionValue('upperLimit', 15); + $durationProvider->setDuration(($upperLimit - $lowerLimit) + * $durationProvider->getWidget()->calculatedDuration); return $this; } diff --git a/lib/Widget/MenuBoardProductProvider.php b/lib/Widget/MenuBoardProductProvider.php index 101ba296d7..ee4c4c9041 100644 --- a/lib/Widget/MenuBoardProductProvider.php +++ b/lib/Widget/MenuBoardProductProvider.php @@ -23,6 +23,7 @@ namespace Xibo\Widget; use Carbon\Carbon; +use Xibo\Event\MenuBoardModifiedDtRequest; use Xibo\Event\MenuBoardProductRequest; use Xibo\Widget\Provider\DataProviderInterface; use Xibo\Widget\Provider\DurationProviderInterface; @@ -45,6 +46,12 @@ public function fetchData(DataProviderInterface $dataProvider): WidgetProviderIn public function fetchDuration(DurationProviderInterface $durationProvider): WidgetProviderInterface { + $this->getLog()->debug('fetchDuration'); + + $lowerLimit = $durationProvider->getWidget()->getOptionValue('lowerLimit', 0); + $upperLimit = $durationProvider->getWidget()->getOptionValue('upperLimit', 15); + $durationProvider->setDuration(($upperLimit - $lowerLimit) + * $durationProvider->getWidget()->calculatedDuration); return $this; } @@ -55,6 +62,15 @@ public function getDataCacheKey(DataProviderInterface $dataProvider): ?string public function getDataModifiedDt(DataProviderInterface $dataProvider): ?Carbon { - return null; + $this->getLog()->debug('fetchData: MenuBoardProductProvider passing to modifiedDt request event'); + $menuId = $dataProvider->getProperty('menuId'); + if ($menuId !== null) { + // Raise an event to get the modifiedDt of this dataSet + $event = new MenuBoardModifiedDtRequest($menuId); + $this->getDispatcher()->dispatch($event, MenuBoardModifiedDtRequest::$NAME); + return max($event->getModifiedDt(), $dataProvider->getWidgetModifiedDt()); + } else { + return null; + } } } diff --git a/lib/Widget/Provider/DurationProvider.php b/lib/Widget/Provider/DurationProvider.php index 21ce9b2b25..d8581475ee 100644 --- a/lib/Widget/Provider/DurationProvider.php +++ b/lib/Widget/Provider/DurationProvider.php @@ -22,13 +22,19 @@ namespace Xibo\Widget\Provider; +use Xibo\Entity\Module; +use Xibo\Entity\Widget; + /** * Xibo's default implementation of the Duration Provider */ class DurationProvider implements DurationProviderInterface { - /** @var string */ - private $file; + /** @var Module */ + private $module; + + /** @var Widget */ + private $widget; /** @var int Duration in seconds */ private $duration; @@ -38,21 +44,13 @@ class DurationProvider implements DurationProviderInterface /** * Constructor - * @param string|null $file - * @param int|null $duration - */ - public function __construct(string $file, ?int $duration) - { - $this->file = $file; - $this->duration = $duration; - } - - /** - * @inheritDoc + * @param Module $module + * @param Widget $widget */ - public function getFile(): string + public function __construct(Module $module, Widget $widget) { - return $this->file; + $this->module = $module; + $this->widget = $widget; } /** @@ -80,4 +78,20 @@ public function isDurationSet(): bool { return $this->isDurationSet; } + + /** + * @inheritDoc + */ + public function getModule(): Module + { + return $this->module; + } + + /** + * @inheritDoc + */ + public function getWidget(): Widget + { + return $this->widget; + } } diff --git a/lib/Widget/Provider/DurationProviderInterface.php b/lib/Widget/Provider/DurationProviderInterface.php index 15b0276cdb..c7d8c109e5 100644 --- a/lib/Widget/Provider/DurationProviderInterface.php +++ b/lib/Widget/Provider/DurationProviderInterface.php @@ -22,16 +22,25 @@ namespace Xibo\Widget\Provider; +use Xibo\Entity\Module; +use Xibo\Entity\Widget; + /** * A duration provider is used to return the duration for a Widget which has a media file */ interface DurationProviderInterface { /** - * Get the fully qualified path name of the file that needs its duration assessed - * @return string the fully qualified path to the file + * Get the Module + * @return Module + */ + public function getModule(): Module; + + /** + * Get the Widget + * @return Widget */ - public function getFile(): string; + public function getWidget(): Widget; /** * Get the duration diff --git a/lib/Widget/VideoProvider.php b/lib/Widget/VideoProvider.php index 3a54339968..0708ee1d42 100644 --- a/lib/Widget/VideoProvider.php +++ b/lib/Widget/VideoProvider.php @@ -23,6 +23,7 @@ namespace Xibo\Widget; use Carbon\Carbon; +use Xibo\Support\Exception\NotFoundException; use Xibo\Widget\Provider\DataProviderInterface; use Xibo\Widget\Provider\DurationProviderInterface; use Xibo\Widget\Provider\WidgetProviderInterface; @@ -42,17 +43,14 @@ public function fetchData(DataProviderInterface $dataProvider): WidgetProviderIn public function fetchDuration(DurationProviderInterface $durationProvider): WidgetProviderInterface { - $fileName = $durationProvider->getFile(); - - $this->getLog()->debug('VideoProvider: fetchDuration with file: ' . $fileName); - - // If we don't have a file name, then we use the default duration of 0 (end-detect) - if (empty($fileName)) { - $durationProvider->setDuration(0); - } else { - $info = new \getID3(); - $file = $info->analyze($fileName); - $durationProvider->setDuration(intval($file['playtime_seconds'] ?? 0)); + // If we have not been provided a specific duration, we should use the duration stored in the library + try { + if ($durationProvider->getWidget()->useDuration === 0) { + $durationProvider->setDuration($durationProvider->getWidget()->getDurationForMedia()); + } + } catch (NotFoundException) { + $this->getLog()->error('fetchDuration: video/audio without primaryMediaId. widgetId: ' + . $durationProvider->getWidget()->getId()); } return $this; } diff --git a/modules/menuboard-product.xml b/modules/menuboard-product.xml index 879bd42621..29eb5560f7 100644 --- a/modules/menuboard-product.xml +++ b/modules/menuboard-product.xml @@ -83,7 +83,7 @@ Lower Row Limit - Optionally provide a Lower Row Limit (enter 0 for no limit). + Provide a Lower Row Limit. 0 @@ -96,8 +96,8 @@ Upper Row Limit - Optionally provide an Upper Row Limit (enter 0 for no limit). - 0 + Provide an Upper Row Limit. + 15 0 From 25c2b28a20e5ab595cc4def0a980a034196403f2 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Sat, 5 Aug 2023 11:39:20 +0100 Subject: [PATCH 04/13] Widget: fix new duration calc on import. --- lib/Entity/Module.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/Entity/Module.php b/lib/Entity/Module.php index 172cb34ccb..105924c410 100644 --- a/lib/Entity/Module.php +++ b/lib/Entity/Module.php @@ -387,6 +387,8 @@ public function calculateDuration(Widget $widget): ?int } return $widget->calculatedDuration * $numItems; + } else { + return null; } } else if ($this->widgetProvider === null) { return null; From e4e46c99268a1d333245fdabc2928bd58141f1d5 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Sat, 5 Aug 2023 12:18:58 +0100 Subject: [PATCH 05/13] Editor preview, fix js error. --- ui/src/layout-editor/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/layout-editor/main.js b/ui/src/layout-editor/main.js index 974ae2e378..d27672a0ab 100644 --- a/ui/src/layout-editor/main.js +++ b/ui/src/layout-editor/main.js @@ -3526,7 +3526,7 @@ lD.handleMessage = function(event) { const messageFromSender = event.data; if (messageFromSender == 'viewerStoppedPlaying') { // Refresh designer - lDrefreshEditor({ + lD.refreshEditor({ reloadToolbar: false, reloadViewer: true, reloadPropertiesPanel: true, From 7e11fdd6fcde435e1467451e5e988d7facdcdbcd Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Sat, 5 Aug 2023 12:34:43 +0100 Subject: [PATCH 06/13] Widget: fix RSS/Mastodon/Notification duration calculation. --- lib/Entity/Module.php | 3 ++ lib/Widget/MastodonProvider.php | 8 +-- lib/Widget/NotificationProvider.php | 10 ++-- .../DurationProviderNumItemsTrait.php | 50 +++++++++++++++++++ lib/Widget/RssProvider.php | 10 ++-- 5 files changed, 61 insertions(+), 20 deletions(-) create mode 100644 lib/Widget/Provider/DurationProviderNumItemsTrait.php diff --git a/lib/Entity/Module.php b/lib/Entity/Module.php index 105924c410..4dac4a35d7 100644 --- a/lib/Entity/Module.php +++ b/lib/Entity/Module.php @@ -393,6 +393,9 @@ public function calculateDuration(Widget $widget): ?int } else if ($this->widgetProvider === null) { return null; } + + $this->getLog()->debug('calculateDuration: using widget provider'); + $durationProvider = $this->moduleFactory->createDurationProvider($this, $widget); $this->widgetProvider->fetchDuration($durationProvider); diff --git a/lib/Widget/MastodonProvider.php b/lib/Widget/MastodonProvider.php index ed4047dd14..786044f1c7 100644 --- a/lib/Widget/MastodonProvider.php +++ b/lib/Widget/MastodonProvider.php @@ -26,7 +26,7 @@ use GuzzleHttp\Exception\RequestException; use Xibo\Widget\DataType\SocialMedia; use Xibo\Widget\Provider\DataProviderInterface; -use Xibo\Widget\Provider\DurationProviderInterface; +use Xibo\Widget\Provider\DurationProviderNumItemsTrait; use Xibo\Widget\Provider\WidgetProviderInterface; use Xibo\Widget\Provider\WidgetProviderTrait; @@ -36,6 +36,7 @@ class MastodonProvider implements WidgetProviderInterface { use WidgetProviderTrait; + use DurationProviderNumItemsTrait; public function fetchData(DataProviderInterface $dataProvider): WidgetProviderInterface { @@ -146,11 +147,6 @@ public function fetchData(DataProviderInterface $dataProvider): WidgetProviderIn return $this; } - public function fetchDuration(DurationProviderInterface $durationProvider): WidgetProviderInterface - { - return $this; - } - public function getDataCacheKey(DataProviderInterface $dataProvider): ?string { // No special cache key requirements. diff --git a/lib/Widget/NotificationProvider.php b/lib/Widget/NotificationProvider.php index f9e9d08ef6..ad38c45012 100644 --- a/lib/Widget/NotificationProvider.php +++ b/lib/Widget/NotificationProvider.php @@ -2,7 +2,7 @@ /* * Copyright (C) 2023 Xibo Signage Ltd * - * Xibo - Digital Signage - http://www.xibo.org.uk + * Xibo - Digital Signage - https://xibosignage.com * * This file is part of Xibo. * @@ -25,13 +25,14 @@ use Carbon\Carbon; use Xibo\Event\NotificationDataRequestEvent; use Xibo\Widget\Provider\DataProviderInterface; -use Xibo\Widget\Provider\DurationProviderInterface; +use Xibo\Widget\Provider\DurationProviderNumItemsTrait; use Xibo\Widget\Provider\WidgetProviderInterface; use Xibo\Widget\Provider\WidgetProviderTrait; class NotificationProvider implements WidgetProviderInterface { use WidgetProviderTrait; + use DurationProviderNumItemsTrait; public function fetchData(DataProviderInterface $dataProvider): WidgetProviderInterface { @@ -43,11 +44,6 @@ public function fetchData(DataProviderInterface $dataProvider): WidgetProviderIn return $this; } - public function fetchDuration(DurationProviderInterface $durationProvider): WidgetProviderInterface - { - return $this; - } - public function getDataCacheKey(DataProviderInterface $dataProvider): ?string { return $dataProvider->getWidgetId() . '_' . $dataProvider->getDisplayId(); diff --git a/lib/Widget/Provider/DurationProviderNumItemsTrait.php b/lib/Widget/Provider/DurationProviderNumItemsTrait.php new file mode 100644 index 0000000000..5840bc9c69 --- /dev/null +++ b/lib/Widget/Provider/DurationProviderNumItemsTrait.php @@ -0,0 +1,50 @@ +. + */ + +namespace Xibo\Widget\Provider; + +/** + * A trait providing the duration for widgets using numItems, durationIsPerItem and itemsPerPage + */ +trait DurationProviderNumItemsTrait +{ + public function fetchDuration(DurationProviderInterface $durationProvider): WidgetProviderInterface + { + $this->getLog()->debug('fetchDuration: DurationProviderNumItemsTrait'); + + // Take some default action to cover the majourity of region specific widgets + // Duration can depend on the number of items per page for some widgets + // this is a legacy way of working, and our preference is to use elements + $numItems = $durationProvider->getWidget()->getOptionValue('numItems', 15); + + if ($durationProvider->getWidget()->getOptionValue('durationIsPerItem', 0) == 1 && $numItems > 1) { + // If we have paging involved then work out the page count. + $itemsPerPage = $durationProvider->getWidget()->getOptionValue('itemsPerPage', 0); + if ($itemsPerPage > 0) { + $numItems = ceil($numItems / $itemsPerPage); + } + + $durationProvider->setDuration($durationProvider->getWidget()->calculatedDuration * $numItems); + } + return $this; + } +} diff --git a/lib/Widget/RssProvider.php b/lib/Widget/RssProvider.php index efee996ee6..313b372d7a 100644 --- a/lib/Widget/RssProvider.php +++ b/lib/Widget/RssProvider.php @@ -33,16 +33,17 @@ use Xibo\Support\Exception\InvalidArgumentException; use Xibo\Widget\DataType\Article; use Xibo\Widget\Provider\DataProviderInterface; -use Xibo\Widget\Provider\DurationProviderInterface; +use Xibo\Widget\Provider\DurationProviderNumItemsTrait; use Xibo\Widget\Provider\WidgetProviderInterface; use Xibo\Widget\Provider\WidgetProviderTrait; /** - * Downloads a RSS feed and returns Article data types + * Downloads an RSS feed and returns Article data types */ class RssProvider implements WidgetProviderInterface { use WidgetProviderTrait; + use DurationProviderNumItemsTrait; public function fetchData(DataProviderInterface $dataProvider): WidgetProviderInterface { @@ -225,11 +226,6 @@ public function fetchData(DataProviderInterface $dataProvider): WidgetProviderIn return $this; } - public function fetchDuration(DurationProviderInterface $durationProvider): WidgetProviderInterface - { - return $this; - } - public function getDataCacheKey(DataProviderInterface $dataProvider): ?string { // No special cache key requirements. From 13fcf96be4cf965df715ddc4a8401548143b33a2 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Sat, 5 Aug 2023 20:18:54 +0100 Subject: [PATCH 07/13] Widget: calculate itemsPerPage for save elements. relates to xibosignageltd/xibo-private#365 --- lib/Controller/Widget.php | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/Controller/Widget.php b/lib/Controller/Widget.php index dadb4aeb4e..32f86483c2 100644 --- a/lib/Controller/Widget.php +++ b/lib/Controller/Widget.php @@ -1539,8 +1539,32 @@ public function saveElements(Request $request, Response $response, $id) // Store the target regionId $widget->load(); + // Pull out elements directly from the request body + $elements = $request->getBody()->getContents(); + $elementJson = json_decode($elements, true); + if ($elementJson === null) { + throw new InvalidArgumentException(__('Invalid element JSON'), 'body'); + } + + // Parse the element JSON to see if we need to set `itemsPerPage` + $slots = []; + $uniqueSlots = 0; + foreach ($elementJson as $widgetElement) { + foreach ($widgetElement['elements'] ?? [] as $element) { + $slotNo = 'slot_' . $element['slot'] ?? 0; + if (!in_array($slotNo, $slots)) { + $slots[] = $slotNo; + $uniqueSlots++; + } + } + } + + if ($uniqueSlots > 0) { + $widget->setOptionValue('itemsPerPage', 'attrib', $uniqueSlots); + } + // Save elements - $widget->setOptionValue('elements', 'raw', $request->getBody()->getContents()); + $widget->setOptionValue('elements', 'raw', $elements); // Save $widget->save([ From 3f700190f67bcc97037645a57efa72b259daeed6 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Sat, 5 Aug 2023 20:43:47 +0100 Subject: [PATCH 08/13] Widget: fix itemsPerPage for menu boards --- lib/Widget/MenuBoardProductProvider.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/Widget/MenuBoardProductProvider.php b/lib/Widget/MenuBoardProductProvider.php index ee4c4c9041..d0788e1051 100644 --- a/lib/Widget/MenuBoardProductProvider.php +++ b/lib/Widget/MenuBoardProductProvider.php @@ -50,7 +50,14 @@ public function fetchDuration(DurationProviderInterface $durationProvider): Widg $lowerLimit = $durationProvider->getWidget()->getOptionValue('lowerLimit', 0); $upperLimit = $durationProvider->getWidget()->getOptionValue('upperLimit', 15); - $durationProvider->setDuration(($upperLimit - $lowerLimit) + $numItems = $upperLimit - $lowerLimit; + + $itemsPerPage = $durationProvider->getWidget()->getOptionValue('itemsPerPage', 0); + if ($itemsPerPage > 0) { + $numItems = ceil($numItems / $itemsPerPage); + } + + $durationProvider->setDuration(($numItems) * $durationProvider->getWidget()->calculatedDuration); return $this; } From a7af4c1d43c39fd21bddf6495d2eaeecce0b82e3 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Sun, 6 Aug 2023 13:56:08 +0100 Subject: [PATCH 09/13] Widget: fix for when slot doesn't exist (e.g. global elements) --- lib/Controller/Widget.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Controller/Widget.php b/lib/Controller/Widget.php index 32f86483c2..2c571e6208 100644 --- a/lib/Controller/Widget.php +++ b/lib/Controller/Widget.php @@ -1551,7 +1551,7 @@ public function saveElements(Request $request, Response $response, $id) $uniqueSlots = 0; foreach ($elementJson as $widgetElement) { foreach ($widgetElement['elements'] ?? [] as $element) { - $slotNo = 'slot_' . $element['slot'] ?? 0; + $slotNo = 'slot_' . ($element['slot'] ?? 0); if (!in_array($slotNo, $slots)) { $slots[] = $slotNo; $uniqueSlots++; From 209ec62e0ee8c5a0111038447b997297aae7d136 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Sun, 6 Aug 2023 16:20:08 +0100 Subject: [PATCH 10/13] Layout Import: - ignore widgets for modules which don't exist in the CMS - DataSet don't test formulas relates to xibosignageltd/xibo-private#384 --- lib/Entity/DataSet.php | 1 - lib/Entity/DataSetColumn.php | 54 ++++++++++++++++++++++++----------- lib/Factory/LayoutFactory.php | 10 +++++-- 3 files changed, 46 insertions(+), 19 deletions(-) diff --git a/lib/Entity/DataSet.php b/lib/Entity/DataSet.php index 3314431ab1..d81c7737e7 100644 --- a/lib/Entity/DataSet.php +++ b/lib/Entity/DataSet.php @@ -840,7 +840,6 @@ public function save($options = []) // Columns if ($options['saveColumns']) { foreach ($this->columns as $column) { - /* @var \Xibo\Entity\DataSetColumn $column */ $column->dataSetId = $this->dataSetId; $column->save(); } diff --git a/lib/Entity/DataSetColumn.php b/lib/Entity/DataSetColumn.php index 5ea26b1bdc..d7c26e2aa4 100644 --- a/lib/Entity/DataSetColumn.php +++ b/lib/Entity/DataSetColumn.php @@ -194,8 +194,12 @@ public function listContentArray() * Validate * @throws InvalidArgumentException */ - public function validate() + public function validate($options = []) { + $options = array_merge([ + 'testFormulas' => true, + ], $options); + if ($this->dataSetId == 0 || $this->dataSetId == '') throw new InvalidArgumentException(__('Missing dataSetId'), 'dataSetId'); @@ -273,33 +277,51 @@ public function validate() throw new InvalidArgumentException(__('New list content value is invalid as it does not include values for existing data'), 'listcontent'); } - // if formula dataSetType is set and formula is not empty, try to execute the SQL to validate it - we're ignoring client side formulas here. - if ($this->dataSetColumnTypeId == 2 && $this->formula != '' && substr($this->formula, 0, 1) !== '$') { - try { - $formula = str_replace('[DisplayId]', 0, $this->formula); - $this->getStore()->select('SELECT * FROM (SELECT `id`, ' . $formula . ' AS `' . $this->heading . '` FROM `dataset_' . $this->dataSetId . '`) dataset WHERE 1 = 1 ', []); - } catch (\Exception $e) { - $this->getLog()->debug('Formula validation failed with following message ' . $e->getMessage()); - throw new InvalidArgumentException(__('Provided formula is invalid'), 'formula'); - } + // if formula dataSetType is set and formula is not empty, try to execute the SQL to validate it - we're + // ignoring client side formulas here. + if ($options['testFormulas'] + && $this->dataSetColumnTypeId == 2 + && $this->formula != '' + && !str_starts_with($this->formula, '$') + ) { + try { + $formula = str_replace('[DisplayId]', 0, $this->formula); + $this->getStore()->select(' + SELECT * + FROM ( + SELECT `id`, ' . $formula . ' AS `' . $this->heading . '` + FROM `dataset_' . $this->dataSetId . '` + ) dataset + ', []); + } catch (\Exception $e) { + $this->getLog()->debug('Formula validation failed with following message ' . $e->getMessage()); + throw new InvalidArgumentException(__('Provided formula is invalid'), 'formula'); + } } } /** * Save - * @param array[Optional] $options + * @param array $options * @throws InvalidArgumentException */ public function save($options = []) { - $options = array_merge(['validate' => true, 'rebuilding' => false], $options); + $options = array_merge([ + 'validate' => true, + 'rebuilding' => false, + 'testFormulas' => true, + ], $options); + + if ($options['validate'] && !$options['rebuilding']) { + $this->validate($options); + } - if ($options['validate'] && !$options['rebuilding']) - $this->validate(); - if ($this->dataSetColumnId == 0) + if ($this->dataSetColumnId == 0) { $this->add(); - else + } else { $this->edit($options); + } } /** diff --git a/lib/Factory/LayoutFactory.php b/lib/Factory/LayoutFactory.php index e385da6141..3ef488ddc7 100644 --- a/lib/Factory/LayoutFactory.php +++ b/lib/Factory/LayoutFactory.php @@ -1749,6 +1749,7 @@ public function createFromZip( $existingDataSet->save([ 'activate' => false, 'notify' => false, + 'testFormulas' => false, ]); // Do we need to add data @@ -1905,8 +1906,13 @@ public function createFromZip( // We need one final pass through all widgets on the layout so that we can set the durations properly. foreach ($layout->getAllWidgets() as $widget) { - $module = $this->moduleFactory->getByType($widget->type); - $widget->calculateDuration($module, $importedFromXlf); + try { + $module = $this->moduleFactory->getByType($widget->type); + $widget->calculateDuration($module, $importedFromXlf); + } catch (NotFoundException) { + // This widget does not exist in this CMS, so we can ignore this. + $this->getLog()->error('createFromZip: widget type ' . $widget->type . ' not available in this CMS'); + } // Get global stat setting of widget to set to on/off/inherit $widget->setOptionValue('enableStat', 'attrib', $this->config->getSetting('WIDGET_STATS_ENABLED_DEFAULT')); From 308c1a009d24739d36b7f5f42f87556892902c23 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Sun, 6 Aug 2023 16:33:53 +0100 Subject: [PATCH 11/13] Layout Import: - fix heading validation message relates to xibosignageltd/xibo-private#384 --- lib/Entity/DataSetColumn.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/Entity/DataSetColumn.php b/lib/Entity/DataSetColumn.php index d7c26e2aa4..a6010e0647 100644 --- a/lib/Entity/DataSetColumn.php +++ b/lib/Entity/DataSetColumn.php @@ -212,8 +212,12 @@ public function validate($options = []) if ($this->heading == '') throw new InvalidArgumentException(__('Please provide a column heading.'), 'heading'); - if (!v::stringType()->alnum()->validate($this->heading) || strtolower($this->heading) == 'id') - throw new InvalidArgumentException(__('Please provide an alternative column heading %s can not be used.', $this->heading), 'heading'); + if (!v::stringType()->alnum()->validate($this->heading) || strtolower($this->heading) == 'id') { + throw new InvalidArgumentException(sprintf( + __('Please provide an alternative column heading %s can not be used.'), + $this->heading + ), 'heading'); + } if ($this->dataSetColumnTypeId == 2 && $this->formula == '') { throw new InvalidArgumentException(__('Please enter a valid formula'), 'formula'); From 60839b458a6fb68797eb64ac7d7dc5bcbb6e048e Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Mon, 7 Aug 2023 11:25:30 +0100 Subject: [PATCH 12/13] Layout: import fixes xibosignageltd/xibo-private#384 --- lib/Entity/DataSet.php | 2 +- lib/Entity/DataSetColumn.php | 8 +- lib/Factory/LayoutFactory.php | 106 +++++++++-------- .../DatasetWidgetCompatibility.php | 112 ++++++++++-------- modules/templates/dataset-static.xml | 5 +- 5 files changed, 128 insertions(+), 105 deletions(-) diff --git a/lib/Entity/DataSet.php b/lib/Entity/DataSet.php index d81c7737e7..6549529608 100644 --- a/lib/Entity/DataSet.php +++ b/lib/Entity/DataSet.php @@ -841,7 +841,7 @@ public function save($options = []) if ($options['saveColumns']) { foreach ($this->columns as $column) { $column->dataSetId = $this->dataSetId; - $column->save(); + $column->save($options); } } diff --git a/lib/Entity/DataSetColumn.php b/lib/Entity/DataSetColumn.php index a6010e0647..07ef620770 100644 --- a/lib/Entity/DataSetColumn.php +++ b/lib/Entity/DataSetColumn.php @@ -198,6 +198,7 @@ public function validate($options = []) { $options = array_merge([ 'testFormulas' => true, + 'allowSpacesInHeading' => false, ], $options); if ($this->dataSetId == 0 || $this->dataSetId == '') @@ -212,7 +213,11 @@ public function validate($options = []) if ($this->heading == '') throw new InvalidArgumentException(__('Please provide a column heading.'), 'heading'); - if (!v::stringType()->alnum()->validate($this->heading) || strtolower($this->heading) == 'id') { + // We allow spaces here for backwards compatibility, but only on import and edit. + $additionalCharacters = $options['allowSpacesInHeading'] ? ' ' : ''; + if (!v::stringType()->alnum($additionalCharacters)->validate($this->heading) + || strtolower($this->heading) == 'id' + ) { throw new InvalidArgumentException(sprintf( __('Please provide an alternative column heading %s can not be used.'), $this->heading @@ -314,7 +319,6 @@ public function save($options = []) $options = array_merge([ 'validate' => true, 'rebuilding' => false, - 'testFormulas' => true, ], $options); if ($options['validate'] && !$options['rebuilding']) { diff --git a/lib/Factory/LayoutFactory.php b/lib/Factory/LayoutFactory.php index 3ef488ddc7..4c07e39da6 100644 --- a/lib/Factory/LayoutFactory.php +++ b/lib/Factory/LayoutFactory.php @@ -643,9 +643,8 @@ public function getLinkedFullScreenPlaylistId(int $campaignId): ?int * Load a layout by its XLF * @param string $layoutXlf * @param null $layout - * @return Layout - * @throws InvalidArgumentException - * @throws NotFoundException + * @return \Xibo\Entity\Layout + * @throws \Xibo\Support\Exception\GeneralException */ public function loadByXlf($layoutXlf, $layout = null) { @@ -656,9 +655,6 @@ public function loadByXlf($layoutXlf, $layout = null) $layout = $this->createEmpty(); } - // Get a list of modules for us to use - $modules = $this->moduleFactory->getKeyedArrayOfModules(); - // Parse the XML and fill in the details for this layout $document = new \DOMDocument(); if ($document->loadXML($layoutXlf) === false) { @@ -749,21 +745,6 @@ public function loadByXlf($layoutXlf, $layout = null) $this->setWidgetExpiryDatesOrDefault($widget); - $this->getLog()->debug('Adding Widget to object model. ' . $widget); - - // Does this module type exist? - if (!array_key_exists($widget->type, $modules)) { - $this->getLog()->error(sprintf( - 'Module Type [%s] in imported Layout does not exist. Allowable types: %s', - $widget->type, - json_encode(array_keys($modules)) - )); - continue; - } - - $module = $modules[$widget->type]; - /* @var \Xibo\Entity\Module $module */ - // // Get all widget options // @@ -785,7 +766,6 @@ public function loadByXlf($layoutXlf, $layout = null) && $widgetOption->value == '2' ) { $widget->type = 'datasetticker'; - $module = $modules[$widget->type]; } } } @@ -796,6 +776,15 @@ public function loadByXlf($layoutXlf, $layout = null) $xpathQuery )); + // Check legacy types from conditions, set widget type and upgrade + try { + $module = $this->prepareWidgetAndGetModule($widget); + } catch (NotFoundException) { + // Skip this widget + $this->getLog()->info('loadByJson: ' . $widget->type . ' could not be found or resolved'); + continue; + } + // // Get the MediaId associated with this widget (using the URI) // @@ -928,8 +917,6 @@ public function loadByJson($layoutJson, $playlistJson, $nestedPlaylistJson, Fold $oldIds = []; $newIds = []; $widgets = []; - // Get a list of modules for us to use - $modules = $this->moduleFactory->getKeyedArrayOfModules(); $layout->schemaVersion = (int)$layoutJson['layoutDefinitions']['schemaVersion']; $layout->width = $layoutJson['layoutDefinitions']['width']; @@ -1094,8 +1081,23 @@ public function loadByJson($layoutJson, $playlistJson, $nestedPlaylistJson, Fold $this->getLog()->debug('Adding Widget to object model. ' . $widget); - // Prepare widget options, check legacy types from conditions, set widget type and upgrade - $module = $this->prepareWidgetAndGetModule($widget, $mediaNode['widgetOptions']); + // Prepare widget options + foreach ($mediaNode['widgetOptions'] as $optionsNode) { + $widgetOption = $this->widgetOptionFactory->createEmpty(); + $widgetOption->type = $optionsNode['type']; + $widgetOption->option = $optionsNode['option']; + $widgetOption->value = $optionsNode['value']; + $widget->widgetOptions[] = $widgetOption; + } + + // Resolve the module + try { + $module = $this->prepareWidgetAndGetModule($widget); + } catch (NotFoundException) { + // Skip this widget + $this->getLog()->info('loadByJson: ' . $widget->type . ' could not be found or resolved'); + continue; + } // // Get the MediaId associated with this widget @@ -1445,7 +1447,7 @@ public function createFromZip( $this->getLog()->error('Skipping file on import due to invalid filename. ' . $fileName); continue; } - + $temporaryFileName = $libraryLocationTemp . $fileName; // Get the file from the ZIP @@ -1750,6 +1752,7 @@ public function createFromZip( 'activate' => false, 'notify' => false, 'testFormulas' => false, + 'allowSpacesInHeading' => true, ]); // Do we need to add data @@ -1760,7 +1763,7 @@ public function createFromZip( $existingDataSet->dataSetId )); - foreach ($item['data'] as $itemData) { + foreach (($item['data'] ?? []) as $itemData) { if (isset($itemData['id'])) { unset($itemData['id']); } @@ -1906,13 +1909,9 @@ public function createFromZip( // We need one final pass through all widgets on the layout so that we can set the durations properly. foreach ($layout->getAllWidgets() as $widget) { - try { - $module = $this->moduleFactory->getByType($widget->type); - $widget->calculateDuration($module, $importedFromXlf); - } catch (NotFoundException) { - // This widget does not exist in this CMS, so we can ignore this. - $this->getLog()->error('createFromZip: widget type ' . $widget->type . ' not available in this CMS'); - } + // By now we should not have any modules which don't exist. + $module = $this->moduleFactory->getByType($widget->type); + $widget->calculateDuration($module, $importedFromXlf); // Get global stat setting of widget to set to on/off/inherit $widget->setOptionValue('enableStat', 'attrib', $this->config->getSetting('WIDGET_STATS_ENABLED_DEFAULT')); @@ -1962,10 +1961,27 @@ public function createNestedPlaylistWidgets($widgets, $combined, &$playlists) $playlistWidget->tempId = $widgetsDetail['tempId']; $playlistWidget->mediaIds = $widgetsDetail['mediaIds']; $playlistWidget->widgetOptions = []; - $playlistWidget->schemaVersion = isset($widgetsDetail['schemaVersion']) ? (int)$widgetsDetail['schemaVersion'] : 1; + $playlistWidget->schemaVersion = isset($widgetsDetail['schemaVersion']) + ? (int)$widgetsDetail['schemaVersion'] + : 1; + + // Prepare widget options + foreach ($widgetsDetail['widgetOptions'] as $optionsNode) { + $widgetOption = $this->widgetOptionFactory->createEmpty(); + $widgetOption->type = $optionsNode['type']; + $widgetOption->option = $optionsNode['option']; + $widgetOption->value = $optionsNode['value']; + $playlistWidget->widgetOptions[] = $widgetOption; + } - // Prepare widget options, check legacy types from conditions, set widget type and upgrade - $module = $this->prepareWidgetAndGetModule($playlistWidget, $widgetsDetail['widgetOptions']); + try { + $module = $this->prepareWidgetAndGetModule($playlistWidget); + } catch (NotFoundException) { + // Skip this widget + $this->getLog()->info('createNestedPlaylistWidgets: ' . $playlistWidget->type + . ' could not be found or resolved'); + continue; + } if ($playlistWidget->type == 'subplaylist') { // Get the subplaylists from widget option @@ -2937,17 +2953,8 @@ public function convertOldPlaylistOptions($playlistIds, $playlistOptions) * Prepare widget options, check legacy types from conditions, set widget type and upgrade * @throws NotFoundException */ - private function prepareWidgetAndGetModule(Widget $widget, array $widgetOptions): Module + private function prepareWidgetAndGetModule(Widget $widget): Module { - // Get all widget options - foreach ($widgetOptions as $optionsNode) { - $widgetOption = $this->widgetOptionFactory->createEmpty(); - $widgetOption->type = $optionsNode['type']; - $widgetOption->option = $optionsNode['option']; - $widgetOption->value = $optionsNode['value']; - $widget->widgetOptions[] = $widgetOption; - } - // Form conditions from the widget's option and value, e.g, templateId==worldclock1 $widgetConditionMatch = []; foreach ($widget->widgetOptions as $option) { @@ -2961,7 +2968,8 @@ private function prepareWidgetAndGetModule(Widget $widget, array $widgetOptions) throw new NotFoundException(__('Module not found')); } - // Set the widget type + // Set the widget type and then assert the new one + $widget->setOriginalValue('type', $widget->type); $widget->type = $module->type; // Upgrade if necessary diff --git a/lib/Widget/Compatibility/DatasetWidgetCompatibility.php b/lib/Widget/Compatibility/DatasetWidgetCompatibility.php index 0470191fc9..3699de0562 100644 --- a/lib/Widget/Compatibility/DatasetWidgetCompatibility.php +++ b/lib/Widget/Compatibility/DatasetWidgetCompatibility.php @@ -39,65 +39,73 @@ public function upgradeWidget(Widget $widget, int $fromSchema, int $toSchema): b { $this->getLog()->debug('upgradeWidget: '. $widget->getId(). ' from: '. $fromSchema.' to: '.$toSchema); + // Track if we've been upgraded. $upgraded = false; - $newTemplateId = null; - $overrideTemplate = $widget->getOptionValue('overrideTemplate', 0); - $templateId = $widget->getOptionValue('templateId', ''); - - foreach ($widget->widgetOptions as $option) { - if ($option->option === 'templateId') { - if ($overrideTemplate == 0) { - switch ($templateId) { - case 'empty': - $newTemplateId = 'dataset_table_1'; - break; - - case 'light-green': - $newTemplateId = 'dataset_table_2'; - break; - - case 'simple-round': - $newTemplateId = 'dataset_table_3'; - break; - - case 'transparent-blue': - $newTemplateId = 'dataset_table_4'; - break; - - case 'orange-grey-striped': - $newTemplateId = 'dataset_table_5'; - break; - - case 'split-rows': - $newTemplateId = 'dataset_table_6'; - break; - - case 'dark-round': - $newTemplateId = 'dataset_table_7'; - break; - - case 'pill-colored': - $newTemplateId = 'dataset_table_8'; - break; - - default: - break; + + // Did we originally come from a data set ticker? + if ($widget->getOriginalValue('type') === 'datasetticker') { + $newTemplateId = 'dataset_custom_html'; + $widget->changeOption('css', 'styleSheet'); + } else { + $newTemplateId = null; + $overrideTemplate = $widget->getOptionValue('overrideTemplate', 0); + $templateId = $widget->getOptionValue('templateId', ''); + + foreach ($widget->widgetOptions as $option) { + if ($option->option === 'templateId') { + if ($overrideTemplate == 0) { + switch ($templateId) { + case 'empty': + $newTemplateId = 'dataset_table_1'; + break; + + case 'light-green': + $newTemplateId = 'dataset_table_2'; + break; + + case 'simple-round': + $newTemplateId = 'dataset_table_3'; + break; + + case 'transparent-blue': + $newTemplateId = 'dataset_table_4'; + break; + + case 'orange-grey-striped': + $newTemplateId = 'dataset_table_5'; + break; + + case 'split-rows': + $newTemplateId = 'dataset_table_6'; + break; + + case 'dark-round': + $newTemplateId = 'dataset_table_7'; + break; + + case 'pill-colored': + $newTemplateId = 'dataset_table_8'; + break; + + default: + break; + } + } else { + $newTemplateId = 'dataset_table_custom_html'; } - } else { - $newTemplateId = 'dataset_table_custom_html'; } + } - if (!empty($newTemplateId)) { - $widget->setOptionValue('templateId', 'attrib', $newTemplateId); - $upgraded = true; - } + // We have changed the format of columns to be an array in v4. + $columns = $widget->getOptionValue('columns', ''); + if (!empty($columns)) { + $widget->setOptionValue('columns', 'attrib', '[' . $columns . ']'); + $upgraded = true; } } - // We have changed the format of columns to be an array in v4. - $columns = $widget->getOptionValue('columns', ''); - if (!empty($columns)) { - $widget->setOptionValue('columns', 'attrib', '[' . $columns . ']'); + if (!empty($newTemplateId)) { + $widget->setOptionValue('templateId', 'attrib', $newTemplateId); $upgraded = true; } diff --git a/modules/templates/dataset-static.xml b/modules/templates/dataset-static.xml index e5f269b365..ce0384eb8c 100644 --- a/modules/templates/dataset-static.xml +++ b/modules/templates/dataset-static.xml @@ -2490,8 +2490,11 @@ $datasetTableContainer.dataSetRender(properties); dataset none + + Main Template + - Sample sheet + Optional Stylesheet Template From f1c97ee263849b030ad95849cf7d7b4c30da6752 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Mon, 7 Aug 2023 11:38:55 +0100 Subject: [PATCH 13/13] Layout: sortable search --- lib/Controller/Library.php | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/Controller/Library.php b/lib/Controller/Library.php index 8eef10001f..a4c6c2206b 100644 --- a/lib/Controller/Library.php +++ b/lib/Controller/Library.php @@ -804,15 +804,32 @@ public function grid(Request $request, Response $response) * @throws GeneralException * @throws NotFoundException */ - public function search(Request $request, Response $response) + public function search(Request $request, Response $response): Response { $parsedQueryParams = $this->getSanitizer($request->getQueryParams()); $provider = $parsedQueryParams->getString('provider', ['default' => 'both']); $searchResults = new SearchResults(); if ($provider === 'both' || $provider === 'local') { - // Construct the SQL - $mediaList = $this->mediaFactory->query(['media.name'], $this->gridRenderFilter([ + // Sorting options. + // only allow from a preset list + $sortCol = match ($parsedQueryParams->getString('sortCol')) { + 'mediaId' => '`media`.`mediaId`', + 'orientation' => '`media`.`orientation`', + 'width' => '`media`.`width`', + 'height' => '`media`.`height`', + 'duration' => '`media`.`duration`', + 'fileSize' => '`media`.`fileSize`', + 'createdDt' => '`media`.`createdDt`', + 'modifiedDt' => '`media`.`modifiedDt`', + default => '`media`.`name`', + }; + $sortDir = match ($parsedQueryParams->getString('sortDir')) { + 'DESC' => ' DESC', + default => ' ASC' + }; + + $mediaList = $this->mediaFactory->query([$sortCol . $sortDir], $this->gridRenderFilter([ 'name' => $parsedQueryParams->getString('media'), 'useRegexForName' => $parsedQueryParams->getCheckbox('useRegexForName'), 'nameExact' => $parsedQueryParams->getString('nameExact'),