954 lines
33 KiB
PHP
954 lines
33 KiB
PHP
|
|
<?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\Factory;
|
||
|
|
|
||
|
|
use Illuminate\Support\Str;
|
||
|
|
use Slim\Views\Twig;
|
||
|
|
use Stash\Interfaces\PoolInterface;
|
||
|
|
use Xibo\Entity\Module;
|
||
|
|
use Xibo\Entity\ModuleTemplate;
|
||
|
|
use Xibo\Entity\Widget;
|
||
|
|
use Xibo\Service\ConfigServiceInterface;
|
||
|
|
use Xibo\Support\Exception\NotFoundException;
|
||
|
|
use Xibo\Widget\DataType\DataTypeInterface;
|
||
|
|
use Xibo\Widget\Definition\Asset;
|
||
|
|
use Xibo\Widget\Definition\DataType;
|
||
|
|
use Xibo\Widget\Provider\DataProvider;
|
||
|
|
use Xibo\Widget\Provider\DataProviderInterface;
|
||
|
|
use Xibo\Widget\Provider\DurationProvider;
|
||
|
|
use Xibo\Widget\Provider\DurationProviderInterface;
|
||
|
|
use Xibo\Widget\Provider\WidgetProviderInterface;
|
||
|
|
use Xibo\Widget\Render\WidgetDataProviderCache;
|
||
|
|
use Xibo\Widget\Render\WidgetHtmlRenderer;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Class ModuleFactory
|
||
|
|
* @package Xibo\Factory
|
||
|
|
*/
|
||
|
|
class ModuleFactory extends BaseFactory
|
||
|
|
{
|
||
|
|
use ModuleXmlTrait;
|
||
|
|
|
||
|
|
public static $systemDataTypes = [
|
||
|
|
'Article',
|
||
|
|
'Event',
|
||
|
|
'Forecast',
|
||
|
|
'Product',
|
||
|
|
'ProductCategory',
|
||
|
|
'SocialMedia',
|
||
|
|
'dataset'
|
||
|
|
];
|
||
|
|
|
||
|
|
/** @var Module[] all modules */
|
||
|
|
private $modules = null;
|
||
|
|
|
||
|
|
/** @var \Xibo\Widget\Definition\DataType[] */
|
||
|
|
private $dataTypes = null;
|
||
|
|
|
||
|
|
/** @var \Stash\Interfaces\PoolInterface */
|
||
|
|
private $pool;
|
||
|
|
|
||
|
|
/** @var string */
|
||
|
|
private $cachePath;
|
||
|
|
|
||
|
|
/** @var \Slim\Views\Twig */
|
||
|
|
private $twig;
|
||
|
|
|
||
|
|
/** @var \Xibo\Service\ConfigServiceInterface */
|
||
|
|
private $config;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Construct a factory
|
||
|
|
* @param string $cachePath
|
||
|
|
* @param PoolInterface $pool
|
||
|
|
* @param \Slim\Views\Twig $twig
|
||
|
|
* @param \Xibo\Service\ConfigServiceInterface $config
|
||
|
|
*/
|
||
|
|
public function __construct(string $cachePath, PoolInterface $pool, Twig $twig, ConfigServiceInterface $config)
|
||
|
|
{
|
||
|
|
$this->cachePath = $cachePath;
|
||
|
|
$this->pool = $pool;
|
||
|
|
$this->twig = $twig;
|
||
|
|
$this->config = $config;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param \Xibo\Entity\Module $module
|
||
|
|
* @param \Xibo\Entity\Widget $widget
|
||
|
|
* @return \Xibo\Widget\Provider\DataProviderInterface
|
||
|
|
*/
|
||
|
|
public function createDataProvider(Module $module, Widget $widget): DataProviderInterface
|
||
|
|
{
|
||
|
|
return new DataProvider(
|
||
|
|
$module,
|
||
|
|
$widget,
|
||
|
|
$this->config->getGuzzleProxy(),
|
||
|
|
$this->getSanitizerService(),
|
||
|
|
$this->pool,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param Module $module
|
||
|
|
* @param Widget $widget
|
||
|
|
* @return DurationProviderInterface
|
||
|
|
*/
|
||
|
|
public function createDurationProvider(Module $module, Widget $widget): DurationProviderInterface
|
||
|
|
{
|
||
|
|
return new DurationProvider($module, $widget);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create a widget renderer
|
||
|
|
* @return \Xibo\Widget\Render\WidgetHtmlRenderer
|
||
|
|
*/
|
||
|
|
public function createWidgetHtmlRenderer(): WidgetHtmlRenderer
|
||
|
|
{
|
||
|
|
return (new WidgetHtmlRenderer($this->cachePath, $this->twig, $this->config, $this))
|
||
|
|
->useLogger($this->getLog()->getLoggerInterface());
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create a widget data provider cache
|
||
|
|
*/
|
||
|
|
public function createWidgetDataProviderCache(): WidgetDataProviderCache
|
||
|
|
{
|
||
|
|
return (new WidgetDataProviderCache($this->pool))
|
||
|
|
->useLogger($this->getLog()->getLoggerInterface());
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Determine the cache key
|
||
|
|
* @param \Xibo\Entity\Module $module
|
||
|
|
* @param \Xibo\Entity\Widget $widget
|
||
|
|
* @param int $displayId the displayId (0 for preview)
|
||
|
|
* @param \Xibo\Widget\Provider\DataProviderInterface $dataProvider
|
||
|
|
* @param \Xibo\Widget\Provider\WidgetProviderInterface|null $widgetInterface
|
||
|
|
* @return string
|
||
|
|
*/
|
||
|
|
public function determineCacheKey(
|
||
|
|
Module $module,
|
||
|
|
Widget $widget,
|
||
|
|
int $displayId,
|
||
|
|
DataProviderInterface $dataProvider,
|
||
|
|
?WidgetProviderInterface $widgetInterface
|
||
|
|
): string {
|
||
|
|
// Determine the cache key
|
||
|
|
$cacheKey = $widgetInterface?->getDataCacheKey($dataProvider);
|
||
|
|
|
||
|
|
if ($cacheKey === null) {
|
||
|
|
// Determinthe cache key from the setting in XML.
|
||
|
|
if (empty($module->dataCacheKey)) {
|
||
|
|
// Best we can do here is a cache per widget, but we should log this as an error.
|
||
|
|
$this->getLog()->debug('determineCacheKey: module without dataCacheKey: ' . $module->moduleId);
|
||
|
|
$cacheKey = $widget->widgetId;
|
||
|
|
} else {
|
||
|
|
// Start with the one provided
|
||
|
|
$this->getLog()->debug('determineCacheKey: module dataCacheKey: ' . $module->dataCacheKey);
|
||
|
|
|
||
|
|
$cacheKey = $module->dataCacheKey;
|
||
|
|
|
||
|
|
// Properties
|
||
|
|
$module->decorateProperties($widget, true);
|
||
|
|
$properties = $module->getPropertyValues(false);
|
||
|
|
|
||
|
|
// Is display location in use?
|
||
|
|
// We should see if the display location property is set (this is a special property), and if it is
|
||
|
|
// update the lat/lng with the details stored on the display
|
||
|
|
$latitude = $properties['latitude'] ?? '';
|
||
|
|
$longitude = $properties['longitude'] ?? '';
|
||
|
|
if ($dataProvider->getProperty('useDisplayLocation') == 1) {
|
||
|
|
$latitude = $dataProvider->getDisplayLatitude() ?: $latitude;
|
||
|
|
$longitude = $dataProvider->getDisplayLongitude() ?: $longitude;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Parse the cache key for variables.
|
||
|
|
$matches = [];
|
||
|
|
preg_match_all('/%(.*?)%/', $cacheKey, $matches);
|
||
|
|
foreach ($matches[1] as $match) {
|
||
|
|
if ($match === 'displayId') {
|
||
|
|
$cacheKey = str_replace('%displayId%', $displayId, $cacheKey);
|
||
|
|
} else if ($match === 'widgetId') {
|
||
|
|
$cacheKey = str_replace('%widgetId%', $widget->widgetId, $cacheKey);
|
||
|
|
} else if ($match === 'latitude') {
|
||
|
|
$cacheKey = str_replace('%latitude%', $latitude, $cacheKey);
|
||
|
|
} else if ($match === 'longitude') {
|
||
|
|
$cacheKey = str_replace('%longitude%', $longitude, $cacheKey);
|
||
|
|
} else {
|
||
|
|
$this->getLog()->debug($match);
|
||
|
|
$cacheKey = str_replace(
|
||
|
|
'%' . $match . '%',
|
||
|
|
$properties[$match] ?? '',
|
||
|
|
$cacheKey
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Include a separate cache per fallback data?
|
||
|
|
if ($module->fallbackData == 1) {
|
||
|
|
$cacheKey .= '_fb ' . $widget->getOptionValue('showFallback', 'never');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->getLog()->debug('determineCacheKey: cache key is : ' . $cacheKey);
|
||
|
|
|
||
|
|
return $cacheKey;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param string $dataType
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
public function clearCacheForDataType(string $dataType): void
|
||
|
|
{
|
||
|
|
$this->getLog()->debug('clearCacheForDataType: /widget/' . $dataType);
|
||
|
|
|
||
|
|
$this->pool->deleteItem('/widget/' . $dataType);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @return \Xibo\Entity\Module[]
|
||
|
|
*/
|
||
|
|
public function getKeyedArrayOfModules(): array
|
||
|
|
{
|
||
|
|
$this->getLog()->debug('ModuleFactory: getKeyedArrayOfModules');
|
||
|
|
$modules = [];
|
||
|
|
foreach ($this->load() as $module) {
|
||
|
|
$modules[$module->type] = $module;
|
||
|
|
}
|
||
|
|
return $modules;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @return Module[]
|
||
|
|
*/
|
||
|
|
public function getAssignableModules(): array
|
||
|
|
{
|
||
|
|
$this->getLog()->debug('ModuleFactory: getAssignableModules');
|
||
|
|
$modules = [];
|
||
|
|
foreach ($this->load() as $module) {
|
||
|
|
if ($module->enabled === 1 && $module->assignable === 1) {
|
||
|
|
$modules[] = $module;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return $modules;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @return Module[]
|
||
|
|
*/
|
||
|
|
public function getLibraryModules(): array
|
||
|
|
{
|
||
|
|
$this->getLog()->debug('ModuleFactory: getLibraryModules');
|
||
|
|
$modules = [];
|
||
|
|
foreach ($this->load() as $module) {
|
||
|
|
if ($module->enabled == 1 && $module->regionSpecific === 0) {
|
||
|
|
$modules[] = $module;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return $modules;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get module by Id
|
||
|
|
* @param string $moduleId
|
||
|
|
* @return Module
|
||
|
|
* @throws NotFoundException
|
||
|
|
*/
|
||
|
|
public function getById($moduleId): Module
|
||
|
|
{
|
||
|
|
$this->getLog()->debug('ModuleFactory: getById');
|
||
|
|
foreach ($this->load() as $module) {
|
||
|
|
if ($module->moduleId === $moduleId) {
|
||
|
|
return $module;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
throw new NotFoundException();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get an array of all modules
|
||
|
|
* @return Module[]
|
||
|
|
*/
|
||
|
|
public function getAll(): array
|
||
|
|
{
|
||
|
|
$this->getLog()->debug('ModuleFactory: getAll');
|
||
|
|
return $this->load();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get an array of all modules except canvas
|
||
|
|
* @param array $filter
|
||
|
|
* @return Module[]
|
||
|
|
*/
|
||
|
|
public function getAllExceptCanvas(array $filter = []): array
|
||
|
|
{
|
||
|
|
$sanitizedFilter = $this->getSanitizer($filter);
|
||
|
|
$this->getLog()->debug('ModuleFactory: getAllButCanvas');
|
||
|
|
$modules = [];
|
||
|
|
foreach ($this->load() as $module) {
|
||
|
|
// Hide the canvas module from the module list
|
||
|
|
if ($module->moduleId != 'core-canvas') {
|
||
|
|
// do we have a name filter?
|
||
|
|
if (!empty($sanitizedFilter->getString('name'))) {
|
||
|
|
if (str_contains(strtolower($module->name), strtolower($sanitizedFilter->getString('name')))) {
|
||
|
|
$modules[] = $module;
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
$modules[] = $module;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return $modules;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get an array of all enabled modules
|
||
|
|
* @return Module[]
|
||
|
|
*/
|
||
|
|
public function getEnabled(): array
|
||
|
|
{
|
||
|
|
$this->getLog()->debug('ModuleFactory: getEnabled');
|
||
|
|
$modules = [];
|
||
|
|
foreach ($this->load() as $module) {
|
||
|
|
if ($module->enabled == 1) {
|
||
|
|
$modules[] = $module;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return $modules;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get module by Type
|
||
|
|
* this should return the first module enabled by the type specified.
|
||
|
|
* @param string $type
|
||
|
|
* @param array $conditions Conditions that are created based on the widget's option and value, e.g, templateId==worldclock1
|
||
|
|
* @return Module
|
||
|
|
* @throws \Xibo\Support\Exception\NotFoundException
|
||
|
|
*/
|
||
|
|
public function getByType(string $type, array $conditions = []): Module
|
||
|
|
{
|
||
|
|
$this->getLog()->debug('ModuleFactory: getByType ' . $type);
|
||
|
|
$modules = $this->load();
|
||
|
|
usort($modules, function ($a, $b) {
|
||
|
|
/** @var Module $a */
|
||
|
|
/** @var Module $b */
|
||
|
|
return $a->enabled - $b->enabled;
|
||
|
|
});
|
||
|
|
|
||
|
|
foreach ($modules as $module) {
|
||
|
|
if ($module->type === $type) {
|
||
|
|
return $module;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Match on legacy type
|
||
|
|
foreach ($modules as $module) {
|
||
|
|
// get the name of the legacytypes
|
||
|
|
$legacyTypes = [];
|
||
|
|
$legacyConditions = [];
|
||
|
|
if (count($module->legacyTypes) > 0) {
|
||
|
|
$legacyTypes = array_column($module->legacyTypes, 'name');
|
||
|
|
$legacyConditions = array_column($module->legacyTypes, 'condition');
|
||
|
|
}
|
||
|
|
|
||
|
|
if (in_array($type, $legacyTypes)) {
|
||
|
|
foreach ($conditions as $value) {
|
||
|
|
if (in_array($value, $legacyConditions)) {
|
||
|
|
return $module;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return $module;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
throw new NotFoundException();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get module by extension
|
||
|
|
* @param string $extension
|
||
|
|
* @return Module
|
||
|
|
* @throws NotFoundException
|
||
|
|
*/
|
||
|
|
public function getByExtension(string $extension): Module
|
||
|
|
{
|
||
|
|
$this->getLog()->debug('ModuleFactory: getByExtension');
|
||
|
|
foreach ($this->load() as $module) {
|
||
|
|
$validExtensions = $module->getSetting('validExtensions');
|
||
|
|
if (!empty($validExtensions) && Str::contains($validExtensions, $extension)) {
|
||
|
|
return $module;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
throw new NotFoundException(sprintf(__('Extension %s does not match any enabled Module'), $extension));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get Valid Extensions
|
||
|
|
* @param array $filterBy
|
||
|
|
* @return string[]
|
||
|
|
*/
|
||
|
|
public function getValidExtensions($filterBy = []): array
|
||
|
|
{
|
||
|
|
$this->getLog()->debug('ModuleFactory: getValidExtensions');
|
||
|
|
$filterBy = $this->getSanitizer($filterBy);
|
||
|
|
|
||
|
|
// Do we allow media type changes?
|
||
|
|
$isAllowMediaTypeChange = $filterBy->getCheckbox('allowMediaTypeChange');
|
||
|
|
|
||
|
|
if ($isAllowMediaTypeChange) {
|
||
|
|
// Restrict to any file based media type (i.e. any valid extension)
|
||
|
|
$typeFilter = null;
|
||
|
|
} else {
|
||
|
|
// Restrict to type
|
||
|
|
$typeFilter = $filterBy->getString('type');
|
||
|
|
}
|
||
|
|
|
||
|
|
$extensions = [];
|
||
|
|
foreach ($this->load() as $module) {
|
||
|
|
if ($typeFilter !== null && $module->type !== $typeFilter) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!empty($module->getSetting('validExtensions'))) {
|
||
|
|
foreach (explode(',', $module->getSetting('validExtensions')) as $extension) {
|
||
|
|
$extensions[] = $extension;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return $extensions;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param string $dataTypeId
|
||
|
|
* @return \Xibo\Widget\Definition\DataType
|
||
|
|
* @throws \Xibo\Support\Exception\NotFoundException
|
||
|
|
*/
|
||
|
|
public function getDataTypeById(string $dataTypeId): DataType
|
||
|
|
{
|
||
|
|
// Rely on a class if we have one.
|
||
|
|
$className = ucfirst(str_replace('-', '', ucwords($dataTypeId, '-')));
|
||
|
|
$className = '\\Xibo\\Widget\\DataType\\' . $className;
|
||
|
|
if (class_exists($className)) {
|
||
|
|
$class = new $className();
|
||
|
|
if ($class instanceof DataTypeInterface) {
|
||
|
|
return ($class->getDefinition());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Otherwise look in our XML definitions
|
||
|
|
foreach ($this->loadDataTypes() as $dataType) {
|
||
|
|
if ($dataType->id === $dataTypeId) {
|
||
|
|
return $dataType;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
throw new NotFoundException(__('DataType not found'));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @return DataType[]
|
||
|
|
*/
|
||
|
|
public function getAllDataTypes()
|
||
|
|
{
|
||
|
|
$dataTypes = [];
|
||
|
|
|
||
|
|
// get system data types
|
||
|
|
foreach (self::$systemDataTypes as $dataTypeId) {
|
||
|
|
$className = '\\Xibo\\Widget\\DataType\\' . ucfirst($dataTypeId);
|
||
|
|
if (class_exists($className)) {
|
||
|
|
$class = new $className();
|
||
|
|
if ($class instanceof DataTypeInterface) {
|
||
|
|
$dataTypes[] = $class->getDefinition();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// special handling for dataset
|
||
|
|
if ($dataTypeId === 'dataset') {
|
||
|
|
$dataType = new DataType();
|
||
|
|
$dataType->id = $dataTypeId;
|
||
|
|
$dataType->name = 'DataSet';
|
||
|
|
$dataTypes[] = $dataType;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// get data types from xml
|
||
|
|
$files = array_merge(
|
||
|
|
glob(PROJECT_ROOT . '/modules/datatypes/*.xml'),
|
||
|
|
glob(PROJECT_ROOT . '/custom/modules/datatypes/*.xml')
|
||
|
|
);
|
||
|
|
|
||
|
|
foreach ($files as $file) {
|
||
|
|
$xml = new \DOMDocument();
|
||
|
|
$xml->load($file);
|
||
|
|
$dataType = new DataType();
|
||
|
|
$dataType->id = $this->getFirstValueOrDefaultFromXmlNode($xml, 'id');
|
||
|
|
$dataType->name = $this->getFirstValueOrDefaultFromXmlNode($xml, 'name');
|
||
|
|
$dataTypes[] = $dataType;
|
||
|
|
}
|
||
|
|
|
||
|
|
sort($dataTypes);
|
||
|
|
return $dataTypes;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param string $assetId
|
||
|
|
* @return \Xibo\Widget\Definition\Asset
|
||
|
|
* @throws \Xibo\Support\Exception\NotFoundException
|
||
|
|
*/
|
||
|
|
public function getAssetById(string $assetId): Asset
|
||
|
|
{
|
||
|
|
$this->getLog()->debug('getAssetById: ' . $assetId);
|
||
|
|
foreach ($this->getEnabled() as $module) {
|
||
|
|
foreach ($module->getAssets() as $asset) {
|
||
|
|
if ($asset->id === $assetId) {
|
||
|
|
return $asset;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
throw new NotFoundException(__('Asset not found'));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param string $alias
|
||
|
|
* @return \Xibo\Widget\Definition\Asset
|
||
|
|
* @throws \Xibo\Support\Exception\NotFoundException
|
||
|
|
*/
|
||
|
|
public function getAssetByAlias(string $alias): Asset
|
||
|
|
{
|
||
|
|
$this->getLog()->debug('getAssetByAlias: ' . $alias);
|
||
|
|
foreach ($this->getEnabled() as $module) {
|
||
|
|
foreach ($module->getAssets() as $asset) {
|
||
|
|
if ($asset->alias === $alias) {
|
||
|
|
return $asset;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
throw new NotFoundException(__('Asset not found'));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param ModuleTemplate[] $templates
|
||
|
|
* @return Asset[]
|
||
|
|
*/
|
||
|
|
public function getAssetsFromTemplates(array $templates): array
|
||
|
|
{
|
||
|
|
$assets = [];
|
||
|
|
foreach ($this->getEnabled() as $module) {
|
||
|
|
foreach ($module->getAssets() as $asset) {
|
||
|
|
$assets[$asset->id] = $asset;
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach ($templates as $template) {
|
||
|
|
foreach ($template->getAssets() as $asset) {
|
||
|
|
$assets[$asset->id] = $asset;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return $assets;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get all assets
|
||
|
|
* @return Asset[]
|
||
|
|
*/
|
||
|
|
public function getAllAssets(): array
|
||
|
|
{
|
||
|
|
$assets = [];
|
||
|
|
foreach ($this->getEnabled() as $module) {
|
||
|
|
foreach ($module->getAssets() as $asset) {
|
||
|
|
$assets[$asset->id] = $asset;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return $assets;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get an asset from anywhere by its ID
|
||
|
|
* @param string $assetId
|
||
|
|
* @param ModuleTemplateFactory $moduleTemplateFactory
|
||
|
|
* @param bool $isAlias
|
||
|
|
* @return Asset
|
||
|
|
* @throws NotFoundException
|
||
|
|
*/
|
||
|
|
public function getAssetsFromAnywhereById(
|
||
|
|
string $assetId,
|
||
|
|
ModuleTemplateFactory $moduleTemplateFactory,
|
||
|
|
bool $isAlias = false,
|
||
|
|
): Asset {
|
||
|
|
$asset = null;
|
||
|
|
try {
|
||
|
|
$asset = $isAlias
|
||
|
|
? $this->getAssetByAlias($assetId)
|
||
|
|
: $this->getAssetById($assetId);
|
||
|
|
} catch (NotFoundException) {
|
||
|
|
// Not a module asset.
|
||
|
|
}
|
||
|
|
|
||
|
|
// Try a template instead
|
||
|
|
try {
|
||
|
|
$asset = $isAlias
|
||
|
|
? $moduleTemplateFactory->getAssetByAlias($assetId)
|
||
|
|
: $moduleTemplateFactory->getAssetById($assetId);
|
||
|
|
} catch (NotFoundException) {
|
||
|
|
// Not a module template asset.
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($asset !== null) {
|
||
|
|
return $asset;
|
||
|
|
} else {
|
||
|
|
throw new NotFoundException(__('Asset not found'));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Load all modules into an array for use throughout this request
|
||
|
|
* @return \Xibo\Entity\Module[]
|
||
|
|
*/
|
||
|
|
private function load(): array
|
||
|
|
{
|
||
|
|
if ($this->modules === null) {
|
||
|
|
// TODO: these are the only fields we require in the settings table
|
||
|
|
$sql = '
|
||
|
|
SELECT `moduleId`, `enabled`, `previewEnabled`, `defaultDuration`, `settings`
|
||
|
|
FROM `module`
|
||
|
|
';
|
||
|
|
|
||
|
|
$modulesWithSettings = [];
|
||
|
|
foreach ($this->getStore()->select($sql, []) as $row) {
|
||
|
|
// Make a keyed array of these settings
|
||
|
|
$modulesWithSettings[$row['moduleId']] = $this->getSanitizer($row);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Load in our file system modules.
|
||
|
|
// we consider modules in the module folder, and also custom modules
|
||
|
|
$files = array_merge(
|
||
|
|
glob(PROJECT_ROOT . '/modules/*.xml'),
|
||
|
|
glob(PROJECT_ROOT . '/custom/modules/*.xml')
|
||
|
|
);
|
||
|
|
|
||
|
|
foreach ($files as $file) {
|
||
|
|
// Create our module entity from this file
|
||
|
|
try {
|
||
|
|
$module = $this->createFromXml($file, $modulesWithSettings);
|
||
|
|
|
||
|
|
// Create a widget provider if necessary
|
||
|
|
// Take our module and see if it has a class associated with it
|
||
|
|
if (!empty($module->class)) {
|
||
|
|
// We create a module specific provider
|
||
|
|
if (!class_exists($module->class)) {
|
||
|
|
$module->errors[] = 'Module class not found: ' . $module->class;
|
||
|
|
} else {
|
||
|
|
$class = $module->class;
|
||
|
|
$module->setWidgetProvider(new $class());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create a widget compatibility if necessary
|
||
|
|
if (!empty($module->compatibilityClass)) {
|
||
|
|
// We create a module specific provider
|
||
|
|
if (!class_exists($module->compatibilityClass)) {
|
||
|
|
$module->errors[] = 'Module compatibilityClass not found: ' . $module->compatibilityClass;
|
||
|
|
} else {
|
||
|
|
$compatibilityClass = $module->compatibilityClass;
|
||
|
|
$module->setWidgetCompatibility(new $compatibilityClass());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create a widget validator if necessary
|
||
|
|
foreach ($module->validatorClass as $validatorClass) {
|
||
|
|
// We create a module specific provider
|
||
|
|
if (!class_exists($validatorClass)) {
|
||
|
|
$module->errors[] = 'Module validatorClass not found: ' . $validatorClass;
|
||
|
|
} else {
|
||
|
|
$module->addWidgetValidator(
|
||
|
|
(new $validatorClass())
|
||
|
|
->setLog($this->getLog()->getLoggerInterface())
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Set error state
|
||
|
|
$module->isError = $module->errors !== null && count($module->errors) > 0;
|
||
|
|
|
||
|
|
// Register
|
||
|
|
$this->modules[] = $module;
|
||
|
|
} catch (\Exception $exception) {
|
||
|
|
$this->getLog()->error('Unable to create module from '
|
||
|
|
. basename($file) . ', skipping. e = ' . $exception->getMessage());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return $this->modules;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Load all data types into an array for use throughout this request
|
||
|
|
* @return \Xibo\Widget\Definition\DataType[]
|
||
|
|
*/
|
||
|
|
private function loadDataTypes(): array
|
||
|
|
{
|
||
|
|
if ($this->dataTypes === null) {
|
||
|
|
$files = array_merge(
|
||
|
|
glob(PROJECT_ROOT . '/modules/datatypes/*.xml'),
|
||
|
|
glob(PROJECT_ROOT . '/custom/modules/datatypes/*.xml')
|
||
|
|
);
|
||
|
|
|
||
|
|
foreach ($files as $file) {
|
||
|
|
$this->dataTypes[] = $this->createDataTypeFromXml($file);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return $this->dataTypes ?? [];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create a module from its XML definition
|
||
|
|
* @param string $file the path to the module definition
|
||
|
|
* @param array $modulesWithSettings
|
||
|
|
* @return \Xibo\Entity\Module
|
||
|
|
*/
|
||
|
|
private function createFromXml(string $file, array $modulesWithSettings): Module
|
||
|
|
{
|
||
|
|
// TODO: cache this into Stash
|
||
|
|
$xml = new \DOMDocument();
|
||
|
|
$xml->load($file);
|
||
|
|
|
||
|
|
$module = new Module($this->getStore(), $this->getLog(), $this->getDispatcher(), $this);
|
||
|
|
$module->moduleId = $this->getFirstValueOrDefaultFromXmlNode($xml, 'id');
|
||
|
|
$module->name = __($this->getFirstValueOrDefaultFromXmlNode($xml, 'name'));
|
||
|
|
$module->author = $this->getFirstValueOrDefaultFromXmlNode($xml, 'author');
|
||
|
|
$module->description = __($this->getFirstValueOrDefaultFromXmlNode($xml, 'description'));
|
||
|
|
$module->icon = $this->getFirstValueOrDefaultFromXmlNode($xml, 'icon');
|
||
|
|
$module->class = $this->getFirstValueOrDefaultFromXmlNode($xml, 'class');
|
||
|
|
$module->type = $this->getFirstValueOrDefaultFromXmlNode($xml, 'type');
|
||
|
|
$module->thumbnail = $this->getFirstValueOrDefaultFromXmlNode($xml, 'thumbnail');
|
||
|
|
$module->startWidth = intval($this->getFirstValueOrDefaultFromXmlNode($xml, 'startWidth'));
|
||
|
|
$module->startHeight = intval($this->getFirstValueOrDefaultFromXmlNode($xml, 'startHeight'));
|
||
|
|
$module->dataType = $this->getFirstValueOrDefaultFromXmlNode($xml, 'dataType');
|
||
|
|
$module->dataCacheKey = $this->getFirstValueOrDefaultFromXmlNode($xml, 'dataCacheKey');
|
||
|
|
$module->fallbackData = intval($this->getFirstValueOrDefaultFromXmlNode($xml, 'fallbackData', 0));
|
||
|
|
$module->schemaVersion = intval($this->getFirstValueOrDefaultFromXmlNode($xml, 'schemaVersion'));
|
||
|
|
$module->compatibilityClass = $this->getFirstValueOrDefaultFromXmlNode($xml, 'compatibilityClass');
|
||
|
|
$module->showIn = $this->getFirstValueOrDefaultFromXmlNode($xml, 'showIn') ?? 'both';
|
||
|
|
$module->assignable = intval($this->getFirstValueOrDefaultFromXmlNode($xml, 'assignable'));
|
||
|
|
$module->regionSpecific = intval($this->getFirstValueOrDefaultFromXmlNode($xml, 'regionSpecific'));
|
||
|
|
$module->renderAs = $this->getFirstValueOrDefaultFromXmlNode($xml, 'renderAs');
|
||
|
|
$module->defaultDuration = intval($this->getFirstValueOrDefaultFromXmlNode($xml, 'defaultDuration'));
|
||
|
|
$module->hasThumbnail = intval($this->getFirstValueOrDefaultFromXmlNode($xml, 'hasThumbnail', 0));
|
||
|
|
$module->allowPreview = intval($this->getFirstValueOrDefaultFromXmlNode($xml, 'allowPreview', 1));
|
||
|
|
|
||
|
|
// Validator classes
|
||
|
|
foreach ($xml->getElementsByTagName('validatorClass') as $node) {
|
||
|
|
/** @var \DOMNode $node */
|
||
|
|
if ($node instanceof \DOMElement) {
|
||
|
|
$module->validatorClass[] = trim($node->textContent);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Event listeners
|
||
|
|
$module->onInitialize = $this->getFirstValueOrDefaultFromXmlNode($xml, 'onInitialize');
|
||
|
|
if (!empty($module->onInitialize)) {
|
||
|
|
$module->onInitialize = trim($module->onInitialize);
|
||
|
|
}
|
||
|
|
|
||
|
|
$module->onParseData = $this->getFirstValueOrDefaultFromXmlNode($xml, 'onParseData');
|
||
|
|
if (!empty($module->onParseData)) {
|
||
|
|
$module->onParseData = trim($module->onParseData);
|
||
|
|
}
|
||
|
|
|
||
|
|
$module->onDataLoad = $this->getFirstValueOrDefaultFromXmlNode($xml, 'onDataLoad');
|
||
|
|
if (!empty($module->onDataLoad)) {
|
||
|
|
$module->onDataLoad = trim($module->onDataLoad);
|
||
|
|
}
|
||
|
|
|
||
|
|
$module->onRender = $this->getFirstValueOrDefaultFromXmlNode($xml, 'onRender');
|
||
|
|
if (!empty($module->onRender)) {
|
||
|
|
$module->onRender = trim($module->onRender);
|
||
|
|
}
|
||
|
|
|
||
|
|
$module->onVisible = $this->getFirstValueOrDefaultFromXmlNode($xml, 'onVisible');
|
||
|
|
if (!empty($module->onVisible)) {
|
||
|
|
$module->onVisible = trim($module->onVisible);
|
||
|
|
}
|
||
|
|
|
||
|
|
// We might have sample data (usually only if there is a dataType)
|
||
|
|
$sampleData = $this->getFirstValueOrDefaultFromXmlNode($xml, 'sampleData');
|
||
|
|
|
||
|
|
if (!empty($sampleData)) {
|
||
|
|
$module->sampleData = json_decode(trim($sampleData), true);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Legacy types.
|
||
|
|
try {
|
||
|
|
$module->legacyTypes = $this->parseLegacyTypes($xml->getElementsByTagName('legacyType'));
|
||
|
|
} catch (\Exception $e) {
|
||
|
|
$module->errors[] = __('Invalid legacyType');
|
||
|
|
$this->getLog()->error('Module ' . $module->moduleId . ' has invalid legacyType. e: ' . $e->getMessage());
|
||
|
|
}
|
||
|
|
|
||
|
|
// Group for non datatype modules
|
||
|
|
$module->group = [];
|
||
|
|
$groupNodes = $xml->getElementsByTagName('group');
|
||
|
|
foreach ($groupNodes as $groupNode) {
|
||
|
|
if ($groupNode instanceof \DOMElement) {
|
||
|
|
$module->group['id'] = $groupNode->getAttribute('id');
|
||
|
|
$module->group['icon'] = $groupNode->getAttribute('icon');
|
||
|
|
$module->group['name'] = $groupNode->textContent;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Parse assets
|
||
|
|
try {
|
||
|
|
$module->assets = $this->parseAssets($xml->getElementsByTagName('assets'));
|
||
|
|
} catch (\Exception $e) {
|
||
|
|
$module->errors[] = __('Invalid assets');
|
||
|
|
$this->getLog()->error('Module ' . $module->moduleId
|
||
|
|
. ' has invalid assets. e: ' . $e->getMessage());
|
||
|
|
}
|
||
|
|
|
||
|
|
// Default values for remaining expected properties
|
||
|
|
$module->isInstalled = false;
|
||
|
|
$module->isError = false;
|
||
|
|
$module->errors = [];
|
||
|
|
$module->enabled = 0;
|
||
|
|
$module->previewEnabled = 0;
|
||
|
|
|
||
|
|
// Parse settings/property definitions.
|
||
|
|
try {
|
||
|
|
$module->settings = $this->parseProperties($xml->getElementsByTagName('settings'));
|
||
|
|
} catch (\Exception $e) {
|
||
|
|
$module->errors[] = __('Invalid settings');
|
||
|
|
$this->getLog()->error('Module ' . $module->moduleId . ' has invalid settings. e: ' . $e->getMessage());
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add in any settings we already have
|
||
|
|
if (array_key_exists($module->moduleId, $modulesWithSettings)) {
|
||
|
|
$moduleSettings = $modulesWithSettings[$module->moduleId];
|
||
|
|
$module->isInstalled = true;
|
||
|
|
|
||
|
|
// make sure canvas is always enabled
|
||
|
|
if ($module->moduleId === 'core-canvas') {
|
||
|
|
$module->enabled = 1;
|
||
|
|
// update the table
|
||
|
|
if ($moduleSettings->getInt('enabled', ['default' => 0]) === 0) {
|
||
|
|
$this->getStore()->update(
|
||
|
|
'UPDATE `module` SET enabled = 1 WHERE `module`.moduleId = \'core-canvas\' ',
|
||
|
|
[]
|
||
|
|
);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
$module->enabled = $moduleSettings->getInt('enabled', ['default' => 0]);
|
||
|
|
}
|
||
|
|
|
||
|
|
$module->previewEnabled = $moduleSettings->getInt('previewEnabled', ['default' => 0]);
|
||
|
|
$module->defaultDuration = $moduleSettings->getInt('defaultDuration', ['default' => 10]);
|
||
|
|
|
||
|
|
$settings = $moduleSettings->getString('settings');
|
||
|
|
if ($settings !== null) {
|
||
|
|
$settings = json_decode($settings, true);
|
||
|
|
|
||
|
|
foreach ($module->settings as $property) {
|
||
|
|
foreach ($settings as $settingId => $setting) {
|
||
|
|
if ($settingId === $property->id) {
|
||
|
|
$property->value = $setting;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
$module->properties = $this->parseProperties($xml->getElementsByTagName('properties'), $module);
|
||
|
|
} catch (\Exception $e) {
|
||
|
|
$module->errors[] = __('Invalid properties');
|
||
|
|
$this->getLog()->error('Module ' . $module->moduleId . ' has invalid properties. e: ' . $e->getMessage());
|
||
|
|
}
|
||
|
|
|
||
|
|
// Parse group property definitions.
|
||
|
|
try {
|
||
|
|
$module->propertyGroups = $this->parsePropertyGroups($xml->getElementsByTagName('propertyGroups'));
|
||
|
|
} catch (\Exception $e) {
|
||
|
|
$module->errors[] = __('Invalid property groups');
|
||
|
|
$this->getLog()->error('Module ' . $module->moduleId . ' has invalid property groups. e: '
|
||
|
|
. $e->getMessage());
|
||
|
|
}
|
||
|
|
|
||
|
|
// Parse required elements.
|
||
|
|
$requiredElements = $this->getFirstValueOrDefaultFromXmlNode($xml, 'requiredElements');
|
||
|
|
if (!empty($requiredElements)) {
|
||
|
|
$module->requiredElements = explode(',', $requiredElements);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Parse stencils
|
||
|
|
try {
|
||
|
|
$module->preview = $this->getStencils($xml->getElementsByTagName('preview'))[0] ?? null;
|
||
|
|
$module->stencil = $this->getStencils($xml->getElementsByTagName('stencil'))[0] ?? null;
|
||
|
|
} catch (\Exception $e) {
|
||
|
|
$module->errors[] = __('Invalid stencils');
|
||
|
|
$this->getLog()->error('Module ' . $module->moduleId . ' has invalid stencils. e: ' . $e->getMessage());
|
||
|
|
}
|
||
|
|
|
||
|
|
return $module;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create DataType from XML
|
||
|
|
* @param string $file
|
||
|
|
* @return \Xibo\Widget\Definition\DataType
|
||
|
|
*/
|
||
|
|
private function createDataTypeFromXml(string $file): DataType
|
||
|
|
{
|
||
|
|
$xml = new \DOMDocument();
|
||
|
|
$xml->load($file);
|
||
|
|
|
||
|
|
$dataType = new DataType();
|
||
|
|
$dataType->id = $this->getFirstValueOrDefaultFromXmlNode($xml, 'id');
|
||
|
|
$dataType->name = $this->getFirstValueOrDefaultFromXmlNode($xml, 'name');
|
||
|
|
|
||
|
|
// Fields.
|
||
|
|
foreach ($xml->getElementsByTagName('field') as $field) {
|
||
|
|
if ($field instanceof \DOMElement) {
|
||
|
|
$dataType->addField(
|
||
|
|
$field->getAttribute('id'),
|
||
|
|
trim($field->textContent),
|
||
|
|
$field->getAttribute('type'),
|
||
|
|
$field->getAttribute('isRequired') === 'true',
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return $dataType;
|
||
|
|
}
|
||
|
|
}
|