Files
Cloud-CMS/lib/Factory/ModuleTemplateFactory.php

645 lines
23 KiB
PHP
Raw Permalink Normal View History

2025-12-02 10:32:59 -05:00
<?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\Factory;
use Slim\Views\Twig;
use Stash\Interfaces\PoolInterface;
use Xibo\Entity\ModuleTemplate;
use Xibo\Support\Exception\NotFoundException;
use Xibo\Widget\Definition\Asset;
/**
* Factory for working with Module Templates
*/
class ModuleTemplateFactory extends BaseFactory
{
use ModuleXmlTrait;
/** @var ModuleTemplate[]|null */
private $templates = null;
/** @var \Stash\Interfaces\PoolInterface */
private $pool;
/** @var \Slim\Views\Twig */
private $twig;
/**
* Construct a factory
* @param PoolInterface $pool
* @param \Slim\Views\Twig $twig
*/
public function __construct(PoolInterface $pool, Twig $twig)
{
$this->pool = $pool;
$this->twig = $twig;
}
/**
* @param string $type The type of template (element|elementGroup|static)
* @param string $id
* @return \Xibo\Entity\ModuleTemplate
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function getByTypeAndId(string $type, string $id): ModuleTemplate
{
foreach ($this->load() as $template) {
if ($template->type === $type && $template->templateId === $id) {
return $template;
}
}
throw new NotFoundException(sprintf(__('%s not found for %s'), $type, $id));
}
/**
* @param int $id
* @return \Xibo\Entity\ModuleTemplate
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function getUserTemplateById(int $id): ModuleTemplate
{
$templates = $this->loadUserTemplates(null, ['id' => $id]);
if (count($templates) !== 1) {
throw new NotFoundException(sprintf(__('Template not found for %s'), $id));
}
return $templates[0];
}
/**
* @param string $dataType
* @param string $id
* @return \Xibo\Entity\ModuleTemplate
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function getByDataTypeAndId(string $dataType, string $id): ModuleTemplate
{
foreach ($this->load() as $template) {
if ($template->dataType === $dataType && $template->templateId === $id) {
return $template;
}
}
throw new NotFoundException(sprintf(__('Template not found for %s and %s'), $dataType, $id));
}
/**
* @param string $dataType
* @return ModuleTemplate[]
*/
public function getByDataType(string $dataType): array
{
$templates = [];
foreach ($this->load() as $template) {
if ($template->dataType === $dataType) {
$templates[] = $template;
}
}
return $templates;
}
/**
* @param string $type
* @param string $dataType
* @return ModuleTemplate[]
*/
public function getByTypeAndDataType(string $type, string $dataType, bool $includeUserTemplates = true): array
{
$templates = [];
foreach ($this->load($includeUserTemplates) as $template) {
if ($template->dataType === $dataType && $template->type === $type) {
$templates[] = $template;
}
}
return $templates;
}
/**
* @param string $assetId
* @return \Xibo\Widget\Definition\Asset
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function getAssetById(string $assetId): Asset
{
foreach ($this->load() as $template) {
foreach ($template->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
{
foreach ($this->load() as $template) {
foreach ($template->getAssets() as $asset) {
if ($asset->alias === $alias) {
return $asset;
}
}
}
throw new NotFoundException(__('Asset not found'));
}
/**
* Get an array of all modules
* @param string|null $ownership
* @param bool $includeUserTemplates
* @return ModuleTemplate[]
*/
public function getAll(?string $ownership = null, bool $includeUserTemplates = true): array
{
$templates = $this->load($includeUserTemplates);
if ($ownership === null) {
return $templates;
} else {
$ownedBy = [];
foreach ($templates as $template) {
if ($ownership === $template->ownership) {
$ownedBy[] = $template;
}
}
return $ownedBy;
}
}
/**
* Get an array of all modules
* @return Asset[]
*/
public function getAllAssets(): array
{
$assets = [];
foreach ($this->load() as $template) {
foreach ($template->getAssets() as $asset) {
$assets[$asset->id] = $asset;
}
}
return $assets;
}
/**
* Load templates
* @param bool $includeUserTemplates
* @return ModuleTemplate[]
*/
private function load(bool $includeUserTemplates = true): array
{
if ($this->templates === null) {
$this->getLog()->debug('load: Loading templates');
$this->templates = array_merge(
$this->loadFolder(
PROJECT_ROOT . '/modules/templates/*.xml',
'system',
),
$this->loadFolder(
PROJECT_ROOT . '/custom/modules/templates/*.xml',
'custom'
),
);
if ($includeUserTemplates) {
$this->templates = array_merge(
$this->templates,
$this->loadUserTemplates()
);
}
}
return $this->templates;
}
/**
* Load templates
* @return \Xibo\Entity\ModuleTemplate[]
*/
private function loadFolder(string $folder, string $ownership): array
{
$this->getLog()->debug('loadFolder: Loading templates from ' . $folder);
$templates = [];
foreach (glob($folder) as $file) {
// Create our module entity from this file
try {
$templates = array_merge($templates, $this->createMultiFromXml($file, $ownership));
} catch (\Exception $exception) {
$this->getLog()->error('Unable to create template from '
. basename($file) . ', skipping. e = ' . $exception->getMessage());
}
}
return $templates;
}
/**
* Load user templates from the database.
* @return ModuleTemplate[]
*/
public function loadUserTemplates($sortOrder = [], $filterBy = []): array
{
$this->getLog()->debug('load: Loading user templates');
if (empty($sortOrder)) {
$sortOrder = ['id'];
}
$templates = [];
$params = [];
$filter = $this->getSanitizer($filterBy);
$select = 'SELECT *,
(SELECT GROUP_CONCAT(DISTINCT `group`.group)
FROM `permission`
INNER JOIN `permissionentity`
ON `permissionentity`.entityId = permission.entityId
INNER JOIN `group`
ON `group`.groupId = `permission`.groupId
WHERE entity = :permissionEntityGroups
AND objectId = `module_templates`.id
AND view = 1
) AS groupsWithPermissions';
$body = ' FROM `module_templates`
WHERE 1 = 1 ';
if ($filter->getInt('id') !== null) {
$body .= ' AND `id` = :id ';
$params['id'] = $filter->getInt('id');
}
if (!empty($filter->getString('templateId'))) {
$body .= ' AND `templateId` LIKE :templateId ';
$params['templateId'] = '%' . $filter->getString('templateId') . '%';
}
if (!empty($filter->getString('dataType'))) {
$body .= ' AND `dataType` = :dataType ';
$params['dataType'] = $filter->getString('dataType') ;
}
$params['permissionEntityGroups'] = 'Xibo\\Entity\\ModuleTemplate';
$this->viewPermissionSql(
'Xibo\Entity\ModuleTemplate',
$body,
$params,
'module_templates.id',
'module_templates.ownerId',
$filterBy,
);
$order = '';
if (is_array($sortOrder) && !empty($sortOrder)) {
$order .= ' ORDER BY ' . implode(',', $sortOrder);
}
// Paging
$limit = '';
if ($filterBy !== null && $filter->getInt('start') !== null && $filter->getInt('length') !== null) {
$limit .= ' LIMIT ' .
$filter->getInt('start', ['default' => 0]) . ', ' .
$filter->getInt('length', ['default' => 10]);
}
$sql = $select . $body . $order. $limit;
foreach ($this->getStore()->select($sql, $params) as $row) {
$template = $this->createUserTemplate($row['xml']);
$template->id = intval($row['id']);
$template->templateId = $row['templateId'];
$template->dataType = $row['dataType'];
$template->isEnabled = $row['enabled'] == 1;
$template->ownerId = intval($row['ownerId'] ?? 0);
$template->groupsWithPermissions = $row['groupsWithPermissions'];
$templates[] = $template;
}
// Paging
if (!empty($limit) && count($templates) > 0) {
unset($params['permissionEntityGroups']);
$results = $this->getStore()->select('SELECT COUNT(*) AS total ' . $body, $params);
$this->_countLast = intval($results[0]['total']);
}
return $templates;
}
/**
* Create a user template from an XML string
* @param string $xmlString
* @return ModuleTemplate
*/
public function createUserTemplate(string $xmlString): ModuleTemplate
{
$xml = new \DOMDocument();
$xml->loadXML($xmlString);
$template = $this->createFromXml($xml->documentElement, 'user', 'database');
$template->setXml($xmlString);
$template->setDocument($xml);
return $template;
}
/**
* Create multiple templates from XML
* @param string $file
* @param string $ownership
* @return ModuleTemplate[]
*/
private function createMultiFromXml(string $file, string $ownership): array
{
$templates = [];
$xml = new \DOMDocument();
$xml->load($file);
foreach ($xml->getElementsByTagName('templates') as $node) {
if ($node instanceof \DOMElement) {
$this->getLog()->debug('createMultiFromXml: there are ' . count($node->childNodes)
. ' templates in ' . $file);
foreach ($node->childNodes as $childNode) {
if ($childNode instanceof \DOMElement) {
$templates[] = $this->createFromXml($childNode, $ownership, $file);
}
}
}
}
return $templates;
}
/**
* @param \DOMElement $xml
* @param string $ownership
* @param string $file
* @return \Xibo\Entity\ModuleTemplate
*/
private function createFromXml(\DOMElement $xml, string $ownership, string $file): ModuleTemplate
{
// TODO: cache this into Stash
$template = new ModuleTemplate($this->getStore(), $this->getLog(), $this->getDispatcher(), $this, $file);
$template->ownership = $ownership;
$template->templateId = $this->getFirstValueOrDefaultFromXmlNode($xml, 'id');
$template->type = $this->getFirstValueOrDefaultFromXmlNode($xml, 'type');
$template->dataType = $this->getFirstValueOrDefaultFromXmlNode($xml, 'dataType');
$template->title = __($this->getFirstValueOrDefaultFromXmlNode($xml, 'title'));
$template->description = __($this->getFirstValueOrDefaultFromXmlNode($xml, 'description'));
$template->thumbnail = $this->getFirstValueOrDefaultFromXmlNode($xml, 'thumbnail');
$template->icon = $this->getFirstValueOrDefaultFromXmlNode($xml, 'icon');
$template->isVisible = $this->getFirstValueOrDefaultFromXmlNode($xml, 'isVisible') !== 'false';
$template->startWidth = intval($this->getFirstValueOrDefaultFromXmlNode($xml, 'startWidth'));
$template->startHeight = intval($this->getFirstValueOrDefaultFromXmlNode($xml, 'startHeight'));
$template->hasDimensions = $this->getFirstValueOrDefaultFromXmlNode($xml, 'hasDimensions', 'true') === 'true';
$template->canRotate = $this->getFirstValueOrDefaultFromXmlNode($xml, 'canRotate', 'false') === 'true';
$template->onTemplateRender = $this->getFirstValueOrDefaultFromXmlNode($xml, 'onTemplateRender');
$template->onTemplateVisible = $this->getFirstValueOrDefaultFromXmlNode($xml, 'onTemplateVisible');
$template->onElementParseData = $this->getFirstValueOrDefaultFromXmlNode($xml, 'onElementParseData');
$template->showIn = $this->getFirstValueOrDefaultFromXmlNode($xml, 'showIn') ?? 'both';
if (!empty($template->onTemplateRender)) {
$template->onTemplateRender = trim($template->onTemplateRender);
}
if (!empty($template->onTemplateVisible)) {
$template->onTemplateVisible = trim($template->onTemplateVisible);
}
if (!empty($template->onElementParseData)) {
$template->onElementParseData = trim($template->onElementParseData);
}
$template->isError = false;
$template->errors = [];
// Parse extends definition
try {
$template->extends = $this->getExtends($xml->getElementsByTagName('extends'))[0] ?? null;
} catch (\Exception $e) {
$template->errors[] = __('Invalid Extends');
$this->getLog()->error('Module Template ' . $template->templateId
. ' has invalid extends definition. e: ' . $e->getMessage());
}
// Parse property definitions.
try {
$template->properties = $this->parseProperties($xml->getElementsByTagName('properties'));
} catch (\Exception $e) {
$template->errors[] = __('Invalid properties');
$this->getLog()->error('Module Template ' . $template->templateId
. ' has invalid properties. e: ' . $e->getMessage());
}
// Parse group property definitions.
try {
$template->propertyGroups = $this->parsePropertyGroups($xml->getElementsByTagName('propertyGroups'));
} catch (\Exception $e) {
$template->errors[] = __('Invalid property groups');
$this->getLog()->error('Module Template ' . $template->templateId . ' has invalid property groups. e: '
. $e->getMessage());
}
// Parse stencil
try {
$template->stencil = $this->getStencils($xml->getElementsByTagName('stencil'))[0] ?? null;
} catch (\Exception $e) {
$template->errors[] = __('Invalid stencils');
$this->getLog()->error('Module Template ' . $template->templateId
. ' has invalid stencils. e: ' . $e->getMessage());
}
// Parse assets
try {
$template->assets = $this->parseAssets($xml->getElementsByTagName('assets'));
} catch (\Exception $e) {
$template->errors[] = __('Invalid assets');
$this->getLog()->error('Module Template ' . $template->templateId
. ' has invalid assets. e: ' . $e->getMessage());
}
return $template;
}
/**
* Parse properties json into xml node.
*
* @param string $properties
* @return \DOMDocument
* @throws \DOMException
*/
public function parseJsonPropertiesToXml(string $properties): \DOMDocument
{
$newPropertiesXml = new \DOMDocument();
$newPropertiesNode = $newPropertiesXml->createElement('properties');
$attributes = [
'id',
'type',
'variant',
'format',
'mode',
'target',
'propertyGroupId',
'allowLibraryRefs',
'allowAssetRefs',
'parseTranslations',
'includeInXlf'
];
$commonNodes = [
'title',
'helpText',
'default',
'dependsOn',
'customPopOver'
];
$newProperties = json_decode($properties, true);
foreach ($newProperties as $property) {
// create property node
$propertyNode = $newPropertiesXml->createElement('property');
// go through possible attributes on the property node.
foreach ($attributes as $attribute) {
if (!empty($property[$attribute])) {
$propertyNode->setAttribute($attribute, $property[$attribute]);
}
}
// go through common nodes on property add them if not empty
foreach ($commonNodes as $commonNode) {
if (!empty($property[$commonNode])) {
$propertyNode->appendChild($newPropertiesXml->createElement($commonNode, $property[$commonNode]));
}
}
// do we have options?
if (!empty($property['options'])) {
$options = $property['options'];
if (!is_array($options)) {
$options = json_decode($options, true);
}
$optionsNode = $newPropertiesXml->createElement('options');
foreach ($options as $option) {
$optionNode = $newPropertiesXml->createElement('option', $option['title']);
$optionNode->setAttribute('name', $option['name']);
if (!empty($option['set'])) {
$optionNode->setAttribute('set', $option['set']);
}
if (!empty($option['image'])) {
$optionNode->setAttribute('image', $option['image']);
}
$optionsNode->appendChild($optionNode);
}
$propertyNode->appendChild($optionsNode);
}
// do we have visibility?
if (!empty($property['visibility'])) {
$visibility = $property['visibility'];
if (!is_array($visibility)) {
$visibility = json_decode($visibility, true);
}
$visibilityNode = $newPropertiesXml->createElement('visibility');
foreach ($visibility as $testElement) {
$testNode = $newPropertiesXml->createElement('test');
$testNode->setAttribute('type', $testElement['type']);
$testNode->setAttribute('message', $testElement['message']);
foreach ($testElement['conditions'] as $condition) {
$conditionNode = $newPropertiesXml->createElement('condition', $condition['value']);
$conditionNode->setAttribute('field', $condition['field']);
$conditionNode->setAttribute('type', $condition['type']);
$testNode->appendChild($conditionNode);
}
$visibilityNode->appendChild($testNode);
}
$propertyNode->appendChild($visibilityNode);
}
// do we have validation rules?
if (!empty($property['validation'])) {
$validation = $property['validation'];
if (!is_array($validation)) {
$validation = json_decode($property['validation'], true);
}
// xml uses rule node for this.
$ruleNode = $newPropertiesXml->createElement('rule');
// attributes on rule node;
$ruleNode->setAttribute('onSave', $validation['onSave'] ? 'true' : 'false');
$ruleNode->setAttribute('onStatus', $validation['onStatus'] ? 'true' : 'false');
// validation property has an array on tests in it
foreach ($validation['tests'] as $validationTest) {
$ruleTestNode = $newPropertiesXml->createElement('test');
$ruleTestNode->setAttribute('type', $validationTest['type']);
$ruleTestNode->setAttribute('message', $validationTest['message']);
foreach ($validationTest['conditions'] as $condition) {
$conditionNode = $newPropertiesXml->createElement('condition', $condition['value']);
$conditionNode->setAttribute('field', $condition['field']);
$conditionNode->setAttribute('type', $condition['type']);
$ruleTestNode->appendChild($conditionNode);
}
$ruleNode->appendChild($ruleTestNode);
}
$propertyNode->appendChild($ruleNode);
}
// do we have player compatibility?
if (!empty($property['playerCompatibility'])) {
$playerCompat = $property['playerCompatibility'];
if (!is_array($playerCompat)) {
$playerCompat = json_decode($property['playerCompatibility'], true);
}
$playerCompatibilityNode = $newPropertiesXml->createElement('playerCompatibility');
foreach ($playerCompat as $player => $value) {
$playerCompatibilityNode->setAttribute($player, $value);
}
$propertyNode->appendChild($playerCompatibilityNode);
}
$newPropertiesNode->appendChild($propertyNode);
}
$newPropertiesXml->appendChild($newPropertiesNode);
return $newPropertiesXml;
}
}