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'),
diff --git a/lib/Controller/Widget.php b/lib/Controller/Widget.php
index dadb4aeb4e..2c571e6208 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([
diff --git a/lib/Entity/DataSet.php b/lib/Entity/DataSet.php
index 3314431ab1..6549529608 100644
--- a/lib/Entity/DataSet.php
+++ b/lib/Entity/DataSet.php
@@ -840,9 +840,8 @@ public function save($options = [])
// Columns
if ($options['saveColumns']) {
foreach ($this->columns as $column) {
- /* @var \Xibo\Entity\DataSetColumn $column */
$column->dataSetId = $this->dataSetId;
- $column->save();
+ $column->save($options);
}
}
diff --git a/lib/Entity/DataSetColumn.php b/lib/Entity/DataSetColumn.php
index 5ea26b1bdc..07ef620770 100644
--- a/lib/Entity/DataSetColumn.php
+++ b/lib/Entity/DataSetColumn.php
@@ -194,8 +194,13 @@ public function listContentArray()
* Validate
* @throws InvalidArgumentException
*/
- public function validate()
+ public function validate($options = [])
{
+ $options = array_merge([
+ 'testFormulas' => true,
+ 'allowSpacesInHeading' => false,
+ ], $options);
+
if ($this->dataSetId == 0 || $this->dataSetId == '')
throw new InvalidArgumentException(__('Missing dataSetId'), 'dataSetId');
@@ -208,8 +213,16 @@ public function validate()
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');
+ // 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
+ ), 'heading');
+ }
if ($this->dataSetColumnTypeId == 2 && $this->formula == '') {
throw new InvalidArgumentException(__('Please enter a valid formula'), 'formula');
@@ -273,33 +286,50 @@ 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,
+ ], $options);
- if ($options['validate'] && !$options['rebuilding'])
- $this->validate();
- if ($this->dataSetColumnId == 0)
+ if ($options['validate'] && !$options['rebuilding']) {
+ $this->validate($options);
+ }
+
+ if ($this->dataSetColumnId == 0) {
$this->add();
- else
+ } else {
$this->edit($options);
+ }
}
/**
diff --git a/lib/Entity/Module.php b/lib/Entity/Module.php
index a5f32cce4b..4dac4a35d7 100644
--- a/lib/Entity/Module.php
+++ b/lib/Entity/Module.php
@@ -354,13 +354,52 @@ 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);
}
- $durationProvider = $this->moduleFactory->createDurationProvider($file, null);
+ }
+
+ /**
+ * 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 {
+ return null;
+ }
+ } else if ($this->widgetProvider === null) {
+ return null;
+ }
+
+ $this->getLog()->debug('calculateDuration: using widget provider');
+
+ $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/LayoutFactory.php b/lib/Factory/LayoutFactory.php
index e385da6141..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
@@ -1749,6 +1751,8 @@ public function createFromZip(
$existingDataSet->save([
'activate' => false,
'notify' => false,
+ 'testFormulas' => false,
+ 'allowSpacesInHeading' => true,
]);
// Do we need to add data
@@ -1759,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']);
}
@@ -1905,6 +1909,7 @@ 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) {
+ // By now we should not have any modules which don't exist.
$module = $this->moduleFactory->getByType($widget->type);
$widget->calculateDuration($module, $importedFromXlf);
@@ -1956,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
@@ -2931,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) {
@@ -2955,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/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/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 @@
registerWithDispatcher($dispatcher);
(new MenuBoardProviderListener(
- $c->get('menuBoardCategoryFactory')
+ $c->get('menuBoardFactory'),
+ $c->get('menuBoardCategoryFactory'),
))
->useLogger($c->get('logger'))
->registerWithDispatcher($dispatcher);
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'])
+ );
}
}
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/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/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/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/MenuBoardProductProvider.php b/lib/Widget/MenuBoardProductProvider.php
index 101ba296d7..d0788e1051 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,19 @@ 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);
+ $numItems = $upperLimit - $lowerLimit;
+
+ $itemsPerPage = $durationProvider->getWidget()->getOptionValue('itemsPerPage', 0);
+ if ($itemsPerPage > 0) {
+ $numItems = ceil($numItems / $itemsPerPage);
+ }
+
+ $durationProvider->setDuration(($numItems)
+ * $durationProvider->getWidget()->calculatedDuration);
return $this;
}
@@ -55,6 +69,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/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/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/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.
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
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
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,