init commit

This commit is contained in:
Matt Batchelder
2026-02-11 20:55:38 -05:00
commit 6e3929c459
2240 changed files with 467828 additions and 0 deletions

View File

@@ -0,0 +1,68 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget;
use Carbon\Carbon;
use Xibo\Support\Exception\NotFoundException;
use Xibo\Widget\Provider\DataProviderInterface;
use Xibo\Widget\Provider\DurationProviderInterface;
use Xibo\Widget\Provider\WidgetProviderInterface;
use Xibo\Widget\Provider\WidgetProviderTrait;
/**
* Handles setting the correct audio duration.
*/
class AudioProvider implements WidgetProviderInterface
{
use WidgetProviderTrait;
public function fetchData(DataProviderInterface $dataProvider): WidgetProviderInterface
{
return $this;
}
public function fetchDuration(DurationProviderInterface $durationProvider): WidgetProviderInterface
{
// 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;
}
public function getDataCacheKey(DataProviderInterface $dataProvider): ?string
{
// No special cache key requirements.
return null;
}
public function getDataModifiedDt(DataProviderInterface $dataProvider): ?Carbon
{
return null;
}
}

View File

@@ -0,0 +1,76 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Compatibility;
use Xibo\Entity\Widget;
use Xibo\Widget\Provider\WidgetCompatibilityInterface;
use Xibo\Widget\Provider\WidgetCompatibilityTrait;
/**
* Convert a v3 calendar or calendaradvanced widget to its v4 counterpart.
*/
class CalendarWidgetCompatibility implements WidgetCompatibilityInterface
{
use WidgetCompatibilityTrait;
/** @inheritDoc */
public function upgradeWidget(Widget $widget, int $fromSchema, int $toSchema): bool
{
$this->getLog()->debug('upgradeWidget: ' . $widget->getId() . ' from: ' . $fromSchema . ' to: ' . $toSchema);
// Track if we've been upgraded.
$upgraded = false;
// Did we originally come from an agenda (the old calendar widget)
if ($widget->getOriginalValue('type') === 'calendar') {
$newTemplateId = 'event_custom_html';
// New options names.
$widget->changeOption('template', 'text');
} else {
// We are a calendaradvanced
// Calendar type is either 1=schedule, 2=daily, 3=weekly or 4=monthly.
$newTemplateId = match ($widget->getOptionValue('calendarType', 1)) {
2 => 'daily',
3 => 'weekly',
4 => 'monthly',
default => 'schedule',
};
// Apply the theme
$newTemplateId .= '_' . $widget->getOptionValue('templateTheme', 'light');
}
if (!empty($newTemplateId)) {
$widget->setOptionValue('templateId', 'attrib', $newTemplateId);
$upgraded = true;
}
return $upgraded;
}
public function saveTemplate(string $template, string $fileName): bool
{
return false;
}
}

View File

@@ -0,0 +1,66 @@
<?php
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Compatibility;
use Xibo\Entity\Widget;
use Xibo\Widget\Provider\WidgetCompatibilityInterface;
use Xibo\Widget\Provider\WidgetCompatibilityTrait;
/**
* Convert widget from an old schema to a new schema
*/
class ClockWidgetCompatibility implements WidgetCompatibilityInterface
{
use WidgetCompatibilityTrait;
/** @inheritdoc
*/
public function upgradeWidget(Widget $widget, int $fromSchema, int $toSchema): bool
{
$this->getLog()->debug('upgradeWidget: ' . $widget->getId() . ' from: ' . $fromSchema . ' to: ' . $toSchema);
// The old clock widget had a `clockTypeId` option which determines the template
// we must make a choice here.
$widget->type = match ($widget->getOptionValue('clockTypeId', 1)) {
2 => 'clock-digital',
3 => 'clock-flip',
default => 'clock-analogue',
};
// in v3 this option used to ba called theme, now it is themeId
if ($widget->type === 'clock-analogue') {
$widget->setOptionValue('themeId', 'attrib', $widget->getOptionValue('theme', 1));
}
// We don't need the old options anymore
$widget->removeOption('clockTypeId');
$widget->removeOption('theme');
return true;
}
public function saveTemplate(string $template, string $fileName): bool
{
return false;
}
}

View File

@@ -0,0 +1,71 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Compatibility;
use Xibo\Entity\Widget;
use Xibo\Widget\Provider\WidgetCompatibilityInterface;
use Xibo\Widget\Provider\WidgetCompatibilityTrait;
/**
* Convert widget from an old schema to a new schema
*/
class CountDownWidgetCompatibility implements WidgetCompatibilityInterface
{
use WidgetCompatibilityTrait;
/** @inheritdoc
*/
public function upgradeWidget(Widget $widget, int $fromSchema, int $toSchema): bool
{
$this->getLog()->debug('upgradeWidget: ' . $widget->getId() . ' from: ' . $fromSchema . ' to: ' . $toSchema);
$countdownType = $widget->getOptionValue('countdownType', 1);
$overrideTemplate = $widget->getOptionValue('overrideTemplate', 0);
// Old countdown had countdownType.
if ($overrideTemplate == 1) {
$widget->type = 'countdown-custom';
} else {
$widget->type = match ($countdownType) {
2 => 'countdown-clock',
3 => 'countdown-table',
4 => 'countdown-days',
default => 'countdown-text',
};
}
// If overriden, we need to tranlate the legacy options to the new values
if ($overrideTemplate == 1) {
$widget->changeOption('widgetOriginalWidth', 'widgetDesignWidth');
$widget->changeOption('widgetOriginalHeight', 'widgetDesignHeight');
$widget->removeOption('templateId');
}
return true;
}
public function saveTemplate(string $template, string $fileName): bool
{
return false;
}
}

View File

@@ -0,0 +1,66 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Compatibility;
use Xibo\Entity\Widget;
use Xibo\Widget\Provider\WidgetCompatibilityInterface;
use Xibo\Widget\Provider\WidgetCompatibilityTrait;
/**
* Convert widget from an old schema to a new schema
*/
class CurrenciesWidgetCompatibility implements WidgetCompatibilityInterface
{
use WidgetCompatibilityTrait;
/** @inheritdoc
*/
public function upgradeWidget(Widget $widget, int $fromSchema, int $toSchema): bool
{
$this->getLog()->debug('upgradeWidget: ' . $widget->getId() . ' from: ' . $fromSchema . ' to: ' . $toSchema);
$upgraded = false;
$overrideTemplate = $widget->getOptionValue('overrideTemplate', 0);
// If the widget has override template
if ($overrideTemplate == 1) {
$widget->setOptionValue('templateId', 'attrib', 'currencies_custom_html');
$upgraded = true;
// We need to tranlate the legacy options to the new values
$widget->changeOption('widgetOriginalWidth', 'widgetDesignWidth');
$widget->changeOption('widgetOriginalHeight', 'widgetDesignHeight');
}
// We need to change duration per page to duration per item
$widget->changeOption('durationIsPerPage', 'durationIsPerItem');
return $upgraded;
}
public function saveTemplate(string $template, string $fileName): bool
{
return false;
}
}

View File

@@ -0,0 +1,78 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Compatibility;
use Xibo\Entity\Widget;
use Xibo\Widget\Provider\WidgetCompatibilityInterface;
use Xibo\Widget\Provider\WidgetCompatibilityTrait;
/**
* Convert widget from an old schema to a new schema
*/
class DatasetWidgetCompatibility implements WidgetCompatibilityInterface
{
use WidgetCompatibilityTrait;
/** @inheritdoc
*/
public function upgradeWidget(Widget $widget, int $fromSchema, int $toSchema): bool
{
$this->getLog()->debug('upgradeWidget: ' . $widget->getId() . ' from: ' . $fromSchema . ' to: ' . $toSchema);
// Did we originally come from a dataset ticker?
if ($widget->getOriginalValue('type') === 'datasetticker') {
$newTemplateId = 'dataset_custom_html';
$widget->changeOption('css', 'styleSheet');
} else {
if ($widget->getOptionValue('overrideTemplate', 0) == 0) {
$newTemplateId = match ($widget->getOptionValue('templateId', '')) {
'light-green' => 'dataset_table_2',
'simple-round' => 'dataset_table_3',
'transparent-blue' => 'dataset_table_4',
'orange-grey-striped' => 'dataset_table_5',
'split-rows' => 'dataset_table_6',
'dark-round' => 'dataset_table_7',
'pill-colored' => 'dataset_table_8',
default => 'dataset_table_1',
};
} else {
$newTemplateId = 'dataset_table_custom_html';
}
// 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 . ']');
}
}
$widget->setOptionValue('templateId', 'attrib', $newTemplateId);
return true;
}
public function saveTemplate(string $template, string $fileName): bool
{
return false;
}
}

View File

@@ -0,0 +1,52 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Compatibility;
use Xibo\Entity\Widget;
use Xibo\Widget\Provider\WidgetCompatibilityInterface;
use Xibo\Widget\Provider\WidgetCompatibilityTrait;
class NotificationViewCompatibility implements WidgetCompatibilityInterface
{
use WidgetCompatibilityTrait;
public function upgradeWidget(Widget $widget, int $fromSchema, int $toSchema): bool
{
$this->getLog()->debug('upgradeWidget: ' . $widget->getId() . ' from: ' . $fromSchema . ' to: ' . $toSchema);
$upgraded = false;
if ($fromSchema <= 1) {
// Add a templateId.
$widget->setOptionValue('templateId', 'attrib', 'message_custom_html');
$upgraded = true;
}
return $upgraded;
}
public function saveTemplate(string $template, string $fileName): bool
{
return false;
}
}

View File

@@ -0,0 +1,157 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Compatibility;
use Xibo\Entity\Widget;
use Xibo\Widget\Provider\WidgetCompatibilityInterface;
use Xibo\Widget\Provider\WidgetCompatibilityTrait;
/**
* Convert RSS old kebab-case properties to camelCase
*/
class RssWidgetCompatibility implements WidgetCompatibilityInterface
{
use WidgetCompatibilityTrait;
/** @inheritdoc
*/
public function upgradeWidget(Widget $widget, int $fromSchema, int $toSchema): bool
{
$this->getLog()->debug('upgradeWidget: ' . $widget->getId() . ' from: ' . $fromSchema . ' to: ' . $toSchema);
// Decode URL (always make sure we save URLs decoded)
$widget->setOptionValue('uri', 'attrib', urldecode($widget->getOptionValue('uri', '')));
// Swap to new template names.
$overrideTemplate = $widget->getOptionValue('overrideTemplate', 0);
if ($overrideTemplate) {
$newTemplateId = 'article_custom_html';
} else {
$newTemplateId = match ($widget->getOptionValue('templateId', '')) {
'media-rss-image-only' => 'article_image_only',
'media-rss-with-left-hand-text' => 'article_with_left_hand_text',
'media-rss-with-title' => 'article_with_title',
'prominent-title-with-desc-and-name-separator' => 'article_with_desc_and_name_separator',
default => 'article_title_only',
};
// If template id is "article_with_desc_and_name_separator"
// set showSideBySide to 1 to replicate behaviour in v3 for marquee
$effect = $widget->getOptionValue('effect', null);
if (
$newTemplateId === 'article_with_desc_and_name_separator' &&
$effect === 'marqueeLeft' ||
$effect === 'marqueeRight' ||
$effect === 'marqueeUp' ||
$effect === 'marqueeDown'
) {
$widget->setOptionValue('showSideBySide', 'attrib', 1);
}
}
$widget->setOptionValue('templateId', 'attrib', $newTemplateId);
// If the new templateId is custom, we need to parse the old template for image enclosures
if ($newTemplateId === 'article_custom_html') {
$template = $widget->getOptionValue('template', null);
if (!empty($template)) {
$modified = false;
$matches = [];
preg_match_all('/\[(.*?)\]/', $template, $matches);
for ($i = 0; $i < count($matches[1]); $i++) {
// We have a [Link] or a [xxx|image] tag
$match = $matches[1][$i];
if ($match === 'Link' || $match === 'Link|image') {
// This is a straight-up enclosure (which is the default).
$template = str_replace($matches[0][$i], '<img src="[image]" alt="Image" />', $template);
$modified = true;
} else if (str_contains($match, '|image')) {
// [tag|image|attribute]
// Set the necessary options depending on how our tag is made up
$parts = explode('|', $match);
$tag = $parts[0];
$attribute = $parts[2] ?? null;
$widget->setOptionValue('imageSource', 'attrib', 'custom');
$widget->setOptionValue('imageSourceTag', 'attrib', $tag);
if (!empty($attribute)) {
$widget->setOptionValue('imageSourceAttribute', 'attrib', $attribute);
}
$template = str_replace($matches[0][$i], '<img src="[image]" alt="Image"/>', $template);
$modified = true;
}
}
if ($modified) {
$widget->setOptionValue('template', 'cdata', $template);
}
}
}
// Change some other options if they have been set.
foreach ($widget->widgetOptions as $option) {
$widgetChangeOption = null;
switch ($option->option) {
case 'background-color':
$widgetChangeOption = 'itemBackgroundColor';
break;
case 'title-color':
$widgetChangeOption = 'itemTitleColor';
break;
case 'name-color':
$widgetChangeOption = 'itemNameColor';
break;
case 'description-color':
$widgetChangeOption = 'itemDescriptionColor';
break;
case 'font-size':
$widgetChangeOption = 'itemFontSize';
break;
case 'image-fit':
$widgetChangeOption = 'itemImageFit';
break;
default:
break;
}
if (!empty($widgetChangeOption)) {
$widget->changeOption($option->option, $widgetChangeOption);
}
}
return true;
}
public function saveTemplate(string $template, string $fileName): bool
{
return false;
}
}

View File

@@ -0,0 +1,77 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Compatibility;
use Xibo\Entity\Widget;
use Xibo\Widget\Provider\WidgetCompatibilityInterface;
use Xibo\Widget\Provider\WidgetCompatibilityTrait;
/**
* Convert widget from an old schema to a new schema
*/
class SocialMediaWidgetCompatibility implements WidgetCompatibilityInterface
{
use WidgetCompatibilityTrait;
/** @inheritdoc
*/
public function upgradeWidget(Widget $widget, int $fromSchema, int $toSchema): bool
{
$this->getLog()->debug('upgradeWidget: ' . $widget->getId() . ' from: ' . $fromSchema . ' to: ' . $toSchema);
$overrideTemplate = $widget->getOptionValue('overrideTemplate', 0);
if ($overrideTemplate == 1) {
$newTemplateId = 'social_media_custom_html';
} else {
$newTemplateId = match ($widget->getOptionValue('templateId', '')) {
'full-timeline-np' => 'social_media_static_1',
'full-timeline' => 'social_media_static_2',
'tweet-with-profileimage-left' => 'social_media_static_4',
'tweet-with-profileimage-right' => 'social_media_static_5',
'tweet-1' => 'social_media_static_6',
'tweet-2' => 'social_media_static_7',
'tweet-4' => 'social_media_static_8',
'tweet-6NP' => 'social_media_static_9',
'tweet-6PL' => 'social_media_static_10',
'tweet-7' => 'social_media_static_11',
'tweet-8' => 'social_media_static_12',
default => 'social_media_static_3',
};
}
$widget->setOptionValue('templateId', 'attrib', $newTemplateId);
// If overriden, we need to tranlate the legacy options to the new values
if ($overrideTemplate == 1) {
$widget->changeOption('widgetOriginalWidth', 'widgetDesignWidth');
$widget->changeOption('widgetOriginalHeight', 'widgetDesignHeight');
$widget->changeOption('widgetOriginalPadding', 'widgetDesignGap');
}
return true;
}
public function saveTemplate(string $template, string $fileName): bool
{
return false;
}
}

View File

@@ -0,0 +1,66 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Compatibility;
use Xibo\Entity\Widget;
use Xibo\Widget\Provider\WidgetCompatibilityInterface;
use Xibo\Widget\Provider\WidgetCompatibilityTrait;
/**
* Convert widget from an old schema to a new schema
*/
class StocksWidgetCompatibility implements WidgetCompatibilityInterface
{
use WidgetCompatibilityTrait;
/** @inheritdoc
*/
public function upgradeWidget(Widget $widget, int $fromSchema, int $toSchema): bool
{
$this->getLog()->debug('upgradeWidget: ' . $widget->getId() . ' from: ' . $fromSchema . ' to: ' . $toSchema);
$upgraded = false;
$overrideTemplate = $widget->getOptionValue('overrideTemplate', 0);
// If the widget has override template
if ($overrideTemplate == 1) {
$widget->setOptionValue('templateId', 'attrib', 'stocks_custom_html');
$upgraded = true;
// We need to tranlate the legacy options to the new values
$widget->changeOption('widgetOriginalWidth', 'widgetDesignWidth');
$widget->changeOption('widgetOriginalHeight', 'widgetDesignHeight');
}
// We need to change duration per page to duration per item
$widget->changeOption('durationIsPerPage', 'durationIsPerItem');
return $upgraded;
}
public function saveTemplate(string $template, string $fileName): bool
{
return false;
}
}

View File

@@ -0,0 +1,109 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Compatibility;
use Xibo\Entity\Widget;
use Xibo\Widget\Provider\WidgetCompatibilityInterface;
use Xibo\Widget\Provider\WidgetCompatibilityTrait;
use Xibo\Widget\SubPlaylistItem;
/**
* Convert widget from an old schema to a new schema
*/
class SubPlaylistWidgetCompatibility implements WidgetCompatibilityInterface
{
use WidgetCompatibilityTrait;
/** @inheritdoc
*/
public function upgradeWidget(Widget $widget, int $fromSchema, int $toSchema): bool
{
$this->getLog()->debug('upgradeWidget: ' . $widget->getId() . ' from: ' . $fromSchema . ' to: ' . $toSchema);
$upgraded = false;
$playlists = [];
$playlistIds = [];
// subPlaylistOptions and subPlaylistIds are no longer in use from 2.3
// we need to capture these options to support Layout with sub-playlist import from older CMS
foreach ($widget->widgetOptions as $option) {
if ($option->option === 'subPlaylists') {
$playlists = json_decode($widget->getOptionValue('subPlaylists', '[]'), true);
}
if ($option->option === 'subPlaylistIds') {
$playlistIds = json_decode($widget->getOptionValue('subPlaylistIds', '[]'), true);
}
if ($option->option === 'subPlaylistOptions') {
$subPlaylistOptions = json_decode($widget->getOptionValue('subPlaylistOptions', '[]'), true);
}
}
if (count($playlists) <= 0) {
$i = 0;
foreach ($playlistIds as $playlistId) {
$i++;
$playlists[] = [
'rowNo' => $i,
'playlistId' => $playlistId,
'spotFill' => $subPlaylistOptions[$playlistId]['subPlaylistIdSpotFill'] ?? null,
'spotLength' => $subPlaylistOptions[$playlistId]['subPlaylistIdSpotLength'] ?? null,
'spots' => $subPlaylistOptions[$playlistId]['subPlaylistIdSpots'] ?? null,
];
}
$playlistItems = [];
foreach ($playlists as $playlist) {
$item = new SubPlaylistItem();
$item->rowNo = intval($playlist['rowNo']);
$item->playlistId = $playlist['playlistId'];
$item->spotFill = $playlist['spotFill'] ?? '';
$item->spotLength = $playlist['spotLength'] !== '' ? intval($playlist['spotLength']) : '';
$item->spots = $playlist['spots'] !== '' ? intval($playlist['spots']) : '';
$playlistItems[] = $item;
}
if (count($playlistItems) > 0) {
$widget->setOptionValue('subPlaylists', 'attrib', json_encode($playlistItems));
$widget->removeOption('subPlaylistIds');
$widget->removeOption('subPlaylistOptions');
$upgraded = true;
}
} else {
$this->getLog()->debug(
'upgradeWidget : subplaylist ' . $widget->widgetId .
' with already updated widget options, save to update schema version'
);
$upgraded = true;
}
return $upgraded;
}
public function saveTemplate(string $template, string $fileName): bool
{
return false;
}
}

View File

@@ -0,0 +1,100 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Compatibility;
use Xibo\Entity\Widget;
use Xibo\Widget\Provider\WidgetCompatibilityInterface;
use Xibo\Widget\Provider\WidgetCompatibilityTrait;
/**
* Convert widget from an old schema to a new schema
*/
class WeatherWidgetCompatibility implements WidgetCompatibilityInterface
{
use WidgetCompatibilityTrait;
/** @inheritdoc
*/
public function upgradeWidget(Widget $widget, int $fromSchema, int $toSchema): bool
{
$this->getLog()->debug('upgradeWidget: ' . $widget->getId() . ' from: ' . $fromSchema . ' to: ' . $toSchema);
$overrideTemplate = $widget->getOptionValue('overrideTemplate', 0);
if ($overrideTemplate == 1) {
$newTemplateId = 'weather_custom_html';
} else {
$newTemplateId = match ($widget->getOptionValue('templateId', '')) {
'weather-module0-singleday' => 'weather_2',
'weather-module0-singleday2' => 'weather_3',
'weather-module1l' => 'weather_4',
'weather-module1p' => 'weather_5',
'weather-module2l' => 'weather_6',
'weather-module2p' => 'weather_7',
'weather-module3l' => 'weather_8',
'weather-module3p' => 'weather_9',
'weather-module4l' => 'weather_10',
'weather-module4p' => 'weather_11',
'weather-module5l' => 'weather_12',
'weather-module6h' => 'weather_13',
'weather-module6v' => 'weather_14',
'weather-module-7s' => 'weather_15',
'weather-module-8s' => 'weather_16',
'weather-module-9' => 'weather_17',
'weather-module-10l' => 'weather_18',
default => 'weather_1',
};
}
$widget->setOptionValue('templateId', 'attrib', $newTemplateId);
// If overriden, we need to tranlate the legacy options to the new values
if ($overrideTemplate == 1) {
$widget->changeOption('widgetOriginalWidth', 'widgetDesignWidth');
$widget->changeOption('widgetOriginalHeight', 'widgetDesignHeight');
}
// Process the background image properties so that they are removed if empty
$this->removeOptionIfEquals($widget, 'cloudy-image');
$this->removeOptionIfEquals($widget, 'day-cloudy-image');
$this->removeOptionIfEquals($widget, 'day-sunny-image');
$this->removeOptionIfEquals($widget, 'fog-image');
$this->removeOptionIfEquals($widget, 'hail-image');
$this->removeOptionIfEquals($widget, 'night-clear-image');
$this->removeOptionIfEquals($widget, 'night-partly-cloudy-image');
$this->removeOptionIfEquals($widget, 'rain-image');
$this->removeOptionIfEquals($widget, 'snow-image');
$this->removeOptionIfEquals($widget, 'windy-image');
return true;
}
private function removeOptionIfEquals(Widget $widget, string $option): void
{
if ($widget->getOptionValue($option, null) === $option) {
$widget->removeOption($option);
}
}
public function saveTemplate(string $template, string $fileName): bool
{
return false;
}
}

View File

@@ -0,0 +1,78 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Compatibility;
use Xibo\Entity\Widget;
use Xibo\Widget\Provider\WidgetCompatibilityInterface;
use Xibo\Widget\Provider\WidgetCompatibilityTrait;
/**
* Convert widget from an old schema to a new schema
*/
class WorldClockWidgetCompatibility implements WidgetCompatibilityInterface
{
use WidgetCompatibilityTrait;
/** @inheritdoc
*/
public function upgradeWidget(Widget $widget, int $fromSchema, int $toSchema): bool
{
$this->getLog()->debug('upgradeWidget: ' . $widget->getId() . ' from: ' . $fromSchema . ' to: ' . $toSchema);
$overrideTemplate = $widget->getOptionValue('overrideTemplate', 0);
if ($overrideTemplate) {
$widget->type = 'worldclock-digital-custom';
} else {
$widget->type = match ($widget->getOptionValue('clockType', 1)) {
2 => 'worldclock-analogue',
default => match ($widget->getOptionValue('templateId', '')) {
'worldclock1' => 'worldclock-digital-text',
'worldclock2' => 'worldclock-digital-date',
default => 'worldclock-digital-custom',
},
};
}
// We need to tranlate the legacy options to the new values
$widget->changeOption('clockCols', 'numCols');
$widget->changeOption('clockRows', 'numRows');
if ($overrideTemplate == 1) {
$widget->changeOption('mainTemplate', 'template_html');
$widget->changeOption('styleSheet', 'template_style');
$widget->changeOption('widgetOriginalWidth', 'widgetDesignWidth');
$widget->changeOption('widgetOriginalHeight', 'widgetDesignHeight');
}
// Always remove template id / clockType from world clock
$widget->removeOption('templateId');
$widget->removeOption('clockType');
return true;
}
public function saveTemplate(string $template, string $fileName): bool
{
return false;
}
}

View File

@@ -0,0 +1,96 @@
<?php
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget;
use Carbon\Carbon;
use Xibo\Widget\Provider\DataProviderInterface;
use Xibo\Widget\Provider\DurationProviderInterface;
use Xibo\Widget\Provider\WidgetProviderInterface;
use Xibo\Widget\Provider\WidgetProviderTrait;
/**
* A widget provider for stocks and currencies, only used to correctly set the numItems
*/
class CurrenciesAndStocksProvider implements WidgetProviderInterface
{
use WidgetProviderTrait;
/**
* We want to pass this out to the event mechanism for 3rd party sources.
* @param \Xibo\Widget\Provider\DataProviderInterface $dataProvider
* @return \Xibo\Widget\Provider\WidgetProviderInterface
*/
public function fetchData(DataProviderInterface $dataProvider): WidgetProviderInterface
{
$dataProvider->setIsUseEvent();
return $this;
}
/**
* Special handling for currencies and stocks where the number of data items is based on the quantity of
* items input in the `items` property.
* @param \Xibo\Widget\Provider\DurationProviderInterface $durationProvider
* @return \Xibo\Widget\Provider\WidgetProviderInterface
*/
public function fetchDuration(DurationProviderInterface $durationProvider): WidgetProviderInterface
{
$this->getLog()->debug('fetchDuration: CurrenciesAndStocksProvider');
// Currencies and stocks are based on the number of items set in the respective fields.
$items = $durationProvider->getWidget()->getOptionValue('items', null);
if ($items === null) {
$this->getLog()->debug('fetchDuration: CurrenciesAndStocksProvider: no items set');
return $this;
}
if ($durationProvider->getWidget()->getOptionValue('durationIsPerItem', 0) == 0) {
$this->getLog()->debug('fetchDuration: CurrenciesAndStocksProvider: duration per item not set');
return $this;
}
$numItems = count(explode(',', $items));
$this->getLog()->debug('fetchDuration: CurrenciesAndStocksProvider: number of items: ' . $numItems);
if ($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;
}
public function getDataCacheKey(DataProviderInterface $dataProvider): ?string
{
return null;
}
public function getDataModifiedDt(DataProviderInterface $dataProvider): ?Carbon
{
return null;
}
}

View File

@@ -0,0 +1,60 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget;
use Carbon\Carbon;
use Xibo\Event\DashboardDataRequestEvent;
use Xibo\Widget\Provider\DataProviderInterface;
use Xibo\Widget\Provider\DurationProviderInterface;
use Xibo\Widget\Provider\WidgetProviderInterface;
use Xibo\Widget\Provider\WidgetProviderTrait;
class DashboardProvider implements WidgetProviderInterface
{
use WidgetProviderTrait;
public function fetchData(DataProviderInterface $dataProvider): WidgetProviderInterface
{
$this->getLog()->debug('fetchData: DashboardProvider passing to event');
$this->getDispatcher()->dispatch(
new DashboardDataRequestEvent($dataProvider),
DashboardDataRequestEvent::$NAME
);
return $this;
}
public function fetchDuration(DurationProviderInterface $durationProvider): WidgetProviderInterface
{
return $this;
}
public function getDataCacheKey(DataProviderInterface $dataProvider): ?string
{
return null;
}
public function getDataModifiedDt(DataProviderInterface $dataProvider): ?Carbon
{
return null;
}
}

View File

@@ -0,0 +1,97 @@
<?php
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget;
use Carbon\Carbon;
use Xibo\Event\DataSetDataRequestEvent;
use Xibo\Event\DataSetModifiedDtRequestEvent;
use Xibo\Widget\Provider\DataProviderInterface;
use Xibo\Widget\Provider\DurationProviderInterface;
use Xibo\Widget\Provider\WidgetProviderInterface;
use Xibo\Widget\Provider\WidgetProviderTrait;
/**
* Provides data from DataSets.
*/
class DataSetProvider implements WidgetProviderInterface
{
use WidgetProviderTrait;
public function fetchData(DataProviderInterface $dataProvider): WidgetProviderInterface
{
$this->getLog()->debug('fetchData: DataSetProvider passing to event');
$this->getDispatcher()->dispatch(
new DataSetDataRequestEvent($dataProvider),
DataSetDataRequestEvent::$NAME
);
return $this;
}
public function fetchDuration(DurationProviderInterface $durationProvider): WidgetProviderInterface
{
if ($durationProvider->getWidget()->getOptionValue('durationIsPerItem', 0) == 1) {
// Count of rows
$numItems = $durationProvider->getWidget()->getOptionValue('numItems', 0);
// Workaround: dataset static (from v3 dataset view) has rowsPerPage instead.
$rowsPerPage = $durationProvider->getWidget()->getOptionValue('rowsPerPage', 0);
$itemsPerPage = $durationProvider->getWidget()->getOptionValue('itemsPerPage', 0);
// If we have paging involved then work out the page count.
$itemsPerPage = max($itemsPerPage, $rowsPerPage);
if ($itemsPerPage > 0) {
$numItems = ceil($numItems / $itemsPerPage);
}
$durationProvider->setDuration($durationProvider->getWidget()->calculatedDuration * $numItems);
$this->getLog()->debug(sprintf(
'fetchDuration: duration is per item, numItems: %s, rowsPerPage: %s, itemsPerPage: %s',
$numItems,
$rowsPerPage,
$itemsPerPage
));
}
return $this;
}
public function getDataCacheKey(DataProviderInterface $dataProvider): ?string
{
// No special cache key requirements.
return null;
}
public function getDataModifiedDt(DataProviderInterface $dataProvider): ?Carbon
{
$this->getLog()->debug('fetchData: DataSetProvider passing to modifiedDt request event');
$dataSetId = $dataProvider->getProperty('dataSetId');
if ($dataSetId !== null) {
// Raise an event to get the modifiedDt of this dataSet
$event = new DataSetModifiedDtRequestEvent($dataSetId);
$this->getDispatcher()->dispatch($event, DataSetModifiedDtRequestEvent::$NAME);
return max($event->getModifiedDt(), $dataProvider->getWidgetModifiedDt());
} else {
return null;
}
}
}

View File

@@ -0,0 +1,80 @@
<?php
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\DataType;
use Xibo\Widget\Definition\DataType;
/**
* An article, usually from a blog or news feed.
*/
class Article implements \JsonSerializable, DataTypeInterface
{
public static $NAME = 'article';
public $title;
public $summary;
public $content;
public $author;
public $permalink;
public $link;
public $image;
/** @var \Carbon\Carbon */
public $date;
/** @var \Carbon\Carbon */
public $publishedDate;
/** @inheritDoc */
public function jsonSerialize(): array
{
return [
'title' => $this->title,
'summary' => $this->summary,
'content' => $this->content,
'author' => $this->author,
'permalink' => $this->permalink,
'link' => $this->link,
'date' => $this->date->format('c'),
'publishedDate' => $this->publishedDate->format('c'),
'image' => $this->image,
];
}
public function getDefinition(): DataType
{
$dataType = new DataType();
$dataType->id = self::$NAME;
$dataType->name = __('Article');
$dataType
->addField('title', __('Title'), 'text')
->addField('summary', __('Summary'), 'text')
->addField('content', __('Content'), 'text')
->addField('author', __('Author'), 'text')
->addField('permalink', __('Permalink'), 'text')
->addField('link', __('Link'), 'text')
->addField('date', __('Created Date'), 'datetime')
->addField('publishedDate', __('Published Date'), 'datetime')
->addField('image', __('Image'), 'image');
return $dataType;
}
}

View File

@@ -0,0 +1,37 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\DataType;
use Xibo\Widget\Definition\DataType;
/**
* A class representation of a data type.
*/
interface DataTypeInterface
{
/**
* Return the definition
* @return \Xibo\Widget\Definition\DataType
*/
public function getDefinition(): DataType;
}

View File

@@ -0,0 +1,73 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\DataType;
use Xibo\Widget\Definition\DataType;
/**
* Event data type
*/
class Event implements \JsonSerializable, DataTypeInterface
{
public static $NAME = 'event';
public $summary;
public $description;
public $location;
/** @var \Carbon\Carbon */
public $startDate;
/** @var \Carbon\Carbon */
public $endDate;
/** @var bool */
public $isAllDay = false;
/** @inheritDoc */
public function jsonSerialize(): array
{
return [
'summary' => $this->summary,
'description' => $this->description,
'location' => $this->location,
'startDate' => $this->startDate->format('c'),
'endDate' => $this->endDate->format('c'),
'isAllDay' => $this->isAllDay,
];
}
public function getDefinition(): DataType
{
$dataType = new DataType();
$dataType->id = self::$NAME;
$dataType->name = __('Event');
$dataType
->addField('summary', __('Summary'), 'text')
->addField('description', __('Description'), 'text')
->addField('location', __('Location'), 'text')
->addField('startDate', __('Start Date'), 'datetime')
->addField('endDate', __('End Date'), 'datetime')
->addField('isAllDay', __('All Day Event'), 'boolean');
return $dataType;
}
}

View File

@@ -0,0 +1,161 @@
<?php
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\DataType;
use Xibo\Widget\Definition\DataType;
/**
* Forecast DataType
*/
class Forecast implements \JsonSerializable, DataTypeInterface
{
public static $NAME = 'forecast';
public $time;
public $sunSet;
public $sunRise;
public $summary;
public $icon;
public $wicon;
public $temperature;
public $temperatureRound;
public $temperatureNight;
public $temperatureNightRound;
public $temperatureMorning;
public $temperatureMorningRound;
public $temperatureEvening;
public $temperatureEveningRound;
public $temperatureHigh;
public $temperatureMaxRound;
public $temperatureLow;
public $temperatureMinRound;
public $temperatureMean;
public $temperatureMeanRound;
public $apparentTemperature;
public $apparentTemperatureRound;
public $dewPoint;
public $humidity;
public $humidityPercent;
public $pressure;
public $windSpeed;
public $windBearing;
public $windDirection;
public $cloudCover;
public $uvIndex;
public $visibility;
public $ozone;
public $location;
public $temperatureUnit;
public $windSpeedUnit;
public $visibilityDistanceUnit;
/** @inheritDoc */
public function jsonSerialize(): array
{
return [
'time' => $this->time,
'sunSet' => $this->sunSet,
'sunRise' => $this->sunRise,
'summary' => $this->summary,
'icon' => $this->icon,
'wicon' => $this->wicon,
'temperature' => $this->temperature,
'temperatureRound' => $this->temperatureRound,
'temperatureNight' => $this->temperatureNight,
'temperatureNightRound' => $this->temperatureNightRound,
'temperatureMorning' => $this->temperatureMorning,
'temperatureMorningRound' => $this->temperatureMorningRound,
'temperatureEvening' => $this->temperatureEvening,
'temperatureEveningRound' => $this->temperatureEveningRound,
'temperatureHigh' => $this->temperatureHigh,
'temperatureMaxRound' => $this->temperatureMaxRound,
'temperatureLow' => $this->temperatureLow,
'temperatureMinRound' => $this->temperatureMinRound,
'temperatureMean' => $this->temperatureMean,
'temperatureMeanRound' => $this->temperatureMeanRound,
'apparentTemperature' => $this->apparentTemperature,
'apparentTemperatureRound' => $this->apparentTemperatureRound,
'dewPoint' => $this->dewPoint,
'humidity' => $this->humidity,
'humidityPercent' => $this->humidityPercent,
'pressure' => $this->pressure,
'windSpeed' => $this->windSpeed,
'windBearing' => $this->windBearing,
'windDirection' => $this->windDirection,
'cloudCover' => $this->cloudCover,
'uvIndex' => $this->uvIndex,
'visibility' => $this->visibility,
'ozone' => $this->ozone,
'location' => $this->location,
'temperatureUnit' => $this->temperatureUnit,
'windSpeedUnit' => $this->windSpeedUnit,
'visibilityDistanceUnit' => $this->visibilityDistanceUnit
];
}
public function getDefinition(): DataType
{
$dataType = new DataType();
$dataType->id = self::$NAME;
$dataType->name = __('Forecast');
$dataType
->addField('time', 'Time', 'datetime')
->addField('sunSet', 'Sun Set', 'datetime')
->addField('sunRise', 'Sun Rise', 'datetime')
->addField('summary', 'Summary', 'text')
->addField('icon', 'Icon', 'text')
->addField('wicon', 'Weather Icon', 'text')
->addField('temperature', 'Temperature', 'number')
->addField('temperatureRound', 'Temperature Round', 'number')
->addField('temperatureNight', 'Temperature Night', 'number')
->addField('temperatureNightRound', 'Temperature Night Round', 'number')
->addField('temperatureMorning', 'Temperature Morning', 'number')
->addField('temperatureMorningRound', 'Temperature Morning Round', 'number')
->addField('temperatureEvening', 'Temperature Evening', 'number')
->addField('temperatureEveningRound', 'Temperature Evening Round', 'number')
->addField('temperatureHigh', 'Temperature High', 'number')
->addField('temperatureMaxRound', 'Temperature Max Round', 'number')
->addField('temperatureLow', 'Temperature Low', 'number')
->addField('temperatureMinRound', 'Temperature Min Round', 'number')
->addField('temperatureMean', 'Temperature Mean', 'number')
->addField('temperatureMeanRound', 'Temperature Mean Round', 'number')
->addField('apparentTemperature', 'Apparent Temperature', 'number')
->addField('apparentTemperatureRound', 'Apparent Temperature Round', 'number')
->addField('dewPoint', 'Dew Point', 'number')
->addField('humidity', 'Humidity', 'number')
->addField('humidityPercent', 'Humidity Percent', 'number')
->addField('pressure', 'Pressure', 'number')
->addField('windSpeed', 'Wind Speed', 'number')
->addField('windBearing', 'Wind Bearing', 'number')
->addField('windDirection', 'Wind Direction', 'text')
->addField('cloudCover', 'Cloud Cover', 'number')
->addField('uvIndex', 'Uv Index', 'number')
->addField('visibility', 'Visibility', 'number')
->addField('ozone', 'Ozone', 'number')
->addField('location', 'Location', 'text')
->addField('temperatureUnit', 'Temperature Unit', 'text')
->addField('windSpeedUnit', 'WindSpeed Unit', 'text')
->addField('visibilityDistanceUnit', 'VisibilityDistance Unit', 'text');
return $dataType;
}
}

View File

@@ -0,0 +1,70 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\DataType;
use Xibo\Widget\Definition\DataType;
/**
* Product DataType (primarily used for the Menu Board component)
*/
class Product implements \JsonSerializable, DataTypeInterface
{
public $name;
public $price;
public $description;
public $availability;
public $allergyInfo;
public $calories;
public $image;
public $productOptions;
public function getDefinition(): DataType
{
$dataType = new DataType();
$dataType->id = 'product';
$dataType->name = __('Product');
$dataType->addField('name', __('Name'), 'string');
$dataType->addField('price', __('Price'), 'decimal');
$dataType->addField('description', __('Description'), 'string');
$dataType->addField('availability', __('Availability'), 'int');
$dataType->addField('allergyInfo', __('Allergy Information'), 'string');
$dataType->addField('calories', __('Calories'), 'string');
$dataType->addField('image', __('Image'), 'int');
$dataType->addField('productOptions', __('Product Options'), 'array');
return $dataType;
}
public function jsonSerialize(): array
{
return [
'name' => $this->name,
'price' => $this->price,
'description' => $this->description,
'availability' => $this->availability,
'calories' => $this->calories,
'allergyInfo' => $this->allergyInfo,
'image' => $this->image,
'productOptions' => $this->productOptions,
];
}
}

View File

@@ -0,0 +1,55 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\DataType;
use Xibo\Widget\Definition\DataType;
/**
* Product Category (primarily used for the Menu Board component)
*/
class ProductCategory implements \JsonSerializable, DataTypeInterface
{
public $name;
public $description;
public $image;
public function getDefinition(): DataType
{
$dataType = new DataType();
$dataType->id = 'product-category';
$dataType->name = __('Product Category');
$dataType->addField('name', __('Name'), 'string');
$dataType->addField('description', __('Description'), 'string');
$dataType->addField('image', __('Image'), 'int');
return $dataType;
}
public function jsonSerialize(): array
{
return [
'name' => $this->name,
'description' => $this->description,
'image' => $this->image,
];
}
}

View File

@@ -0,0 +1,76 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\DataType;
use Xibo\Widget\Definition\DataType;
/**
* Social Media DataType
*/
class SocialMedia implements \JsonSerializable, DataTypeInterface
{
public static $NAME = 'social-media';
public $text;
public $user;
public $userProfileImage;
public $userProfileImageMini;
public $userProfileImageBigger;
public $location;
public $screenName;
public $date;
public $photo;
/** @inheritDoc */
public function jsonSerialize(): array
{
return [
'text' => $this->text,
'user' => $this->user,
'userProfileImage' => $this->userProfileImage,
'userProfileImageMini' => $this->userProfileImageMini,
'userProfileImageBigger' => $this->userProfileImageBigger,
'location' => $this->location,
'screenName' => $this->screenName,
'date' => $this->date,
'photo' => $this->photo,
];
}
public function getDefinition(): DataType
{
$dataType = new DataType();
$dataType->id = self::$NAME;
$dataType->name = __('Social Media');
$dataType
->addField('text', __('Text'), 'text', true)
->addField('user', __('User'), 'text')
->addField('userProfileImage', __('Profile Image'), 'image')
->addField('userProfileImageMini', __('Mini Profile Image'), 'image')
->addField('userProfileImageBigger', __('Bigger Profile Image'), 'image')
->addField('location', __('Location'), 'text')
->addField('screenName', __('Screen Name'), 'text')
->addField('date', __('Date'), 'datetime')
->addField('photo', __('Photo'), 'image');
return $dataType;
}
}

View File

@@ -0,0 +1,196 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Definition;
use GuzzleHttp\Psr7\Stream;
use Illuminate\Support\Str;
use Intervention\Image\ImageManagerStatic as Img;
use Psr\Http\Message\ResponseInterface;
use Slim\Http\Response;
use Slim\Http\ServerRequest;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\NotFoundException;
use Xibo\Xmds\Entity\Dependency;
/**
* An asset
*/
class Asset implements \JsonSerializable
{
public $id;
public $type;
public $alias;
public $path;
public $mimeType;
/** @var bool */
public $autoInclude;
/** @var bool */
public $cmsOnly;
public $assetNo;
private $fileSize;
private $md5;
/** @inheritDoc */
public function jsonSerialize(): array
{
return [
'id' => $this->id,
'alias' => $this->alias,
'type' => $this->type,
'path' => $this->path,
'mimeType' => $this->mimeType,
'cmsOnly' => $this->cmsOnly,
'autoInclude' => $this->autoInclude,
];
}
/**
* Should this asset be sent to the player?
* @return bool
*/
public function isSendToPlayer(): bool
{
return !($this->cmsOnly ?? false);
}
/**
* Should this asset be auto included in the HTML sent to the player
* @return bool
*/
public function isAutoInclude(): bool
{
return $this->autoInclude && $this->isSendToPlayer();
}
/**
* @param string $libraryLocation
* @param bool $forceUpdate
* @return $this
* @throws GeneralException
*/
public function updateAssetCache(string $libraryLocation, bool $forceUpdate = false): Asset
{
// Verify the asset is cached and update its path.
$assetPath = $libraryLocation . 'assets/' . $this->getFilename();
if (!file_exists($assetPath) || $forceUpdate) {
$result = @copy(PROJECT_ROOT . $this->path, $assetPath);
if (!$result) {
throw new GeneralException('Unable to copy asset');
}
$forceUpdate = true;
}
// Get the bundle MD5
$assetMd5CachePath = $assetPath . '.md5';
if (!file_exists($assetMd5CachePath) || $forceUpdate) {
$assetMd5 = md5_file($assetPath);
file_put_contents($assetMd5CachePath, $assetMd5);
} else {
$assetMd5 = file_get_contents($assetPath . '.md5');
}
$this->path = $assetPath;
$this->md5 = $assetMd5;
$this->fileSize = filesize($assetPath);
return $this;
}
/**
* Get this asset as a dependency.
* @return \Xibo\Xmds\Entity\Dependency
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function getDependency(): Dependency
{
// Check that this asset is valid.
if (!file_exists($this->path)) {
throw new NotFoundException(sprintf(__('Asset %s not found'), $this->path));
}
// Return a dependency
return new Dependency(
'asset',
$this->id,
$this->getLegacyId(),
$this->path,
$this->fileSize,
$this->md5,
true
);
}
/**
* Get the file name for this asset
* @return string
*/
public function getFilename(): string
{
return basename($this->path);
}
/**
* Generate a PSR response for this asset.
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function psrResponse(ServerRequest $request, Response $response, string $sendFileMode): ResponseInterface
{
// Make sure this asset exists
if (!file_exists($this->path)) {
throw new NotFoundException(__('Asset file does not exist'));
}
$response = $response->withHeader('Content-Length', $this->fileSize);
$response = $response->withHeader('Content-Type', $this->mimeType);
// Output the file
if ($sendFileMode === 'Apache') {
// Send via Apache X-Sendfile header?
$response = $response->withHeader('X-Sendfile', $this->path);
} else if ($sendFileMode === 'Nginx') {
// Send via Nginx X-Accel-Redirect?
$response = $response->withHeader('X-Accel-Redirect', '/download/assets/' . $this->getFilename());
} else if (Str::startsWith('image', $this->mimeType)) {
$response = Img::make('/' . $this->path)->psrResponse();
} else {
// Set the right content type.
$response = $response->withBody(new Stream(fopen($this->path, 'r')));
}
return $response;
}
/**
* Get Legacy ID for this asset on older players
* there is a risk that this ID will change as modules/templates with assets are added/removed in the system
* however, we have mitigated by ensuring that only one instance of any required file is added to rf return
* @return int
*/
private function getLegacyId(): int
{
return (Dependency::LEGACY_ID_OFFSET_ASSET + $this->assetNo) * -1;
}
}

View File

@@ -0,0 +1,43 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Definition;
/**
* Represents a condition for a test
*/
class Condition implements \JsonSerializable
{
public $field;
public $type;
public $value;
/** @inheritDoc */
public function jsonSerialize(): array
{
return [
'field' => $this->field,
'type' => $this->type,
'value' => $this->value
];
}
}

View File

@@ -0,0 +1,56 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Definition;
/**
* A module data type
*/
class DataType implements \JsonSerializable
{
public $id;
public $name;
/** @var \Xibo\Widget\Definition\Field[] */
public $fields = [];
public function addField(string $id, string $title, string $type, bool $isRequired = false): DataType
{
$field = new Field();
$field->id = $id;
$field->type = $type;
$field->title = $title;
$field->isRequired = $isRequired;
$this->fields[] = $field;
return $this;
}
/** @inheritDoc */
public function jsonSerialize(): array
{
return [
'id' => $this->id,
'name' => $this->name,
'fields' => $this->fields,
];
}
}

View File

@@ -0,0 +1,56 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Definition;
/**
* @SWG\Definition()
* A class representing an instance of an element template
*/
class Element implements \JsonSerializable
{
public $id;
public $top;
public $left;
public $width;
public $height;
public $rotation;
public $layer;
public $elementGroupId;
public $properties = [];
/** @inheritDoc */
public function jsonSerialize(): array
{
return [
'id' => $this->id,
'top' => $this->top,
'left' => $this->left,
'width' => $this->width,
'height' => $this->height,
'rotation' => $this->rotation,
'layer' => $this->layer,
'elementGroupId' => $this->elementGroupId,
'properties' => $this->properties
];
}
}

View File

@@ -0,0 +1,56 @@
<?php
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Definition;
/**
* A class representing an instance of a group of elements
* @SWG\Definition()
*/
class ElementGroup implements \JsonSerializable
{
public $id;
public $top;
public $left;
public $width;
public $height;
public $layer;
public $title;
public $slot;
public $pinSlot;
/** @inheritDoc */
public function jsonSerialize(): array
{
return [
'id' => $this->id,
'top' => $this->top,
'left' => $this->left,
'width' => $this->width,
'height' => $this->height,
'layer' => $this->layer,
'title' => $this->title,
'slot' => $this->slot,
'pinSlot' => $this->pinSlot
];
}
}

View File

@@ -0,0 +1,46 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Definition;
/**
* @SWG\Definition()
* A class representing one template extending another
*/
class Extend implements \JsonSerializable
{
public $template;
public $override;
public $with;
public $escapeHtml;
/** @inheritDoc */
public function jsonSerialize(): array
{
return [
'template' => $this->template,
'override' => $this->override,
'with' => $this->with,
'escapeHtml' => $this->escapeHtml,
];
}
}

View File

@@ -0,0 +1,45 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Definition;
/**
* Class representing a data type field
*/
class Field implements \JsonSerializable
{
public $id;
public $type;
public $title;
public $isRequired;
/** @inheritDoc */
public function jsonSerialize(): array
{
return [
'id' => $this->id,
'type' => $this->type,
'title' => $this->title,
'isRequired' => $this->isRequired,
];
}
}

View File

@@ -0,0 +1,42 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Definition;
/**
* A Legacy Type
* @SWG\Definition()
*/
class LegacyType implements \JsonSerializable
{
public $name;
public $condition;
/** @inheritDoc */
public function jsonSerialize(): array
{
return [
'name' => $this->name,
'condition' => $this->condition,
];
}
}

View File

@@ -0,0 +1,67 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Definition;
/**
* Option: typically used when paired with a dropdown
* @SWG\Definition()
*/
class Option implements \JsonSerializable
{
/**
* @SWG\Property(description="Name")
* @var string
*/
public $name;
/**
* @SWG\Property(description="Image: optional image asset")
* @var string
*/
public $image;
/**
* @SWG\Property(description="Set")
* @var string[]
*/
public $set = [];
/**
* * @SWG\Property(description="Title: shown in the dropdown/select")
* @var string
*/
public $title;
/**
* @inheritDoc
*/
public function jsonSerialize(): array
{
return [
'name' => $this->name,
'image' => $this->image,
'set' => $this->set,
'title' => $this->title
];
}
}

View File

@@ -0,0 +1,53 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Definition;
/**
* Player compatibility
*/
class PlayerCompatibility implements \JsonSerializable
{
public $windows;
public $linux;
public $android;
public $webos;
public $tizen;
public $chromeos;
public $message;
/**
* @inheritDoc
*/
public function jsonSerialize(): array
{
return [
'windows' => $this->windows,
'linux' => $this->linux,
'android' => $this->android,
'webos' => $this->webos,
'tizen' => $this->tizen,
'chromeos' => $this->chromeos,
'message' => $this->message,
];
}
}

View File

@@ -0,0 +1,554 @@
<?php
/*
* Copyright (C) 2025 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Definition;
use Carbon\Carbon;
use Carbon\CarbonInterval;
use Illuminate\Support\Str;
use Respect\Validation\Validator as v;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\ValueTooLargeException;
use Xibo\Support\Sanitizer\SanitizerInterface;
/**
* A Property
* @SWG\Definition()
*/
class Property implements \JsonSerializable
{
/**
* @SWG\Property(description="ID, saved as a widget option")
* @var string
*/
public $id;
/**
* @SWG\Property(description="Type, determines the field type")
* @var string
*/
public $type;
/**
* @SWG\Property(description="Title: shown in the property panel")
* @var string
*/
public $title;
/**
* @SWG\Property(description="Help Text: shown in the property panel")
* @var string
*/
public $helpText;
/** @var \Xibo\Widget\Definition\Rule */
public $validation;
/**
* @SWG\Property()
* @var string An optional default value
*/
public $default;
/** @var \Xibo\Widget\Definition\Option[] */
public $options;
/** @var \Xibo\Widget\Definition\Test[] */
public $visibility = [];
/** @var string The element variant */
public $variant;
/** @var string The data format */
public $format;
/** @var bool Should library refs be permitted in the value? */
public $allowLibraryRefs = false;
/** @var bool Should asset refs be permitted in the value? */
public $allowAssetRefs = false;
/** @var bool Should translations be parsed in the value? */
public $parseTranslations = false;
/** @var bool Should the property be included in the XLF? */
public $includeInXlf = false;
/** @var bool Should the property be sent into Elements */
public $sendToElements = false;
/** @var bool Should the default value be written out to widget options */
public $saveDefault = false;
/** @var \Xibo\Widget\Definition\PlayerCompatibility */
public $playerCompatibility;
/** @var string HTML to populate a custom popover to be shown next to the input */
public $customPopOver;
/** @var string HTML selector of the element that this property depends on */
public $dependsOn;
/** @var string ID of the target element */
public $target;
/** @var string The mode of the property */
public $mode;
/** @var string The group ID of the property */
public $propertyGroupId;
/** @var mixed The value assigned to this property. This is set from widget options, or settings, never via XML */
public $value;
/** @inheritDoc */
public function jsonSerialize(): array
{
return [
'id' => $this->id,
'value' => $this->value,
'type' => $this->type,
'variant' => $this->variant,
'format' => $this->format,
'title' => $this->title,
'mode' => $this->mode,
'target' => $this->target,
'propertyGroupId' => $this->propertyGroupId,
'helpText' => $this->helpText,
'validation' => $this->validation,
'default' => $this->default,
'options' => $this->options,
'customPopOver' => $this->customPopOver,
'playerCompatibility' => $this->playerCompatibility,
'visibility' => $this->visibility,
'allowLibraryRefs' => $this->allowLibraryRefs,
'allowAssetRefs' => $this->allowAssetRefs,
'parseTranslations' => $this->parseTranslations,
'saveDefault' => $this->saveDefault,
'dependsOn' => $this->dependsOn,
'sendToElements' => $this->sendToElements,
];
}
/**
* Add an option
* @param string $name
* @param string $image
* @param array $set
* @param string $title
* @return $this
*/
public function addOption(string $name, string $image, array $set, string $title): Property
{
$option = new Option();
$option->name = $name;
$option->image = $image;
$option->set = $set;
$option->title = __($title);
$this->options[] = $option;
return $this;
}
/**
* Add a visibility test
* @param string $type
* @param string|null $message
* @param array $conditions
* @return $this
*/
public function addVisibilityTest(string $type, ?string $message, array $conditions): Property
{
$this->visibility[] = $this->parseTest($type, $message, $conditions);
return $this;
}
/**
* @param \Xibo\Support\Sanitizer\SanitizerInterface $params
* @param string|null $key
* @return \Xibo\Widget\Definition\Property
* @throws \Xibo\Support\Exception\InvalidArgumentException
*/
public function setDefaultByType(SanitizerInterface $params, ?string $key = null): Property
{
$this->default = $this->getByType($params, $key);
return $this;
}
/**
* @param SanitizerInterface $params
* @param string|null $key
* @param bool $ignoreDefault
* @return Property
* @throws InvalidArgumentException
*/
public function setValueByType(
SanitizerInterface $params,
?string $key = null,
bool $ignoreDefault = false
): Property {
$value = $this->getByType($params, $key);
if ($value !== $this->default || $ignoreDefault || $this->saveDefault) {
$this->value = $value;
}
return $this;
}
/**
* @param array $properties A key/value array of all properties for this entity (be it module or template)
* @param string $stage What stage are we at?
* @return Property
* @throws InvalidArgumentException
* @throws ValueTooLargeException
*/
public function validate(array $properties, string $stage): Property
{
if (!empty($this->value) && strlen($this->value) > 67108864) {
throw new ValueTooLargeException(sprintf(__('Value too large for %s'), $this->title), $this->id);
}
// Skip if no validation.
if ($this->validation === null
|| ($stage === 'save' && !$this->validation->onSave)
|| ($stage === 'status' && !$this->validation->onStatus)
) {
return $this;
}
foreach ($this->validation->tests as $test) {
// We have a test, evaulate its conditions.
$exceptions = [];
foreach ($test->conditions as $condition) {
try {
// Assume we're testing the field we belong to, and if that's empty use the default value
$testValue = $this->value ?? $this->default;
// What value are we testing against (only used by certain types)
if (empty($condition->field)) {
$valueToTestAgainst = $condition->value;
} else {
// If a field and a condition value is provided, test against those, ignoring my own field value
if (!empty($condition->value)) {
$testValue = $condition->value;
}
$valueToTestAgainst = $properties[$condition->field] ?? null;
}
// Do we have a message
$message = empty($test->message) ? null : __($test->message);
switch ($condition->type) {
case 'required':
// We will accept the default value here
if (empty($testValue) && empty($this->default)) {
throw new InvalidArgumentException(
$message ?? sprintf(__('Missing required property %s'), $this->title),
$this->id
);
}
break;
case 'uri':
if (!empty($testValue)
&& !v::url()->validate($testValue)
) {
throw new InvalidArgumentException(
$message ?? sprintf(__('%s must be a valid URI'), $this->title),
$this->id
);
}
break;
case 'windowsPath':
// Ensure the path is a valid Windows file path ending in a file, not a directory
$windowsPathRegex = '/^(?P<Root>[A-Za-z]:)(?P<Relative>(?:\\\\[^<>:"\/\\\\|?*\r\n]+)+)(?P<File>\\\\[^<>:"\/\\\\|?*\r\n]+)$/';
// Check if the test value is not empty and does not match the regular expression
if (!empty($testValue)
&& !preg_match($windowsPathRegex, $testValue)
) {
// Throw an InvalidArgumentException if the test value is not a valid Windows path
throw new InvalidArgumentException(
$message ?? sprintf(__('%s must be a valid Windows path'), $this->title),
$this->id
);
}
break;
case 'interval':
if (!empty($testValue)) {
// Try to create a date interval from it
$dateInterval = CarbonInterval::createFromDateString($testValue);
if ($dateInterval === false) {
throw new InvalidArgumentException(
// phpcs:ignore Generic.Files.LineLength
__('That is not a valid date interval, please use natural language such as 1 week'),
'customInterval'
);
}
// Use now and add the date interval to it
$now = Carbon::now();
$check = $now->copy()->add($dateInterval);
if ($now->equalTo($check)) {
throw new InvalidArgumentException(
// phpcs:ignore Generic.Files.LineLength
$message ?? __('That is not a valid date interval, please use natural language such as 1 week'),
$this->id
);
}
}
break;
case 'eq':
if ($testValue != $valueToTestAgainst) {
throw new InvalidArgumentException(
$message ?? sprintf(__('%s must equal %s'), $this->title, $valueToTestAgainst),
$this->id,
);
}
break;
case 'neq':
if ($testValue == $valueToTestAgainst) {
throw new InvalidArgumentException(
$message ?? sprintf(__('%s must not equal %s'), $this->title, $valueToTestAgainst),
$this->id,
);
}
break;
case 'contains':
if (!empty($testValue) && !Str::contains($testValue, $valueToTestAgainst)) {
throw new InvalidArgumentException(
$message ?? sprintf(__('%s must contain %s'), $this->title, $valueToTestAgainst),
$this->id,
);
}
break;
case 'ncontains':
if (!empty($testValue) && Str::contains($testValue, $valueToTestAgainst)) {
throw new InvalidArgumentException(
// phpcs:ignore Generic.Files.LineLength
$message ?? sprintf(__('%s must not contain %s'), $this->title, $valueToTestAgainst),
$this->id,
);
}
break;
case 'lt':
// Value must be < to the condition value, or field value
if (!($testValue < $valueToTestAgainst)) {
throw new InvalidArgumentException(
// phpcs:ignore Generic.Files.LineLength
$message ?? sprintf(__('%s must be less than %s'), $this->title, $valueToTestAgainst),
$this->id
);
}
break;
case 'lte':
// Value must be <= to the condition value, or field value
if (!($testValue <= $valueToTestAgainst)) {
throw new InvalidArgumentException(
// phpcs:ignore Generic.Files.LineLength
$message ?? sprintf(__('%s must be less than or equal to %s'), $this->title, $valueToTestAgainst),
$this->id
);
}
break;
case 'gte':
// Value must be >= to the condition value, or field value
if (!($testValue >= $valueToTestAgainst)) {
throw new InvalidArgumentException(
// phpcs:ignore Generic.Files.LineLength
$message ?? sprintf(__('%s must be greater than or equal to %s'), $this->title, $valueToTestAgainst),
$this->id
);
}
break;
case 'gt':
// Value must be > to the condition value, or field value
if (!($testValue > $valueToTestAgainst)) {
throw new InvalidArgumentException(
// phpcs:ignore Generic.Files.LineLength
$message ?? sprintf(__('%s must be greater than %s'), $this->title, $valueToTestAgainst),
$this->id
);
}
break;
default:
// Nothing to validate
}
} catch (InvalidArgumentException $invalidArgumentException) {
// If we are an AND test, all conditions must pass, so we know already to exception here.
if ($test->type === 'and') {
throw $invalidArgumentException;
}
// We're an OR
$exceptions[] = $invalidArgumentException;
}
}
// If we are an OR then make sure all conditions have failed.
$countOfFailures = count($exceptions);
if ($test->type === 'or' && $countOfFailures === count($test->conditions)) {
throw $exceptions[0];
}
}
return $this;
}
/**
* @param \Xibo\Support\Sanitizer\SanitizerInterface $params
* @param string|null $key
* @return bool|float|int|string|null
* @throws \Xibo\Support\Exception\InvalidArgumentException
*/
private function getByType(SanitizerInterface $params, ?string $key = null)
{
$key = $key ?: $this->id;
if (!$params->hasParam($key) && $this->type !== 'checkbox') {
// Clear the stored value and therefore use the default
return null;
}
// Parse according to the type of field we're expecting
switch ($this->type) {
case 'checkbox':
return $params->getCheckbox($key);
case 'integer':
return $params->getInt($key);
case 'number':
return $params->getDouble($key);
case 'dropdown':
$value = $params->getString($key);
if ($value === null) {
return null;
}
$found = false;
foreach ($this->options as $option) {
if ($option->name === $value) {
$found = true;
break;
}
}
if ($found) {
return $value;
} else {
throw new InvalidArgumentException(
sprintf(__('%s is not a valid option'), $value),
$key
);
}
case 'code':
case 'richText':
return $params->getParam($key);
case 'text':
if ($this->variant === 'sql') {
// Handle raw SQL clauses
return str_ireplace(Sql::DISALLOWED_KEYWORDS, '', $params->getParam($key));
} else {
return $params->getString($key);
}
default:
return $params->getString($key);
}
}
/**
* Apply any filters on the data.
* @return void
*/
public function applyFilters(): void
{
if ($this->variant === 'uri' || $this->type === 'commandBuilder') {
$this->value = urlencode($this->value);
}
}
/**
* Reverse filters
* @return void
*/
public function reverseFilters(): void
{
$this->value = $this->reverseFiltersOnValue($this->value);
}
/**
* @param mixed $value
* @return mixed|string
*/
public function reverseFiltersOnValue(mixed $value): mixed
{
if (($this->variant === 'uri' || $this->type === 'commandBuilder') && !empty($value)) {
$value = urldecode($value);
}
return $value;
}
/**
* Should this property be represented with CData
* @return bool
*/
public function isCData(): bool
{
return $this->type === 'code' || $this->type === 'richText';
}
/**
* @param string $type
* @param string $message
* @param array $conditions
* @return Test
*/
public function parseTest(string $type, string $message, array $conditions): Test
{
$test = new Test();
$test->type = $type ?: 'and';
$test->message = $message;
foreach ($conditions as $item) {
$condition = new Condition();
$condition->type = $item['type'];
$condition->field = $item['field'];
$condition->value = $item['value'];
$test->conditions[] = $condition;
}
return $test;
}
}

View File

@@ -0,0 +1,46 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Definition;
/**
* A class representing an instance of a group property to put a property in assigned Tab
* @SWG\Definition()
*/
class PropertyGroup implements \JsonSerializable
{
public $id;
public $expanded;
public $title;
public $helpText;
/** @inheritDoc */
public function jsonSerialize(): array
{
return [
'id' => $this->id,
'expanded' => $this->expanded,
'title' => $this->title,
'helpText' => $this->helpText
];
}
}

View File

@@ -0,0 +1,52 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Definition;
/**
* A rule to apply to a property
* @SWG\Definition()
*/
class Rule implements \JsonSerializable
{
public $onSave = true;
public $onStatus = true;
/** @var Test[] */
public $tests;
public function addRuleTest(Test $test): Rule
{
$this->tests[] = $test;
return $this;
}
public function jsonSerialize(): array
{
return [
'onSave' => $this->onSave,
'onStatus' => $this->onStatus,
'tests' => $this->tests,
];
}
}

View File

@@ -0,0 +1,46 @@
<?php
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Definition;
/**
* SQL definitions
*/
class Sql
{
const DISALLOWED_KEYWORDS = [
';',
'INSERT',
'UPDATE',
'SELECT',
'FROM',
'WHERE',
'DELETE',
'TRUNCATE',
'TABLE',
'ALTER',
'GRANT',
'REVOKE',
'CREATE',
'DROP',
];
}

View File

@@ -0,0 +1,80 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Definition;
/**
* @SWG\Definition()
* A Stencil is a template which is rendered in the server and/or client
* it can optionally have properties and/or elements
*/
class Stencil implements \JsonSerializable
{
/** @var \Xibo\Widget\Definition\Element[] */
public $elements = [];
/** @var string|null */
public $twig;
/** @var string|null */
public $hbs;
/** @var string|null */
public $head;
/** @var string|null */
public $style;
/** @var string|null */
public $hbsId;
/** @var double Optional positional information if contained as part of an element group */
public $width;
/** @var double Optional positional information if contained as part of an element group */
public $height;
/** @var double Optional positional information if contained as part of an element group */
public $gapBetweenHbs;
/**
* @SWG\Property(description="An array of element groups")
* @var \Xibo\Widget\Definition\ElementGroup[]
*/
public $elementGroups = [];
/** @inheritDoc */
public function jsonSerialize(): array
{
return [
'hbsId' => $this->hbsId,
'hbs' => $this->hbs,
'head' => $this->head,
'style' => $this->style,
'width' => $this->width,
'height' => $this->height,
'gapBetweenHbs' => $this->gapBetweenHbs,
'elements' => $this->elements,
'elementGroups' => $this->elementGroups
];
}
}

View File

@@ -0,0 +1,49 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Definition;
/**
* Represents a test/group of conditions
* @SWG\Definition()
*/
class Test implements \JsonSerializable
{
/** @var string */
public $type;
/** @var Condition[] */
public $conditions;
/** @var string|null */
public $message;
/** @inheritDoc */
public function jsonSerialize(): array
{
return [
'type' => $this->type,
'message' => $this->message,
'conditions' => $this->conditions,
];
}
}

265
lib/Widget/IcsProvider.php Normal file
View File

@@ -0,0 +1,265 @@
<?php
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget;
use Carbon\Carbon;
use GuzzleHttp\Exception\RequestException;
use ICal\ICal;
use Xibo\Helper\DateFormatHelper;
use Xibo\Support\Exception\ConfigurationException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Widget\DataType\Event;
use Xibo\Widget\Provider\DataProviderInterface;
use Xibo\Widget\Provider\DurationProviderNumItemsTrait;
use Xibo\Widget\Provider\WidgetProviderInterface;
use Xibo\Widget\Provider\WidgetProviderTrait;
/**
* Download and parse an ISC feed
*/
class IcsProvider implements WidgetProviderInterface
{
use WidgetProviderTrait;
use DurationProviderNumItemsTrait;
/**
* Fetch the ISC feed and load its data.
* @inheritDoc
*/
public function fetchData(DataProviderInterface $dataProvider): WidgetProviderInterface
{
// Do we have a feed configured?
$uri = $dataProvider->getProperty('uri');
if (empty($uri)) {
throw new InvalidArgumentException(__('Please enter the URI to a valid ICS feed.'), 'uri');
}
// Create an ICal helper and pass it the contents of the file.
$iCalConfig = [
'replaceWindowsTimeZoneIds' => ($dataProvider->getProperty('replaceWindowsTimeZoneIds', 0) == 1),
'defaultSpan' => 1,
];
// What event range are we interested in?
// Decide on the Range we're interested in
// $iCal->eventsFromInterval only works for future events
$excludeAllDay = $dataProvider->getProperty('excludeAllDay', 0) == 1;
$excludePastEvents = $dataProvider->getProperty('excludePast', 0) == 1;
$startOfDay = match ($dataProvider->getProperty('startIntervalFrom')) {
'month' => Carbon::now()->startOfMonth(),
'week' => Carbon::now()->startOfWeek(),
default => Carbon::now()->startOfDay(),
};
// Force timezone of each event?
$useEventTimezone = $dataProvider->getProperty('useEventTimezone', 1);
// do we use interval or provided date range?
if ($dataProvider->getProperty('useDateRange')) {
$rangeStart = $dataProvider->getProperty('rangeStart');
$rangeStart = empty($rangeStart)
? Carbon::now()->startOfMonth()
: Carbon::createFromFormat(DateFormatHelper::getSystemFormat(), $rangeStart);
$rangeEnd = $dataProvider->getProperty('rangeEnd');
$rangeEnd = empty($rangeEnd)
? Carbon::now()->endOfMonth()
: Carbon::createFromFormat(DateFormatHelper::getSystemFormat(), $rangeEnd);
} else {
$interval = $dataProvider->getProperty('customInterval');
$rangeStart = $startOfDay->copy();
$rangeEnd = $rangeStart->copy()->add(
\DateInterval::createFromDateString(empty($interval) ? '1 week' : $interval)
);
}
$this->getLog()->debug('fetchData: final range, start=' . $rangeStart->toAtomString()
. ', end=' . $rangeEnd->toAtomString());
// Set up fuzzy filtering supported by the ICal library. This is included for performance.
// https://github.com/u01jmg3/ics-parser?tab=readme-ov-file#variables
$iCalConfig['filterDaysBefore'] = $rangeStart->diffInDays(Carbon::now(), false) + 2;
$iCalConfig['filterDaysAfter'] = $rangeEnd->diffInDays(Carbon::now()) + 2;
$this->getLog()->debug('Range start: ' . $rangeStart->toDateTimeString()
. ', range end: ' . $rangeEnd->toDateTimeString()
. ', config: ' . var_export($iCalConfig, true));
try {
$iCal = new ICal(false, $iCalConfig);
$iCal->initString($this->downloadIcs($uri, $dataProvider));
$this->getLog()->debug('Feed initialised');
// Before we parse anything - should we use the calendar timezone as a base for our calculations?
if ($dataProvider->getProperty('useCalendarTimezone') == 1) {
$iCal->defaultTimeZone = $iCal->calendarTimeZone();
}
$this->getLog()->debug('Calendar timezone set to: ' . $iCal->defaultTimeZone);
// Get an array of events
/** @var \ICal\Event[] $events */
$events = $iCal->eventsFromRange($rangeStart, $rangeEnd);
// Go through each event returned
foreach ($events as $event) {
try {
// Parse the ICal Event into our own data type object.
$entry = new Event();
$entry->summary = $event->summary;
$entry->description = $event->description;
$entry->location = $event->location;
// Parse out the start/end dates.
if ($useEventTimezone === 1) {
// Use the timezone from the event.
$entry->startDate = Carbon::instance($iCal->iCalDateToDateTime($event->dtstart_array[3]));
$entry->endDate = Carbon::instance($iCal->iCalDateToDateTime($event->dtend_array[3]));
} else {
// Use the parser calculated timezone shift
$entry->startDate = Carbon::instance($iCal->iCalDateToDateTime($event->dtstart_tz));
$entry->endDate = Carbon::instance($iCal->iCalDateToDateTime($event->dtend_tz));
}
$this->getLog()->debug('Event: ' . $event->summary . ' with '
. $entry->startDate->format('c') . ' / ' . $entry->endDate->format('c'));
// Detect all day event
$isAllDay = false;
// If dtstart has value DATE
// (following RFC recommendations in https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.4 )
if (isset($event->dtstart_array[0])) {
// If it's a string
if (is_string($event->dtstart_array[0]) && strtoupper($event->dtstart_array[0]) === 'DATE') {
$isAllDay = true;
}
// If it's an array
if (is_array($event->dtstart_array[0]) &&
isset($event->dtstart_array[0]['VALUE']) &&
strtoupper($event->dtstart_array[0]['VALUE']) === 'DATE') {
$isAllDay = true;
}
}
// If MS extension flags it as all day
if (isset($event->x_microsoft_cdo_alldayevent) &&
is_string($event->x_microsoft_cdo_alldayevent) &&
strtoupper($event->x_microsoft_cdo_alldayevent) === 'TRUE') {
$isAllDay = true;
}
// Fallback: If both times are midnight and event is more than one day
if (!$isAllDay) {
$startAtMidnight = $entry->startDate->isStartOfDay();
$endsAtMidnight = $entry->endDate->isStartOfDay();
$diffDays = $entry->endDate->copy()->startOfDay()->diffInDays(
$entry->startDate->copy()->startOfDay()
);
$isAllDay = $startAtMidnight && $endsAtMidnight && $diffDays >= 1;
}
$entry->isAllDay = $isAllDay;
if ($excludeAllDay && $isAllDay) {
continue;
}
if ($excludePastEvents && $entry->endDate->isPast()) {
continue;
}
$dataProvider->addItem($entry);
} catch (\Exception $exception) {
$this->getLog()->error('Unable to parse event. ' . var_export($event, true));
}
}
$dataProvider->setCacheTtl($dataProvider->getProperty('updateInterval', 60) * 60);
$dataProvider->setIsHandled();
} catch (\Exception $exception) {
$this->getLog()->error('iscProvider: fetchData: ' . $exception->getMessage());
$this->getLog()->debug($exception->getTraceAsString());
$dataProvider->addError(__('The iCal provided is not valid, please choose a valid feed'));
}
return $this;
}
public function getDataCacheKey(DataProviderInterface $dataProvider): ?string
{
// No special cache key requirements.
return null;
}
/**
* @throws \Xibo\Support\Exception\GeneralException
*/
private function downloadIcs(string $uri, DataProviderInterface $dataProvider): string
{
// See if we have this ICS cached already.
$cache = $dataProvider->getPool()->getItem('/widget/' . $dataProvider->getDataType() . '/' . md5($uri));
$ics = $cache->get();
if ($cache->isMiss() || $ics === null) {
// Make a new request.
$this->getLog()->debug('downloadIcs: cache miss');
try {
// Create a Guzzle Client to get the Feed XML
$response = $dataProvider
->getGuzzleClient([
'timeout' => 20, // wait no more than 20 seconds
])
->get($uri);
$ics = $response->getBody()->getContents();
// Save the resonse to cache
$cache->set($ics);
$cache->expiresAfter($dataProvider->getSetting('cachePeriod', 1440) * 60);
$dataProvider->getPool()->saveDeferred($cache);
} catch (RequestException $requestException) {
// Log and return empty?
$this->getLog()->error('downloadIcs: Unable to get feed: ' . $requestException->getMessage());
$this->getLog()->debug($requestException->getTraceAsString());
throw new ConfigurationException(__('Unable to download feed'));
}
} else {
$this->getLog()->debug('downloadIcs: cache hit');
}
return $ics;
}
public function getDataModifiedDt(DataProviderInterface $dataProvider): ?Carbon
{
return null;
}
}

View File

@@ -0,0 +1,202 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget;
use Carbon\Carbon;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\RequestException;
use Xibo\Widget\DataType\SocialMedia;
use Xibo\Widget\Provider\DataProviderInterface;
use Xibo\Widget\Provider\DurationProviderNumItemsTrait;
use Xibo\Widget\Provider\WidgetProviderInterface;
use Xibo\Widget\Provider\WidgetProviderTrait;
/**
* Downloads a Mastodon feed and returns SocialMedia data types
*/
class MastodonProvider implements WidgetProviderInterface
{
use WidgetProviderTrait;
use DurationProviderNumItemsTrait;
public function fetchData(DataProviderInterface $dataProvider): WidgetProviderInterface
{
$uri = $dataProvider->getSetting('defaultServerUrl', 'https://mastodon.social');
try {
$httpOptions = [
'timeout' => 20, // wait no more than 20 seconds
];
$queryOptions = [
'limit' => $dataProvider->getProperty('numItems', 15)
];
if ($dataProvider->getProperty('searchOn', 'all') === 'local') {
$queryOptions['local'] = true;
} elseif ($dataProvider->getProperty('searchOn', 'all') === 'remote') {
$queryOptions['remote'] = true;
}
// Media Only
if ($dataProvider->getProperty('onlyMedia', 0)) {
$queryOptions['only_media'] = true;
}
if (!empty($dataProvider->getProperty('serverUrl', ''))) {
$uri = $dataProvider->getProperty('serverUrl');
}
// Hashtag
$hashtag = trim($dataProvider->getProperty('hashtag', ''));
// when username is provided do not search in public timeline
if (!empty($dataProvider->getProperty('userName', ''))) {
// username search: get account ID, always returns one record
$accountId = $this->getAccountId($uri, $dataProvider->getProperty('userName'), $dataProvider);
$queryOptions['tagged'] = trim($hashtag, '#');
$queryOptions['exclude_replies'] = true; // exclude replies to other users
$queryOptions['exclude_reblogs'] = true; // exclude reposts/boosts
$uri = rtrim($uri, '/') . '/api/v1/accounts/' . $accountId . '/statuses?';
} else {
// Hashtag: When empty we should do a public search, when filled we should do a hashtag search
if (!empty($hashtag)) {
$uri = rtrim($uri, '/') . '/api/v1/timelines/tag/' . trim($hashtag, '#');
} else {
$uri = rtrim($uri, '/') . '/api/v1/timelines/public';
}
}
$response = $dataProvider
->getGuzzleClient($httpOptions)
->get($uri, [
'query' => $queryOptions
]);
$result = json_decode($response->getBody()->getContents(), true);
$this->getLog()->debug('Mastodon: uri: ' . $uri . ' httpOptions: ' . json_encode($httpOptions));
$this->getLog()->debug('Mastodon: count: ' . count($result));
// Expiry time for any media that is downloaded
$expires = Carbon::now()->addHours($dataProvider->getSetting('cachePeriodImages', 24))->format('U');
foreach ($result as $item) {
// Parse the mastodon
$mastodon = new SocialMedia();
$mastodon->text = strip_tags($item['content']);
$mastodon->user = $item['account']['acct'];
$mastodon->screenName = $item['account']['display_name'];
$mastodon->date = $item['created_at'];
// Original Default Image
$mastodon->userProfileImage = $dataProvider->addImage(
'mastodon_' . $item['account']['id'],
$item['account']['avatar'],
$expires
);
// Mini image
$mastodon->userProfileImageMini = $mastodon->userProfileImage;
// Bigger image
$mastodon->userProfileImageBigger = $mastodon->userProfileImage;
// Photo
// See if there are any photos associated with this status.
if ((isset($item['media_attachments']) && count($item['media_attachments']) > 0)) {
// only take the first one
$mediaObject = $item['media_attachments'][0];
$photoUrl = $mediaObject['preview_url'];
if (!empty($photoUrl)) {
$mastodon->photo = $dataProvider->addImage(
'mastodon_' . $mediaObject['id'],
$photoUrl,
$expires
);
}
}
// Add the mastodon topic.
$dataProvider->addItem($mastodon);
}
// If we've got data, then set our cache period.
$dataProvider->setCacheTtl($dataProvider->getSetting('cachePeriod', 3600));
$dataProvider->setIsHandled();
} catch (RequestException $requestException) {
// Log and return empty?
$this->getLog()->error('Mastodon: Unable to get posts: ' . $uri
. ', e: ' . $requestException->getMessage());
$dataProvider->addError(__('Unable to download posts'));
} catch (\Exception $exception) {
// Log and return empty?
$this->getLog()->error('Mastodon: ' . $exception->getMessage());
$this->getLog()->debug($exception->getTraceAsString());
$dataProvider->addError(__('Unknown issue getting posts'));
}
return $this;
}
public function getDataCacheKey(DataProviderInterface $dataProvider): ?string
{
// No special cache key requirements.
return null;
}
public function getDataModifiedDt(DataProviderInterface $dataProvider): ?Carbon
{
return null;
}
/**
* Get Mastodon Account Id from username
* @throws GuzzleException
*/
private function getAccountId(string $uri, string $username, DataProviderInterface $dataProvider)
{
$uri = rtrim($uri, '/').'/api/v1/accounts/lookup?';
$httpOptions = [
'timeout' => 20, // wait no more than 20 seconds
'query' => [
'acct' => $username
],
];
$response = $dataProvider
->getGuzzleClient($httpOptions)
->get($uri);
$result = json_decode($response->getBody()->getContents(), true);
$this->getLog()->debug('Mastodon: getAccountId: ID ' . $result['id']);
return $result['id'];
}
}

View File

@@ -0,0 +1,73 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget;
use Carbon\Carbon;
use Xibo\Event\MenuBoardCategoryRequest;
use Xibo\Event\MenuBoardModifiedDtRequest;
use Xibo\Widget\Provider\DataProviderInterface;
use Xibo\Widget\Provider\DurationProviderInterface;
use Xibo\Widget\Provider\WidgetProviderInterface;
use Xibo\Widget\Provider\WidgetProviderTrait;
/**
* Menu Board Category Provider
*/
class MenuBoardCategoryProvider implements WidgetProviderInterface
{
use WidgetProviderTrait;
public function fetchData(DataProviderInterface $dataProvider): WidgetProviderInterface
{
$this->getLog()->debug('fetchData: MenuBoardCategoryRequest passing to event');
$this->getDispatcher()->dispatch(new MenuBoardCategoryRequest($dataProvider), MenuBoardCategoryRequest::$NAME);
return $this;
}
public function fetchDuration(DurationProviderInterface $durationProvider): WidgetProviderInterface
{
return $this;
}
public function getDataCacheKey(DataProviderInterface $dataProvider): ?string
{
return null;
}
public function getDataModifiedDt(DataProviderInterface $dataProvider): ?Carbon
{
$this->getLog()->debug('fetchData: MenuBoardCategoryProvider 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());
}
return null;
}
}

View File

@@ -0,0 +1,84 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget;
use Carbon\Carbon;
use Xibo\Event\MenuBoardModifiedDtRequest;
use Xibo\Event\MenuBoardProductRequest;
use Xibo\Widget\Provider\DataProviderInterface;
use Xibo\Widget\Provider\DurationProviderInterface;
use Xibo\Widget\Provider\WidgetProviderInterface;
use Xibo\Widget\Provider\WidgetProviderTrait;
/**
* Menu Board Product Provider
*/
class MenuBoardProductProvider implements WidgetProviderInterface
{
use WidgetProviderTrait;
public function fetchData(DataProviderInterface $dataProvider): WidgetProviderInterface
{
$this->getLog()->debug('fetchData: MenuBoardProductProvider passing to event');
$this->getDispatcher()->dispatch(new MenuBoardProductRequest($dataProvider), MenuBoardProductRequest::$NAME);
return $this;
}
public function fetchDuration(DurationProviderInterface $durationProvider): WidgetProviderInterface
{
if ($durationProvider->getWidget()->getOptionValue('durationIsPerItem', 0) == 1) {
$this->getLog()->debug('fetchDuration: duration is per item');
$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($durationProvider->getWidget()->calculatedDuration * $numItems);
}
return $this;
}
public function getDataCacheKey(DataProviderInterface $dataProvider): ?string
{
return null;
}
public function getDataModifiedDt(DataProviderInterface $dataProvider): ?Carbon
{
$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;
}
}
}

View File

@@ -0,0 +1,59 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget;
use Carbon\Carbon;
use Xibo\Event\NotificationDataRequestEvent;
use Xibo\Event\NotificationModifiedDtRequestEvent;
use Xibo\Widget\Provider\DataProviderInterface;
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
{
$this->getLog()->debug('fetchData: NotificationProvider passing to event');
$this->getDispatcher()->dispatch(
new NotificationDataRequestEvent($dataProvider),
NotificationDataRequestEvent::$NAME
);
return $this;
}
public function getDataCacheKey(DataProviderInterface $dataProvider): ?string
{
return null;
}
public function getDataModifiedDt(DataProviderInterface $dataProvider): ?Carbon
{
$event = new NotificationModifiedDtRequestEvent($dataProvider->getDisplayId());
$this->getDispatcher()->dispatch($event, NotificationModifiedDtRequestEvent::$NAME);
return $event->getModifiedDt();
}
}

View File

@@ -0,0 +1,85 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget;
use Carbon\Carbon;
use Mpdf\Mpdf;
use Xibo\Widget\Provider\DataProviderInterface;
use Xibo\Widget\Provider\DurationProviderInterface;
use Xibo\Widget\Provider\WidgetProviderInterface;
use Xibo\Widget\Provider\WidgetProviderTrait;
/**
* PDF provider to calculate the duration if durationIsPerItem is selected.
*/
class PdfProvider implements WidgetProviderInterface
{
use WidgetProviderTrait;
public function fetchData(DataProviderInterface $dataProvider): WidgetProviderInterface
{
return $this;
}
public function fetchDuration(DurationProviderInterface $durationProvider): WidgetProviderInterface
{
$widget = $durationProvider->getWidget();
if ($widget->getOptionValue('durationIsPerItem', 0) == 1) {
// Do we already have an option stored for the number of pages?
$pageCount = 1;
$cachedPageCount = $widget->getOptionValue('pageCount', null);
if ($cachedPageCount === null) {
try {
$sourceFile = $widget->getPrimaryMediaPath();
$this->getLog()->debug('fetchDuration: loading PDF file to get the number of pages, file: '
. $sourceFile);
$mPdf = new Mpdf([
'tempDir' => $widget->getLibraryTempPath(),
]);
$pageCount = $mPdf->setSourceFile($sourceFile);
$widget->setOptionValue('pageCount', 'attrib', $pageCount);
} catch (\Exception $e) {
$this->getLog()->error('fetchDuration: unable to get PDF page count, e: ' . $e->getMessage());
}
} else {
$pageCount = $cachedPageCount;
}
$durationProvider->setDuration($durationProvider->getWidget()->calculatedDuration * $pageCount);
}
return $this;
}
public function getDataCacheKey(DataProviderInterface $dataProvider): ?string
{
return null;
}
public function getDataModifiedDt(DataProviderInterface $dataProvider): ?Carbon
{
return null;
}
}

View File

@@ -0,0 +1,449 @@
<?php
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Provider;
use Carbon\Carbon;
use GuzzleHttp\Client;
use Stash\Interfaces\PoolInterface;
use Xibo\Entity\Module;
use Xibo\Entity\Widget;
use Xibo\Factory\MediaFactory;
use Xibo\Helper\SanitizerService;
use Xibo\Support\Sanitizer\SanitizerInterface;
/**
* Xibo default implementation of a Widget Data Provider
*/
class DataProvider implements DataProviderInterface
{
/** @var \Xibo\Factory\MediaFactory */
private $mediaFactory;
/** @var boolean should we use the event? */
private $isUseEvent = false;
/** @var bool Is this data provider handled? */
private $isHandled = false;
/** @var array errors */
private $errors = [];
/** @var array the data */
private $data = [];
/** @var array the metadata */
private $meta = [];
/** @var \Xibo\Entity\Media[] */
private $media = [];
/** @var int the cache ttl in seconds - default to 7 days */
private $cacheTtl = 86400 * 7;
/** @var int the displayId */
private $displayId = 0;
/** @var float the display latitude */
private $latitude;
/** @var float the display longitude */
private $longitude;
/** @var bool Is this data provider in preview mode? */
private $isPreview = false;
/** @var \GuzzleHttp\Client */
private $client;
/** @var null cached property values. */
private $properties = null;
/** @var null cached setting values. */
private $settings = null;
/**
* Constructor
* @param Module $module
* @param Widget $widget
* @param array $guzzleProxy
* @param SanitizerService $sanitizer
* @param PoolInterface $pool
*/
public function __construct(
private readonly Module $module,
private readonly Widget $widget,
private readonly array $guzzleProxy,
private readonly SanitizerService $sanitizer,
private readonly PoolInterface $pool
) {
}
/**
* Set the latitude and longitude for this data provider.
* This is primary used if a widget is display specific
* @param $latitude
* @param $longitude
* @param int $displayId
* @return \Xibo\Widget\Provider\DataProviderInterface
*/
public function setDisplayProperties($latitude, $longitude, int $displayId = 0): DataProviderInterface
{
$this->latitude = $latitude;
$this->longitude = $longitude;
$this->displayId = $displayId;
return $this;
}
/**
* @param \Xibo\Factory\MediaFactory $mediaFactory
* @return \Xibo\Widget\Provider\DataProviderInterface
*/
public function setMediaFactory(MediaFactory $mediaFactory): DataProviderInterface
{
$this->mediaFactory = $mediaFactory;
return $this;
}
/**
* Set whether this data provider is in preview mode
* @param bool $isPreview
* @return DataProviderInterface
*/
public function setIsPreview(bool $isPreview): DataProviderInterface
{
$this->isPreview = $isPreview;
return $this;
}
/**
* @inheritDoc
*/
public function getDataSource(): string
{
return $this->module->type;
}
/**
* @inheritDoc
*/
public function getDataType(): string
{
return $this->module->dataType;
}
/**
* @inheritDoc
*/
public function getDisplayId(): int
{
return $this->displayId ?? 0;
}
/**
* @inheritDoc
*/
public function getDisplayLatitude(): ?float
{
return $this->latitude;
}
/**
* @inheritDoc
*/
public function getDisplayLongitude(): ?float
{
return $this->longitude;
}
/**
* @inheritDoc
*/
public function isPreview(): bool
{
return $this->isPreview;
}
/**
* @inheritDoc
*/
public function getWidgetId(): int
{
return $this->widget->widgetId;
}
/**
* @inheritDoc
*/
public function getProperty(string $property, $default = null)
{
if ($this->properties === null) {
$this->properties = $this->module->getPropertyValues(false);
}
$value = $this->properties[$property] ?? $default;
if (is_integer($default)) {
return intval($value);
} else if (is_numeric($value)) {
return doubleval($value);
}
return $value;
}
/**
* @inheritDoc
*/
public function getSetting(string $setting, $default = null)
{
if ($this->settings === null) {
foreach ($this->module->settings as $item) {
$this->settings[$item->id] = $item->value ?: $item->default;
}
}
return $this->settings[$setting] ?? $default;
}
/**
* Is this data provider handled?
* @return bool
*/
public function isHandled(): bool
{
return $this->isHandled;
}
/**
* @inheritDoc
*/
public function setIsUseEvent(): DataProviderInterface
{
$this->isUseEvent = true;
return $this;
}
/**
* @inheritDoc
*/
public function setIsHandled(): DataProviderInterface
{
$this->isHandled = true;
return $this;
}
/**
* @inheritDoc
*/
public function getData(): array
{
return $this->data;
}
/**
* @inheritDoc
*/
public function getMeta(): array
{
return $this->meta;
}
/**
* Get any errors recorded on this provider
* @return array
*/
public function getErrors(): array
{
return $this->errors;
}
/**
* @inheritDoc
*/
public function getWidgetModifiedDt(): ?Carbon
{
return Carbon::createFromTimestamp($this->widget->modifiedDt);
}
/**
* @inheritDoc
*/
public function addError(string $errorMessage): DataProviderInterface
{
$this->errors[] = $errorMessage;
return $this;
}
/**
* @inheritDoc
*/
public function addItem($item): DataProviderInterface
{
if (!is_array($item) && !is_object($item)) {
throw new \RuntimeException('Item must be an array or an object');
}
if (is_object($item) && !($item instanceof \JsonSerializable)) {
throw new \RuntimeException('Item must be JSON serilizable');
}
$this->data[] = $item;
return $this;
}
/**
* @inheritDoc
*/
public function addItems(array $items): DataProviderInterface
{
foreach ($items as $item) {
$this->addItem($item);
}
return $this;
}
/**
* @inheritDoc
*/
public function addOrUpdateMeta(string $key, $item): DataProviderInterface
{
if (!is_array($item) && (is_object($item) && !$item instanceof \JsonSerializable)) {
throw new \RuntimeException('Item must be an array or a JSON serializable object');
}
$this->meta[$key] = $item;
return $this;
}
/**
* @inheritDoc
*/
public function addImage(string $id, string $url, int $expiresAt): string
{
$media = $this->mediaFactory->queueDownload($id, $url, $expiresAt);
$this->media[] = $media;
return '[[mediaId=' . $media->mediaId . ']]';
}
/**
* @inheritDoc
*/
public function addLibraryFile(int $mediaId): string
{
$media = $this->mediaFactory->getById($mediaId);
$this->media[] = $media;
return '[[mediaId=' . $media->mediaId . ']]';
}
/**
* @return \Xibo\Entity\Media[]
*/
public function getImages(): array
{
return $this->media;
}
/**
* @return int[]
*/
public function getImageIds(): array
{
$mediaIds = [];
foreach ($this->getImages() as $media) {
$mediaIds[] = $media->mediaId;
}
return $mediaIds;
}
/**
* @inheritDoc
*/
public function clearData(): DataProviderInterface
{
$this->media = [];
$this->data = [];
return $this;
}
/**
* @inheritDoc
*/
public function clearMeta(): DataProviderInterface
{
$this->meta = [];
return $this;
}
/**
* @inheritDoc
*/
public function isUseEvent(): bool
{
return $this->isUseEvent;
}
/**
* @inheritDoc
*/
public function setCacheTtl(int $ttlSeconds): DataProviderInterface
{
$this->cacheTtl = $ttlSeconds;
return $this;
}
/**
* @inheritDoc
*/
public function getCacheTtl(): int
{
return $this->cacheTtl;
}
/**
* @inheritDoc
*/
public function getGuzzleClient(array $requestOptions = []): Client
{
if ($this->client === null) {
$this->client = new Client(array_merge($this->guzzleProxy, $requestOptions));
}
return $this->client;
}
/**
* @inheritDoc
*/
public function getPool(): PoolInterface
{
return $this->pool;
}
/**
* @inheritDoc
*/
public function getSanitizer(array $params): SanitizerInterface
{
return $this->sanitizer->getSanitizer($params);
}
}

View File

@@ -0,0 +1,205 @@
<?php
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Provider;
use Carbon\Carbon;
use GuzzleHttp\Client;
use Stash\Interfaces\PoolInterface;
use Xibo\Support\Sanitizer\SanitizerInterface;
/**
* Data Provider
* -------------
* A data provider is passed to a Widget which specifies a class in its configuration file
* It should return data for the widget in the formated expected by the widgets datatype
*
* The widget might provid a class for other reasons and wish to use the widget.request.data event
* to supply its data. In which case it should set is `setIsUseEvent()`.
*
* void methods on the data provider are chainable.
*/
interface DataProviderInterface
{
/**
* Get the data source expected by this provider
* This will be the Module type that requested the provider
* @return string
*/
public function getDataSource(): string;
/**
* Get the datatype expected by this provider
* @return string
*/
public function getDataType(): string;
/**
* Get the ID for this display
* @return int
*/
public function getDisplayId(): int;
/**
* Get the latitude for this display
* @return float|null
*/
public function getDisplayLatitude(): ?float;
/**
* Get the longitude for this display
* @return float|null
*/
public function getDisplayLongitude(): ?float;
/**
* Get the preview flag
* @return bool
*/
public function isPreview(): bool;
/**
* Get the ID for this Widget
* @return int
*/
public function getWidgetId(): int;
/**
* Get a configured Guzzle client
* this will have its proxy configuration set and be ready to use.
* @param array $requestOptions An optional array of additional request options.
* @return Client
*/
public function getGuzzleClient(array $requestOptions = []): Client;
/**
* Get a cache pool interface
* this will be a cache pool configured using the CMS settings.
* @return PoolInterface
*/
public function getPool(): PoolInterface;
/**
* Get property
* Properties are set on Widgets and can be things like "feedUrl"
* the property must exist in module properties for this type of widget
* @param string $property The property name
* @param mixed $default An optional default value. The return will be cast to the datatype of this default value.
* @return mixed
*/
public function getProperty(string $property, $default = null);
/**
* Get setting
* Settings are set on Modules and can be things like "apiKey"
* the setting must exist in module settings for this type of widget
* @param string $setting The setting name
* @param mixed $default An optional default value. The return will be cast to the datatype of this default value.
* @return mixed
*/
public function getSetting(string $setting, $default = null);
/**
* Get a Santiziter
* @param array $params key/value array of variable to sanitize
* @return SanitizerInterface
*/
public function getSanitizer(array $params): SanitizerInterface;
/**
* Get the widget modifiedDt
* @return \Carbon\Carbon|null
*/
public function getWidgetModifiedDt(): ?Carbon;
/**
* Indicate that we should use the event mechanism to handle this event.
* @return \Xibo\Widget\Provider\DataProviderInterface
*/
public function setIsUseEvent(): DataProviderInterface;
/**
* Indicate that this data provider has been handled.
* @return DataProviderInterface
*/
public function setIsHandled(): DataProviderInterface;
/**
* Add an error to this data provider, if no other data providers handle this request, the error will be
* thrown as a configuration error.
* @param string $errorMessage
* @return DataProviderInterface
*/
public function addError(string $errorMessage): DataProviderInterface;
/**
* Add an item to the provider
* You should ensure that you provide all properties required by the datatype you are returning
* example data types would be: article, social, event, menu, tabular
* @param array|object $item An array containing the item to render in any templates used by this data provider
* @return \Xibo\Widget\Provider\DataProviderInterface
*/
public function addItem($item): DataProviderInterface;
/**
* Add items to the provider
* You should ensure that you provide all properties required by the datatype you are returning
* example data types would be: article, social, event, menu, tabular
* @param array $items An array containing the item to render in any templates used by this data provider
* @return \Xibo\Widget\Provider\DataProviderInterface
*/
public function addItems(array $items): DataProviderInterface;
/**
* Add metadata to the provider
* This is a key/value array of metadata which should be delivered alongside the data
* @param string $key
* @param mixed $item An array/object containing the metadata, which must be JSON serializable
* @return \Xibo\Widget\Provider\DataProviderInterface
*/
public function addOrUpdateMeta(string $key, $item): DataProviderInterface;
/**
* Add an image to the data provider and return the URL for that image
* @param string $id A unique ID for this image, we recommend adding a module/connector specific prefix
* @param string $url The URL on which this image should be downloaded
* @param int $expiresAt A unix timestamp for when this image should be removed - should be longer than cache ttl
* @return string
* @throws \Xibo\Support\Exception\GeneralException
*/
public function addImage(string $id, string $url, int $expiresAt): string;
/**
* Add a library file
* @param int $mediaId The mediaId for this file.
* @return string
* @throws \Xibo\Support\Exception\GeneralException
*/
public function addLibraryFile(int $mediaId): string;
/**
* Set the cache TTL
* @param int $ttlSeconds The time to live in seconds
* @return \Xibo\Widget\Provider\DataProviderInterface
*/
public function setCacheTtl(int $ttlSeconds): DataProviderInterface;
}

View File

@@ -0,0 +1,97 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Provider;
use Xibo\Entity\Module;
use Xibo\Entity\Widget;
/**
* Xibo's default implementation of the Duration Provider
*/
class DurationProvider implements DurationProviderInterface
{
/** @var Module */
private $module;
/** @var Widget */
private $widget;
/** @var int Duration in seconds */
private $duration;
/** @var bool Has the duration been set? */
private $isDurationSet = false;
/**
* Constructor
* @param Module $module
* @param Widget $widget
*/
public function __construct(Module $module, Widget $widget)
{
$this->module = $module;
$this->widget = $widget;
}
/**
* @inheritDoc
*/
public function setDuration(int $seconds): DurationProviderInterface
{
$this->isDurationSet = true;
$this->duration = $seconds;
return $this;
}
/**
* @inheritDoc
*/
public function getDuration(): int
{
return $this->duration ?? 0;
}
/**
* @inheritDoc
*/
public function isDurationSet(): bool
{
return $this->isDurationSet;
}
/**
* @inheritDoc
*/
public function getModule(): Module
{
return $this->module;
}
/**
* @inheritDoc
*/
public function getWidget(): Widget
{
return $this->widget;
}
}

View File

@@ -0,0 +1,62 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\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 Module
* @return Module
*/
public function getModule(): Module;
/**
* Get the Widget
* @return Widget
*/
public function getWidget(): Widget;
/**
* Get the duration
* @return int the duration in seconds
*/
public function getDuration(): int;
/**
* Set the duration in seconds
* @param int $seconds the duration in seconds
* @return \Xibo\Widget\Provider\DurationProviderInterface
*/
public function setDuration(int $seconds): DurationProviderInterface;
/**
* @return bool true if the duration has been set
*/
public function isDurationSet(): bool;
}

View File

@@ -0,0 +1,50 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\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;
}
}

View File

@@ -0,0 +1,61 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Provider;
use Psr\Log\LoggerInterface;
use Xibo\Entity\Widget;
/**
* Widget Compatibility Interface should be implemented by custom widget upgrade classes.
* Takes necessary actions to make the existing widgets from v3 compatible with v4.
*
* The schema from and the schema to (currently set to 1 and 2, respectively).
* It also provides a method to save a template to the library in a sub-folder named templates/. This method
* is called whenever a widget is loaded with a different schema version.
*
*/
interface WidgetCompatibilityInterface
{
public function getLog(): LoggerInterface;
public function setLog(LoggerInterface $logger): WidgetCompatibilityInterface;
/**
* Upgrade the given widget to be compatible with the specified schema version.
*
* @param Widget $widget The widget model to upgrade.
* @param int $fromSchema The version of the schema the widget is currently using.
* @param int $toSchema The version of the schema to upgrade the widget to.
* @return bool Whether the upgrade was successful
*/
public function upgradeWidget(Widget $widget, int $fromSchema, int $toSchema): bool;
/**
* Save the given widget template to the templates/ subfolder.
*
* @param string $template The widget template to save.
* @param string $fileName The file name to save the template as.
* @return bool Returns true if the template was saved successfully, false otherwise.
*/
public function saveTemplate(string $template, string $fileName): bool;
}

View File

@@ -0,0 +1,48 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Provider;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
/**
* A trait to set common objects on a Widget Compatibility Interface
*/
trait WidgetCompatibilityTrait
{
private $log;
public function getLog(): LoggerInterface
{
if ($this->log === null) {
$this->log = new NullLogger();
}
return $this->log;
}
public function setLog(LoggerInterface $logger): WidgetCompatibilityInterface
{
$this->log = $logger;
return $this;
}
}

View File

@@ -0,0 +1,97 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Provider;
use Carbon\Carbon;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* The Widget Provider Interface should be implemented by any Widget which specifies a `class` in its Module
* configuration.
*
* The provider should be modified accordingly before returning $this
*
* If the widget does not need to fetch Data or fetch Duration, then it can return without
* modifying the provider.
*/
interface WidgetProviderInterface
{
public function getLog(): LoggerInterface;
public function setLog(LoggerInterface $logger): WidgetProviderInterface;
/**
* Get the event dispatcher
* @return \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
public function getDispatcher(): EventDispatcherInterface;
/**
* Set the event dispatcher
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $logger
* @return \Xibo\Widget\Provider\WidgetProviderInterface
*/
public function setDispatcher(EventDispatcherInterface $logger): WidgetProviderInterface;
/**
* Fetch data
* The widget provider must either addItems to the data provider, or indicate that data is provided by
* an event instead by setting isUseEvent()
* If data is to be provided by an event, core will raise the `widget.request.data` event with parameters
* indicating this widget's datatype, name, settings and currently configured options
* @param \Xibo\Widget\Provider\DataProviderInterface $dataProvider
* @return \Xibo\Widget\Provider\WidgetProviderInterface
* @throws \Xibo\Support\Exception\GeneralException
*/
public function fetchData(DataProviderInterface $dataProvider): WidgetProviderInterface;
/**
* Fetch duration
* This is typically only relevant to widgets which have a media file associated, for example video or audio
* in cases where this is not appropriate, return without modifying to use the module default duration from
* module configuration.
* @param \Xibo\Widget\Provider\DurationProviderInterface $durationProvider
* @return \Xibo\Widget\Provider\WidgetProviderInterface
*/
public function fetchDuration(DurationProviderInterface $durationProvider): WidgetProviderInterface;
/**
* Get data cache key
* Use this method to return a cache key for this widget. This is typically only relevant when the data cache
* should be different based on the value of a setting. For example, if the tweetDistance is set on a Twitter
* widget, then the cache should be by displayId. If the cache is always by displayId, then you should supply
* the `dataCacheKey` via module config XML instead.
* @param \Xibo\Widget\Provider\DataProviderInterface $dataProvider
* @return string|null
*/
public function getDataCacheKey(DataProviderInterface $dataProvider): ?string;
/**
* Get data modified date
* Use this method to invalidate cache ahead of its expiry date/time by returning the date/time that the underlying
* data is expected to or has been modified
* @param \Xibo\Widget\Provider\DataProviderInterface $dataProvider
* @return \Carbon\Carbon|null
*/
public function getDataModifiedDt(DataProviderInterface $dataProvider): ?Carbon;
}

View File

@@ -0,0 +1,67 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Provider;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* A trait to set common objects on a Widget Provider Interface
*/
trait WidgetProviderTrait
{
private $log;
private $dispatcher;
public function getLog(): LoggerInterface
{
if ($this->log === null) {
$this->log = new NullLogger();
}
return $this->log;
}
public function setLog(LoggerInterface $logger): WidgetProviderInterface
{
$this->log = $logger;
return $this;
}
/** @inheritDoc */
public function getDispatcher(): EventDispatcherInterface
{
if ($this->dispatcher === null) {
$this->dispatcher = new EventDispatcher();
}
return $this->dispatcher;
}
/** @inheritDoc */
public function setDispatcher(EventDispatcherInterface $dispatcher): WidgetProviderInterface
{
$this->dispatcher = $dispatcher;
return $this;
}
}

View File

@@ -0,0 +1,48 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Provider;
use Psr\Log\LoggerInterface;
use Xibo\Entity\Module;
use Xibo\Entity\Widget;
/**
* Widget Validator Interface
* --------------------------
* Used to validate the properties of a module after it all of its individual properties and those of its
* template have been validated via their property rules.
*/
interface WidgetValidatorInterface
{
public function getLog(): LoggerInterface;
public function setLog(LoggerInterface $logger): WidgetValidatorInterface;
/**
* Validate the widget provided
* @param Module $module The Module
* @param Widget $widget The Widget - this is read only
* @param string $stage Which stage are we validating, either `save` or `status`
*/
public function validate(Module $module, Widget $widget, string $stage): void;
}

View File

@@ -0,0 +1,48 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Provider;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
/**
* A trait to set common objects on a Widget Compatibility Interface
*/
trait WidgetValidatorTrait
{
private $log;
public function getLog(): LoggerInterface
{
if ($this->log === null) {
$this->log = new NullLogger();
}
return $this->log;
}
public function setLog(LoggerInterface $logger): WidgetValidatorInterface
{
$this->log = $logger;
return $this;
}
}

View File

@@ -0,0 +1,517 @@
<?php
/*
* Copyright (C) 2025 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Render;
use Carbon\Carbon;
use Illuminate\Support\Str;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Stash\Interfaces\PoolInterface;
use Stash\Invalidation;
use Stash\Item;
use Xibo\Entity\Display;
use Xibo\Helper\DateFormatHelper;
use Xibo\Helper\LinkSigner;
use Xibo\Helper\ObjectVars;
use Xibo\Service\ConfigServiceInterface;
use Xibo\Support\Exception\GeneralException;
use Xibo\Widget\Provider\DataProvider;
use Xibo\Widget\Provider\DataProviderInterface;
use Xibo\Xmds\Wsdl;
/**
* Acts as a cache for the Widget data cache.
*/
class WidgetDataProviderCache
{
/** @var LoggerInterface */
private $logger;
/** @var \Stash\Interfaces\PoolInterface */
private $pool;
/** @var Item */
private $lock;
/** @var Item */
private $cache;
/** @var string The cache key */
private $key;
/** @var bool Is the cache a miss or old */
private $isMissOrOld = true;
private $cachedMediaIds;
/**
* @param \Stash\Interfaces\PoolInterface $pool
*/
public function __construct(PoolInterface $pool)
{
$this->pool = $pool;
}
/**
* @param \Psr\Log\LoggerInterface $logger
* @return $this
*/
public function useLogger(LoggerInterface $logger): WidgetDataProviderCache
{
$this->logger = $logger;
return $this;
}
/**
* @return \Psr\Log\LoggerInterface
*/
private function getLog(): LoggerInterface
{
if ($this->logger === null) {
$this->logger = new NullLogger();
}
return $this->logger;
}
/**
* Decorate this data provider with cache
* @param DataProvider $dataProvider
* @param string $cacheKey
* @param Carbon|null $dataModifiedDt The date any associated data was modified.
* @param bool $isLockIfMiss Should the cache be locked if it's a miss? Defaults to true.
* @return bool
* @throws \Xibo\Support\Exception\GeneralException
*/
public function decorateWithCache(
DataProvider $dataProvider,
string $cacheKey,
?Carbon $dataModifiedDt,
bool $isLockIfMiss = true,
): bool {
// Construct a key
$this->key = '/widget/'
. ($dataProvider->getDataType() ?: $dataProvider->getDataSource())
. '/' . md5($cacheKey);
$this->getLog()->debug('decorateWithCache: key is ' . $this->key);
// Get the cache
$this->cache = $this->pool->getItem($this->key);
// Invalidation method old means that if this cache key is being regenerated concurrently to this request
// we return the old data we have stored already.
$this->cache->setInvalidationMethod(Invalidation::OLD);
// Get the data (this might be OLD data)
$data = $this->cache->get();
$cacheCreationDt = $this->cache->getCreation();
// Does the cache have data?
// we keep data 50% longer than we need to, so that it has a chance to be regenerated out of band
if ($data === null) {
$this->getLog()->debug('decorateWithCache: miss, no data');
$hasData = false;
} else {
$hasData = true;
// Clear the data provider and add the cached items back to it.
$dataProvider->clearData();
$dataProvider->clearMeta();
$dataProvider->addItems($data->data ?? []);
// Record any cached mediaIds
$this->cachedMediaIds = $data->media ?? [];
// Update any meta
foreach (($data->meta ?? []) as $key => $item) {
$dataProvider->addOrUpdateMeta($key, $item);
}
// Determine whether this cache is a miss (i.e. expired and being regenerated, expired, out of date)
// We use our own expireDt here because Stash will only return expired data with invalidation method OLD
// if the data is currently being regenerated and another process has called lock() on it
$expireDt = $dataProvider->getMeta()['expireDt'] ?? null;
if ($expireDt !== null) {
$expireDt = Carbon::createFromFormat('c', $expireDt);
} else {
$expireDt = $this->cache->getExpiration();
}
// Determine if the cache returned is a miss or older than the modified/expired dates
$this->isMissOrOld = $this->cache->isMiss()
|| ($dataModifiedDt !== null && $cacheCreationDt !== false && $dataModifiedDt->isAfter($cacheCreationDt)
|| ($expireDt->isBefore(Carbon::now()))
);
$this->getLog()->debug('decorateWithCache: cache has data, is miss or old: '
. var_export($this->isMissOrOld, true));
}
// If we do not have data/we're old/missed cache, and we have requested a lock, then we will be refreshing
// the cache, so lock the record
if ($isLockIfMiss && (!$hasData || $this->isMissOrOld)) {
$this->concurrentRequestLock();
}
return $hasData;
}
/**
* Is the cache a miss, or old data.
* @return bool
*/
public function isCacheMissOrOld(): bool
{
return $this->isMissOrOld;
}
/**
* Get the cache date for this data provider and key
* @param DataProvider $dataProvider
* @param string $cacheKey
* @return Carbon|null
*/
public function getCacheDate(DataProvider $dataProvider, string $cacheKey): ?Carbon
{
// Construct a key
$this->key = '/widget/'
. ($dataProvider->getDataType() ?: $dataProvider->getDataSource())
. '/' . md5($cacheKey);
$this->getLog()->debug('getCacheDate: key is ' . $this->key);
// Get the cache
$this->cache = $this->pool->getItem($this->key);
$cacheCreationDt = $this->cache->getCreation();
return $cacheCreationDt ? Carbon::instance($cacheCreationDt) : null;
}
/**
* @param DataProviderInterface $dataProvider
* @throws \Xibo\Support\Exception\GeneralException
*/
public function saveToCache(DataProviderInterface $dataProvider): void
{
if ($this->cache === null) {
throw new GeneralException('No cache to save');
}
// Set some cache dates so that we can track when this data provider was cached and when it should expire.
$dataProvider->addOrUpdateMeta('cacheDt', Carbon::now()->format('c'));
$dataProvider->addOrUpdateMeta(
'expireDt',
Carbon::now()->addSeconds($dataProvider->getCacheTtl())->format('c')
);
// Set our cache from the data provider.
$object = new \stdClass();
$object->data = $dataProvider->getData();
$object->meta = $dataProvider->getMeta();
$object->media = $dataProvider->getImageIds();
$cached = $this->cache->set($object);
if (!$cached) {
throw new GeneralException('Cache failure');
}
// Keep the cache 50% longer than necessary
// The expireDt must always be 15 minutes to allow plenty of time for the WidgetSyncTask to regenerate.
$this->cache->expiresAfter(ceil(max($dataProvider->getCacheTtl() * 1.5, 900)));
// Save to the pool
$this->pool->save($this->cache);
$this->getLog()->debug('saveToCache: cached ' . $this->key
. ' for ' . $dataProvider->getCacheTtl() . ' seconds');
}
/**
* Finalise the cache process
*/
public function finaliseCache(): void
{
$this->concurrentRequestRelease();
}
/**
* Return any cached mediaIds
* @return array
*/
public function getCachedMediaIds(): array
{
return $this->cachedMediaIds ?? [];
}
/**
* Decorate for a preview
* @param array $data The data
* @param callable $urlFor
* @return array
*/
public function decorateForPreview(array $data, callable $urlFor): array
{
foreach ($data as $row => $item) {
// This is either an object or an array
if (is_array($item)) {
foreach ($item as $key => $value) {
if (is_string($value)) {
$data[$row][$key] = $this->decorateMediaForPreview($urlFor, $value);
}
}
} else if (is_object($item)) {
foreach (ObjectVars::getObjectVars($item) as $key => $value) {
if (is_string($value)) {
$item->{$key} = $this->decorateMediaForPreview($urlFor, $value);
}
}
}
}
return $data;
}
/**
* @param callable $urlFor
* @param string|null $data
* @return string|null
*/
private function decorateMediaForPreview(callable $urlFor, ?string $data): ?string
{
if ($data === null) {
return null;
}
$matches = [];
preg_match_all('/\[\[(.*?)\]\]/', $data, $matches);
foreach ($matches[1] as $match) {
if (Str::startsWith($match, 'mediaId')) {
$value = explode('=', $match);
$data = str_replace(
'[[' . $match . ']]',
$urlFor('library.download', ['id' => $value[1], 'type' => 'image']),
$data
);
} else if (Str::startsWith($match, 'connector')) {
$value = explode('=', $match);
$data = str_replace(
'[[' . $match . ']]',
$urlFor('layout.preview.connector', [], ['token' => $value[1], 'isDebug' => 1]),
$data
);
}
}
return $data;
}
/**
* Decorate for a player
* @param \Xibo\Service\ConfigServiceInterface $configService
* @param \Xibo\Entity\Display $display
* @param string $encryptionKey
* @param array $data The data
* @param array $storedAs A keyed array of module files this widget has access to
* @return array
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function decorateForPlayer(
ConfigServiceInterface $configService,
Display $display,
string $encryptionKey,
array $data,
array $storedAs,
): array {
$this->getLog()->debug('decorateForPlayer');
$cdnUrl = $configService->getSetting('CDN_URL');
foreach ($data as $row => $item) {
// Each data item can be an array or an object
if (is_array($item)) {
foreach ($item as $key => $value) {
if (is_string($value)) {
$data[$row][$key] = $this->decorateMediaForPlayer(
$cdnUrl,
$display,
$encryptionKey,
$storedAs,
$value,
);
}
}
} else if (is_object($item)) {
foreach (ObjectVars::getObjectVars($item) as $key => $value) {
if (is_string($value)) {
$item->{$key} = $this->decorateMediaForPlayer(
$cdnUrl,
$display,
$encryptionKey,
$storedAs,
$value
);
}
}
}
}
return $data;
}
/**
* @param string|null $cdnUrl
* @param \Xibo\Entity\Display $display
* @param string $encryptionKey
* @param array $storedAs
* @param string|null $data
* @return string|null
* @throws \Xibo\Support\Exception\NotFoundException
*/
private function decorateMediaForPlayer(
?string $cdnUrl,
Display $display,
string $encryptionKey,
array $storedAs,
?string $data,
): ?string {
if ($data === null) {
return null;
}
// Do we need to add a URL prefix to the requests?
$prefix = $display->isPwa() ? '/pwa/' : '';
// Media substitutes
$matches = [];
preg_match_all('/\[\[(.*?)\]\]/', $data, $matches);
foreach ($matches[1] as $match) {
if (Str::startsWith($match, 'mediaId')) {
$value = explode('=', $match);
if (array_key_exists($value[1], $storedAs)) {
if ($display->isPwa()) {
$url = LinkSigner::generateSignedLink(
$display,
$encryptionKey,
$cdnUrl,
'M',
$value[1],
$storedAs[$value[1]],
null,
true,
);
} else {
$url = $storedAs[$value[1]];
}
$data = str_replace('[[' . $match . ']]', $prefix . $url, $data);
} else {
$data = str_replace('[[' . $match . ']]', '', $data);
}
} else if (Str::startsWith($match, 'connector')) {
// We have WSDL here because this is only called from XMDS.
$value = explode('=', $match);
$data = str_replace(
'[[' . $match . ']]',
Wsdl::getRoot() . '?connector=true&token=' . $value[1],
$data
);
}
}
return $data;
}
// <editor-fold desc="Request locking">
/**
* Hold a lock on concurrent requests
* blocks if the request is locked
* @param int $ttl seconds
* @param int $wait seconds
* @param int $tries
* @throws \Xibo\Support\Exception\GeneralException
*/
private function concurrentRequestLock(int $ttl = 300, int $wait = 2, int $tries = 5)
{
if ($this->cache === null) {
throw new GeneralException('No cache to lock');
}
$this->lock = $this->pool->getItem('locks/concurrency/' . $this->cache->getKey());
// Set the invalidation method to simply return the value (not that we use it, but it gets us a miss on expiry)
// isMiss() returns false if the item is missing or expired, no exceptions.
$this->lock->setInvalidationMethod(Invalidation::NONE);
// Get the lock
// other requests will wait here until we're done, or we've timed out
$locked = $this->lock->get();
// Did we get a lock?
// if we're a miss, then we're not already locked
if ($this->lock->isMiss() || $locked === false) {
$this->getLog()->debug('Lock miss or false. Locking for ' . $ttl
. ' seconds. $locked is '. var_export($locked, true)
. ', key = ' . $this->cache->getKey());
// so lock now
$this->lock->set(true);
$this->lock->expiresAfter($ttl);
$this->lock->save();
} else {
// We are a hit - we must be locked
$this->getLog()->debug('LOCK hit for ' . $this->cache->getKey() . ' expires '
. $this->lock->getExpiration()->format(DateFormatHelper::getSystemFormat())
. ', created ' . $this->lock->getCreation()->format(DateFormatHelper::getSystemFormat()));
// Try again?
$tries--;
if ($tries <= 0) {
// We've waited long enough
throw new GeneralException('Concurrent record locked, time out.');
} else {
$this->getLog()->debug('Unable to get a lock, trying again. Remaining retries: ' . $tries);
// Hang about waiting for the lock to be released.
sleep($wait);
// Recursive request (we've decremented the number of tries)
$this->concurrentRequestLock($ttl, $wait, $tries);
}
}
}
/**
* Release a lock on concurrent requests
*/
private function concurrentRequestRelease()
{
if ($this->lock !== null) {
$this->getLog()->debug('Releasing lock ' . $this->lock->getKey());
// Release lock
$this->lock->set(false);
$this->lock->expiresAfter(10); // Expire straight away (but give time to save)
$this->pool->save($this->lock);
}
}
// </editor-fold>
}

View File

@@ -0,0 +1,340 @@
<?php
/*
* Copyright (C) 2025 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Render;
use GuzzleHttp\Psr7\LimitStream;
use GuzzleHttp\Psr7\Stream;
use Intervention\Image\ImageManagerStatic as Img;
use Psr\Log\LoggerInterface;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Entity\Media;
use Xibo\Helper\HttpCacheProvider;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
use Xibo\Support\Sanitizer\SanitizerInterface;
/**
* A helper class to download widgets from the library (as media files)
*/
class WidgetDownloader
{
/** @var LoggerInterface */
private LoggerInterface $logger;
/**
* @param string $libraryLocation Library location
* @param string $sendFileMode Send file mode
* @param int $resizeLimit CMS resize limit
*/
public function __construct(
private readonly string $libraryLocation,
private readonly string $sendFileMode,
private readonly int $resizeLimit
) {
}
/**
* @param \Psr\Log\LoggerInterface $logger
* @return $this
*/
public function useLogger(LoggerInterface $logger): WidgetDownloader
{
$this->logger = $logger;
return $this;
}
/**
* Return File
* @param \Xibo\Entity\Media $media
* @param \Slim\Http\ServerRequest $request
* @param \Slim\Http\Response $response
* @param string|null $contentType An optional content type, if provided the attachment is ignored
* @param string|null $attachment An optional attachment, defaults to the stored file name (storedAs)
* @return \Slim\Http\Response
*/
public function download(
Media $media,
Request $request,
Response $response,
?string $contentType = null,
?string $attachment = null
): Response {
$this->logger->debug('widgetDownloader::download: Download for mediaId ' . $media->mediaId);
// The file path
$libraryPath = $this->libraryLocation . $media->storedAs;
$this->logger->debug('widgetDownloader::download: ' . $libraryPath . ', ' . $contentType);
// Set some headers
$headers = [];
$fileSize = filesize($libraryPath);
$headers['Content-Length'] = $fileSize;
// If we have been given a content type, then serve that to the browser.
if ($contentType !== null) {
$headers['Content-Type'] = $contentType;
} else {
// This widget is expected to output a file - usually this is for file based media
// Get the name with library
$attachmentName = empty($attachment) ? $media->storedAs : $attachment;
// Issue some headers
$response = HttpCacheProvider::withEtag($response, $media->md5);
$response = HttpCacheProvider::withExpires($response, '+1 week');
$headers['Content-Type'] = 'application/octet-stream';
$headers['Content-Transfer-Encoding'] = 'Binary';
$headers['Content-disposition'] = 'attachment; filename="' . $attachmentName . '"';
}
// Output the file
if ($this->sendFileMode === 'Apache') {
// Send via Apache X-Sendfile header?
$headers['X-Sendfile'] = $libraryPath;
} else if ($this->sendFileMode === 'Nginx') {
// Send via Nginx X-Accel-Redirect?
$headers['X-Accel-Redirect'] = '/download/' . $media->storedAs;
}
// Should we output the file via the application stack, or directly by reading the file.
if ($this->sendFileMode == 'Off') {
// Return the file with PHP
$this->logger->debug('download: Returning Stream with response body, sendfile off.');
$stream = new Stream(fopen($libraryPath, 'r'));
$start = 0;
$end = $fileSize - 1;
$rangeHeader = $request->getHeaderLine('Range');
if ($rangeHeader !== '') {
$this->logger->debug('download: Handling Range request, header: ' . $rangeHeader);
if (preg_match('/bytes=(\d+)-(\d*)/', $rangeHeader, $matches)) {
$start = (int) $matches[1];
$end = $matches[2] !== '' ? (int) $matches[2] : $end;
if ($start > $end || $end >= $fileSize) {
return $response
->withStatus(416)
->withHeader('Content-Range', 'bytes */' . $fileSize);
}
}
$headers['Content-Range'] = 'bytes ' . $start . '-' . $end . '/' . $fileSize;
$headers['Content-Length'] = $end - $start + 1;
$response = $response
->withBody(new LimitStream($stream, $end - $start + 1, $start))
->withStatus(206);
} else {
$response = $response->withBody($stream);
}
} else {
$this->logger->debug('Using sendfile to return the file, only output headers.');
}
// Add the headers we've collected to our response
foreach ($headers as $header => $value) {
$response = $response->withHeader($header, $value);
}
return $response;
}
/**
* Download a thumbnail for the given media
* @param \Xibo\Entity\Media $media
* @param \Slim\Http\Response $response
* @param string|null $errorThumb
* @return \Slim\Http\Response
*/
public function thumbnail(
Media $media,
Response $response,
?string $errorThumb = null
): Response {
// Our convention is to upload media covers in {mediaId}_{mediaType}cover.png
// and then thumbnails in tn_{mediaId}_{mediaType}cover.png
// unless we are an image module, which is its own image, and would then have a thumbnail in
// tn_{mediaId}_{mediaType}cover.png
try {
$width = 120;
$height = 120;
if ($media->mediaType === 'image') {
$filePath = $this->libraryLocation . $media->storedAs;
$thumbnailFilePath = $this->libraryLocation . 'tn_' . $media->storedAs;
} else {
$filePath = $this->libraryLocation . $media->mediaId . '_'
. $media->mediaType . 'cover.png';
$thumbnailFilePath = $this->libraryLocation . 'tn_' . $media->mediaId . '_'
. $media->mediaType . 'cover.png';
// A video cover might not exist
if (!file_exists($filePath)) {
throw new NotFoundException();
}
}
// Does the thumbnail exist already?
Img::configure(['driver' => 'gd']);
$img = null;
$regenerate = true;
if (file_exists($thumbnailFilePath)) {
$img = Img::make($thumbnailFilePath);
if ($img->width() === $width || $img->height() === $height) {
// Correct cache
$regenerate = false;
}
$response = $response->withHeader('Content-Type', $img->mime());
}
if ($regenerate) {
// Check that our source image is not too large
$imageInfo = getimagesize($filePath);
// Make sure none of the sides are greater than allowed
if ($this->resizeLimit > 0
&& ($imageInfo[0] > $this->resizeLimit || $imageInfo[1] > $this->resizeLimit)
) {
throw new InvalidArgumentException(__('Image too large'));
}
// Get the full image and make a thumbnail
$img = Img::make($filePath);
$img->resize($width, $height, function ($constraint) {
$constraint->aspectRatio();
});
$img->save($thumbnailFilePath);
$response = $response->withHeader('Content-Type', $img->mime());
}
// Output Etag
$response = HttpCacheProvider::withEtag($response, md5_file($thumbnailFilePath));
$response->write($img->encode());
} catch (\Exception) {
$this->logger->debug('thumbnail: exception raised.');
if ($errorThumb !== null) {
$img = Img::make($errorThumb);
$response->write($img->encode());
// Output the mime type
$response = $response->withHeader('Content-Type', $img->mime());
}
}
return $response;
}
/**
* Output an image preview
* @param \Xibo\Support\Sanitizer\SanitizerInterface $params
* @param string $filePath
* @param \Slim\Http\Response $response
* @param string|null $errorThumb
* @return \Slim\Http\Response
* @throws \Xibo\Support\Exception\InvalidArgumentException
*/
public function imagePreview(
SanitizerInterface $params,
string $filePath,
Response $response,
?string $errorThumb = null
): Response {
// Image previews call for dynamically generated images as various sizes
// for example a background image will stretch to the entire region
// an image widget may be aspect, fit or scale
try {
$filePath = $this->libraryLocation . $filePath;
// Does it exist?
if (!file_exists($filePath)) {
throw new NotFoundException(__('File not found'));
}
// Check that our source image is not too large
$imageInfo = getimagesize($filePath);
// Make sure none of the sides are greater than allowed
if ($this->resizeLimit > 0
&& ($imageInfo[0] > $this->resizeLimit || $imageInfo[1] > $this->resizeLimit)
) {
throw new InvalidArgumentException(__('Image too large'));
}
// Continue to output at the desired size
$width = intval($params->getDouble('width'));
$height = intval($params->getDouble('height'));
$proportional = !$params->hasParam('proportional')
|| $params->getCheckbox('proportional') == 1;
$fit = $proportional && $params->getCheckbox('fit') === 1;
// only use upsize constraint, if we the requested dimensions are larger than resize limit.
$useUpsizeConstraint = max($width, $height) > $this->resizeLimit;
$this->logger->debug('Whole file: ' . $filePath
. ' requested with Width and Height ' . $width . ' x ' . $height
. ', proportional: ' . var_export($proportional, true)
. ', fit: ' . var_export($fit, true)
. ', upsizeConstraint ' . var_export($useUpsizeConstraint, true));
// Does the thumbnail exist already?
Img::configure(['driver' => 'gd']);
$img = Img::make($filePath);
// Output a specific width/height
if ($width > 0 && $height > 0) {
if ($fit) {
$img->fit($width, $height);
} else {
$img->resize($width, $height, function ($constraint) use ($proportional, $useUpsizeConstraint) {
if ($proportional) {
$constraint->aspectRatio();
}
if ($useUpsizeConstraint) {
$constraint->upsize();
}
});
}
}
$response->write($img->encode());
$response = HttpCacheProvider::withExpires($response, '+1 week');
$response = $response->withHeader('Content-Type', $img->mime());
} catch (\Exception $e) {
if ($errorThumb !== null) {
$img = Img::make($errorThumb);
$response->write($img->encode());
$response = $response->withHeader('Content-Type', $img->mime());
} else {
$this->logger->error('Cannot parse image: ' . $e->getMessage());
throw new InvalidArgumentException(__('Cannot parse image.'), 'storedAs');
}
}
return $response;
}
}

File diff suppressed because it is too large Load Diff

281
lib/Widget/RssProvider.php Normal file
View File

@@ -0,0 +1,281 @@
<?php
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget;
use Carbon\Carbon;
use GuzzleHttp\Exception\GuzzleException;
use PicoFeed\Config\Config;
use PicoFeed\Logging\Logger;
use PicoFeed\Parser\Item;
use PicoFeed\PicoFeedException;
use PicoFeed\Reader\Reader;
use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;
use Xibo\Helper\Environment;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Widget\DataType\Article;
use Xibo\Widget\Provider\DataProviderInterface;
use Xibo\Widget\Provider\DurationProviderNumItemsTrait;
use Xibo\Widget\Provider\WidgetProviderInterface;
use Xibo\Widget\Provider\WidgetProviderTrait;
/**
* Downloads an RSS feed and returns Article data types
*/
class RssProvider implements WidgetProviderInterface
{
use WidgetProviderTrait;
use DurationProviderNumItemsTrait;
public function fetchData(DataProviderInterface $dataProvider): WidgetProviderInterface
{
$uri = $dataProvider->getProperty('uri');
if (empty($uri)) {
throw new InvalidArgumentException(__('Please enter the URI to a valid RSS feed.'), 'uri');
}
$picoFeedLoggingEnabled = Environment::isDevMode();
// Image expiry
$expiresImage = Carbon::now()
->addMinutes($dataProvider->getProperty('updateIntervalImages', 1440))
->format('U');
try {
// Get the feed
$response = $this->getFeed($dataProvider, $uri);
// Pull out the content type
$contentType = $response['contentType'];
$this->getLog()->debug('Feed returned content-type ' . $contentType);
// https://github.com/xibosignage/xibo/issues/1401
if (stripos($contentType, 'rss') === false
&& stripos($contentType, 'xml') === false
&& stripos($contentType, 'text') === false
&& stripos($contentType, 'html') === false
) {
// The content type isn't compatible
$this->getLog()->error('Incompatible content type: ' . $contentType);
return $this;
}
// Get the body, etc
$result = explode('charset=', $contentType);
$document['encoding'] = $result[1] ?? '';
$document['xml'] = $response['body'];
$this->getLog()->debug('Feed downloaded.');
// Load the feed XML document into a feed parser
// Enable logging if we need to
if ($picoFeedLoggingEnabled) {
$this->getLog()->debug('Setting Picofeed Logger to Enabled.');
Logger::enable();
}
// Client config
$clientConfig = new Config();
// Get the feed parser
$reader = new Reader($clientConfig);
$parser = $reader->getParser($uri, $document['xml'], $document['encoding']);
// Get a feed object
$feed = $parser->execute();
// Get all items
$feedItems = $feed->getItems();
// Disable date sorting?
if ($dataProvider->getProperty('disableDateSort') == 0
&& $dataProvider->getProperty('randomiseItems', 0) == 0
) {
// Sort the items array by date
usort($feedItems, function ($a, $b) {
/* @var Item $a */
/* @var Item $b */
return $b->getDate()->getTimestamp() - $a->getDate()->getTimestamp();
});
}
$sanitizer = null;
if ($dataProvider->getProperty('stripTags') != '') {
$sanitizer = (new HtmlSanitizerConfig())->allowSafeElements();
// Add the tags to strip
foreach (explode(',', $dataProvider->getProperty('stripTags')) as $forbidden) {
$this->getLog()->debug('fetchData: blocking element ' . $forbidden);
$sanitizer = $sanitizer->blockElement($forbidden);
}
}
// Where should we get images?
$imageSource = $dataProvider->getProperty('imageSource', 'enclosure');
$imageTag = match ($imageSource) {
'mediaContent' => 'media:content',
'image' => 'image',
'custom' => $dataProvider->getProperty('imageSourceTag', 'image'),
default => 'enclosure'
};
$imageSourceAttribute = null;
if ($imageSource === 'mediaContent') {
$imageSourceAttribute = 'url';
} else if ($imageSource === 'custom') {
$imageSourceAttribute = $dataProvider->getProperty('imageSourceAttribute', null);
}
// Parse each item into an article
foreach ($feedItems as $item) {
/* @var Item $item */
$article = new Article();
$article->title = $item->getTitle();
$article->author = $item->getAuthor();
$article->link = $item->getUrl();
$article->date = Carbon::instance($item->getDate());
$article->publishedDate = Carbon::instance($item->getPublishedDate());
// Body safe HTML
$article->content = $dataProvider->getSanitizer(['content' => $item->getContent()])
->getHtml('content', [
'htmlSanitizerConfig' => $sanitizer
]);
// RSS doesn't support a summary/excerpt tag.
$descriptionTag = $item->getTag('description');
$article->summary = trim($descriptionTag ? strip_tags($descriptionTag[0]) : $article->content);
// Do we have an image included?
$link = null;
if ($imageTag === 'enclosure') {
if (stripos($item->getEnclosureType(), 'image') > -1) {
$link = $item->getEnclosureUrl();
}
} else {
$link = $item->getTag($imageTag, $imageSourceAttribute)[0] ?? null;
}
if (!(empty($link))) {
$article->image = $dataProvider->addImage('ticker_' . md5($link), $link, $expiresImage);
} else {
$this->getLog()->debug('fetchData: no image found for image tag using ' . $imageTag);
}
if ($dataProvider->getProperty('decodeHtml') == 1) {
$article->content = htmlspecialchars_decode($article->content);
}
// Add the article.
$dataProvider->addItem($article);
}
$dataProvider->setCacheTtl($dataProvider->getProperty('updateInterval', 60) * 60);
$dataProvider->setIsHandled();
} catch (GuzzleException $requestException) {
// Log and return empty?
$this->getLog()->error('Unable to get feed: ' . $uri
. ', e: ' . $requestException->getMessage());
$dataProvider->addError(__('Unable to download feed'));
} catch (PicoFeedException $picoFeedException) {
// Output any PicoFeed logs
if ($picoFeedLoggingEnabled) {
$this->getLog()->debug('Outputting Picofeed Logs.');
foreach (Logger::getMessages() as $message) {
$this->getLog()->debug($message);
}
}
// Log and return empty?
$this->getLog()->error('Unable to parse feed: ' . $picoFeedException->getMessage());
$this->getLog()->debug($picoFeedException->getTraceAsString());
$dataProvider->addError(__('Unable to parse feed'));
}
// Output any PicoFeed logs
if ($picoFeedLoggingEnabled) {
foreach (Logger::getMessages() as $message) {
$this->getLog()->debug($message);
}
}
return $this;
}
public function getDataCacheKey(DataProviderInterface $dataProvider): ?string
{
// No special cache key requirements.
return null;
}
public function getDataModifiedDt(DataProviderInterface $dataProvider): ?Carbon
{
return null;
}
/**
* @param DataProviderInterface $dataProvider
* @param string $uri
* @return array body, contentType
* @throws GuzzleException
*/
private function getFeed(DataProviderInterface $dataProvider, string $uri): array
{
// See if we have this feed cached already.
$cache = $dataProvider->getPool()->getItem('/widget/' . $dataProvider->getDataType() . '/' . md5($uri));
$body = $cache->get();
if ($cache->isMiss() || $body === null || !is_array($body)) {
// Make a new request.
$this->getLog()->debug('getFeed: cache miss');
$body = [];
$httpOptions = [
'headers' => [
'Accept' => 'application/rss+xml, application/rdf+xml;q=0.8, application/atom+xml;q=0.6,'
. 'application/xml;q=0.4, text/xml;q=0.4, text/html;q=0.2, text/*;q=0.1'
],
'timeout' => 20, // wait no more than 20 seconds
];
if (!empty($dataProvider->getProperty('userAgent'))) {
$httpOptions['headers']['User-Agent'] = trim($dataProvider->getProperty('userAgent'));
}
$response = $dataProvider
->getGuzzleClient($httpOptions)
->get($uri);
$body['body'] = $response->getBody()->getContents();
$body['contentType'] = $response->getHeaderLine('Content-Type');
// Save the resonse to cache
$cache->set($body);
$cache->expiresAfter($dataProvider->getSetting('cachePeriod', 1440) * 60);
$dataProvider->getPool()->saveDeferred($cache);
} else {
$this->getLog()->debug('getFeed: cache hit');
}
return $body;
}
}

View File

@@ -0,0 +1,53 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget;
class SubPlaylistItem implements \JsonSerializable
{
/** @var int */
public $rowNo;
/** @var int */
public $playlistId;
/** @var string */
public $spotFill;
/** @var int */
public $spotLength;
/** @var ?int */
public $spots;
/** @inheritDoc */
public function jsonSerialize(): array
{
return [
'rowNo' => $this->rowNo,
'playlistId' => $this->playlistId,
'spotFill' => $this->spotFill,
'spotLength' => $this->spotLength,
'spots' => $this->spots,
];
}
}

View File

@@ -0,0 +1,64 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Validator;
use Respect\Validation\Validator as v;
use Xibo\Entity\Module;
use Xibo\Entity\Widget;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Widget\Provider\WidgetValidatorInterface;
use Xibo\Widget\Provider\WidgetValidatorTrait;
/**
* Validate that we either use display location or a lat/lng have been set
*/
class DisplayOrGeoValidator implements WidgetValidatorInterface
{
use WidgetValidatorTrait;
/** @inheritDoc */
public function validate(Module $module, Widget $widget, string $stage): void
{
$useDisplayLocation = $widget->getOptionValue('useDisplayLocation', null);
if ($useDisplayLocation === null) {
foreach ($module->properties as $property) {
if ($property->id === 'useDisplayLocation') {
$useDisplayLocation = $property->default;
}
}
}
if ($useDisplayLocation === 0) {
// Validate lat/long
// only if they have been provided (our default is the CMS lat/long).
$lat = $widget->getOptionValue('latitude', null);
if (!empty($lat) && !v::latitude()->validate($lat)) {
throw new InvalidArgumentException(__('The latitude entered is not valid.'), 'latitude');
}
$lng = $widget->getOptionValue('longitude', null);
if (!empty($lng) && !v::longitude()->validate($lng)) {
throw new InvalidArgumentException(__('The longitude entered is not valid.'), 'longitude');
}
}
}
}

View File

@@ -0,0 +1,64 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Validator;
use Illuminate\Support\Str;
use Xibo\Entity\Module;
use Xibo\Entity\Widget;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Widget\Provider\WidgetValidatorInterface;
use Xibo\Widget\Provider\WidgetValidatorTrait;
/**
* Validate that we have a duration greater than 0
*/
class RemoteUrlsZeroDurationValidator implements WidgetValidatorInterface
{
use WidgetValidatorTrait;
/**
* @inheritDoc
* @throws \Xibo\Support\Exception\InvalidArgumentException
*/
public function validate(Module $module, Widget $widget, string $stage): void
{
$url = urldecode($widget->getOptionValue('uri', ''));
if ($widget->useDuration === 1
&& $widget->duration <= 0
&& !Str::startsWith($url, 'file://')
&& Str::contains($url, '://')
) {
// This is not a locally stored file, and so we should have a duration
throw new InvalidArgumentException(
__('The duration needs to be greater than 0 for remote URLs'),
'duration'
);
} else if ($widget->useDuration === 1 && $widget->duration <= 0) {
// Locally stored file, still needs a positive duration.
throw new InvalidArgumentException(
__('The duration needs to be above 0 for a locally stored file '),
'duration'
);
}
}
}

View File

@@ -0,0 +1,52 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Validator;
use Xibo\Entity\Module;
use Xibo\Entity\Widget;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Widget\Provider\WidgetValidatorInterface;
use Xibo\Widget\Provider\WidgetValidatorTrait;
/**
* Ensure a command has been entered somewhere in the widget
*/
class ShellCommandValidator implements WidgetValidatorInterface
{
use WidgetValidatorTrait;
/** @inheritDoc */
public function validate(Module $module, Widget $widget, string $stage): void
{
if ($widget->getOptionValue('globalCommand', '') == ''
&& $widget->getOptionValue('androidCommand', '') == ''
&& $widget->getOptionValue('windowsCommand', '') == ''
&& $widget->getOptionValue('linuxCommand', '') == ''
&& $widget->getOptionValue('commandCode', '') == ''
&& $widget->getOptionValue('webosCommand', '') == ''
&& $widget->getOptionValue('tizenCommand', '') == ''
) {
throw new InvalidArgumentException(__('You must enter a command'), 'command');
}
}
}

View File

@@ -0,0 +1,52 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget\Validator;
use Xibo\Entity\Module;
use Xibo\Entity\Widget;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Widget\Provider\WidgetValidatorInterface;
use Xibo\Widget\Provider\WidgetValidatorTrait;
/**
* Validate that we have a duration greater than 0
*/
class ZeroDurationValidator implements WidgetValidatorInterface
{
use WidgetValidatorTrait;
/**
* @inheritDoc
* @throws \Xibo\Support\Exception\InvalidArgumentException
*/
public function validate(Module $module, Widget $widget, string $stage): void
{
// Videos can have 0 durations (but not if useDuration is selected)
if ($widget->useDuration === 1 && $widget->duration <= 0) {
throw new InvalidArgumentException(
sprintf(__('Duration needs to be above 0 for %s'), $module->name),
'duration'
);
}
}
}

View File

@@ -0,0 +1,68 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Widget;
use Carbon\Carbon;
use Xibo\Support\Exception\NotFoundException;
use Xibo\Widget\Provider\DataProviderInterface;
use Xibo\Widget\Provider\DurationProviderInterface;
use Xibo\Widget\Provider\WidgetProviderInterface;
use Xibo\Widget\Provider\WidgetProviderTrait;
/**
* Handles setting the correct video duration.
*/
class VideoProvider implements WidgetProviderInterface
{
use WidgetProviderTrait;
public function fetchData(DataProviderInterface $dataProvider): WidgetProviderInterface
{
return $this;
}
public function fetchDuration(DurationProviderInterface $durationProvider): WidgetProviderInterface
{
// 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;
}
public function getDataCacheKey(DataProviderInterface $dataProvider): ?string
{
// No special cache key requirements.
return null;
}
public function getDataModifiedDt(DataProviderInterface $dataProvider): ?Carbon
{
return null;
}
}