524 lines
22 KiB
PHP
524 lines
22 KiB
PHP
<?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 Illuminate\Support\Str;
|
|
use Xibo\Entity\Module;
|
|
use Xibo\Widget\Definition\Asset;
|
|
use Xibo\Widget\Definition\Element;
|
|
use Xibo\Widget\Definition\ElementGroup;
|
|
use Xibo\Widget\Definition\Extend;
|
|
use Xibo\Widget\Definition\LegacyType;
|
|
use Xibo\Widget\Definition\PlayerCompatibility;
|
|
use Xibo\Widget\Definition\Property;
|
|
use Xibo\Widget\Definition\PropertyGroup;
|
|
use Xibo\Widget\Definition\Rule;
|
|
use Xibo\Widget\Definition\Stencil;
|
|
|
|
/**
|
|
* A trait to help with parsing modules from XML
|
|
*/
|
|
trait ModuleXmlTrait
|
|
{
|
|
/**
|
|
* @var array cache of already loaded assets - id => asset
|
|
*/
|
|
private $assetCache = [];
|
|
|
|
/**
|
|
* Get stencils from a DOM node list
|
|
* @param \DOMNodeList $nodes
|
|
* @return Stencil[]
|
|
*/
|
|
private function getStencils(\DOMNodeList $nodes): array
|
|
{
|
|
$stencils = [];
|
|
|
|
foreach ($nodes as $node) {
|
|
$stencil = new Stencil();
|
|
|
|
/** @var \DOMNode $node */
|
|
foreach ($node->childNodes as $childNode) {
|
|
/** @var \DOMElement $childNode */
|
|
if ($childNode->nodeName === 'twig') {
|
|
$stencil->twig = $childNode->textContent;
|
|
} else if ($childNode->nodeName === 'hbs') {
|
|
$stencil->hbsId = $childNode->getAttribute('id');
|
|
$stencil->hbs = trim($childNode->textContent);
|
|
} else if ($childNode->nodeName === 'head') {
|
|
$stencil->head = trim($childNode->textContent);
|
|
} else if ($childNode->nodeName === 'style') {
|
|
$stencil->style = trim($childNode->textContent);
|
|
} else if ($childNode->nodeName === 'elements') {
|
|
$stencil->elements = $this->parseElements($childNode->childNodes);
|
|
} else if ($childNode->nodeName === 'width') {
|
|
$stencil->width = doubleval($childNode->textContent);
|
|
} else if ($childNode->nodeName === 'height') {
|
|
$stencil->height = doubleval($childNode->textContent);
|
|
} else if ($childNode->nodeName === 'gapBetweenHbs') {
|
|
$stencil->gapBetweenHbs = doubleval($childNode->textContent);
|
|
} else if ($childNode->nodeName === 'elementGroups') {
|
|
$stencil->elementGroups = $this->parseElementGroups($childNode->childNodes);
|
|
}
|
|
}
|
|
|
|
if ($stencil->twig !== null
|
|
|| $stencil->hbs !== null
|
|
|| $stencil->head !== null
|
|
|| $stencil->style !== null
|
|
) {
|
|
$stencils[] = $stencil;
|
|
}
|
|
}
|
|
|
|
return $stencils;
|
|
}
|
|
|
|
/**
|
|
* @param \DOMNode[]|\DOMNodeList $propertyNodes
|
|
* @return \Xibo\Widget\Definition\Property[]
|
|
* @throws \Xibo\Support\Exception\InvalidArgumentException
|
|
*/
|
|
private function parseProperties($propertyNodes, ?Module $module = null): array
|
|
{
|
|
if ($propertyNodes instanceof \DOMNodeList) {
|
|
// Property nodes are the parent node
|
|
if (count($propertyNodes) <= 0) {
|
|
return [];
|
|
}
|
|
$propertyNodes = $propertyNodes->item(0)->childNodes;
|
|
}
|
|
|
|
$defaultValues = [];
|
|
$properties = [];
|
|
foreach ($propertyNodes as $node) {
|
|
if ($node->nodeType === XML_ELEMENT_NODE) {
|
|
/** @var \DOMElement $node */
|
|
$property = new Property();
|
|
$property->id = $node->getAttribute('id');
|
|
$property->type = $node->getAttribute('type');
|
|
$property->variant = $node->getAttribute('variant');
|
|
$property->format = $node->getAttribute('format');
|
|
$property->mode = $node->getAttribute('mode');
|
|
$property->target = $node->getAttribute('target');
|
|
$property->propertyGroupId = $node->getAttribute('propertyGroupId');
|
|
$property->allowLibraryRefs = $node->getAttribute('allowLibraryRefs') === 'true';
|
|
$property->allowAssetRefs = $node->getAttribute('allowAssetRefs') === 'true';
|
|
$property->parseTranslations = $node->getAttribute('parseTranslations') === 'true';
|
|
$property->saveDefault = $node->getAttribute('saveDefault') === 'true';
|
|
$property->sendToElements = $node->getAttribute('sendToElements') === 'true';
|
|
$property->title = __($this->getFirstValueOrDefaultFromXmlNode($node, 'title'));
|
|
$property->helpText = __($this->getFirstValueOrDefaultFromXmlNode($node, 'helpText'));
|
|
$property->dependsOn = $this->getFirstValueOrDefaultFromXmlNode($node, 'dependsOn');
|
|
|
|
// How should we default includeInXlf?
|
|
if ($module?->renderAs === 'native') {
|
|
// Include by default
|
|
$property->includeInXlf = $node->getAttribute('includeInXlf') !== 'false';
|
|
} else {
|
|
// Exclude by default
|
|
$property->includeInXlf = $node->getAttribute('includeInXlf') === 'true';
|
|
}
|
|
|
|
// Default value
|
|
$defaultValue = $this->getFirstValueOrDefaultFromXmlNode($node, 'default');
|
|
|
|
// Is this a variable?
|
|
$defaultValues[$property->id] = $this->decorateWithSettings($module, $defaultValue);
|
|
|
|
// Validation (rule) conditions
|
|
$validationNodes = $node->getElementsByTagName('rule');
|
|
if (count($validationNodes) > 0) {
|
|
// We have a rule
|
|
$ruleNode = $validationNodes->item(0);
|
|
if ($ruleNode->nodeType === XML_ELEMENT_NODE) {
|
|
/** @var \DOMElement $ruleNode */
|
|
$rule = new Rule();
|
|
$rule->onSave = ($ruleNode->getAttribute('onSave') ?: 'true') === 'true';
|
|
$rule->onStatus = ($ruleNode->getAttribute('onStatus') ?: 'true') === 'true';
|
|
|
|
// Get tests
|
|
foreach ($ruleNode->childNodes as $testNode) {
|
|
if ($testNode->nodeType === XML_ELEMENT_NODE) {
|
|
/** @var \DOMElement $testNode */
|
|
$conditions = [];
|
|
foreach ($testNode->getElementsByTagName('condition') as $condNode) {
|
|
if ($condNode instanceof \DOMElement) {
|
|
$conditions[] = [
|
|
'field' => $condNode->getAttribute('field'),
|
|
'type' => $condNode->getAttribute('type'),
|
|
'value' => $this->decorateWithSettings($module, trim($condNode->textContent)),
|
|
];
|
|
}
|
|
}
|
|
|
|
$rule->addRuleTest($property->parseTest(
|
|
$testNode->getAttribute('type'),
|
|
$testNode->getAttribute('message'),
|
|
$conditions,
|
|
));
|
|
}
|
|
}
|
|
|
|
$property->validation = $rule;
|
|
}
|
|
}
|
|
|
|
// Options
|
|
$options = $node->getElementsByTagName('options');
|
|
if (count($options) > 0) {
|
|
foreach ($options->item(0)->childNodes as $optionNode) {
|
|
if ($optionNode->nodeType === XML_ELEMENT_NODE) {
|
|
$set = [];
|
|
if (!empty($optionNode->getAttribute('set'))) {
|
|
$set = explode(',', $optionNode->getAttribute('set'));
|
|
}
|
|
|
|
/** @var \DOMElement $optionNode */
|
|
$property->addOption(
|
|
$optionNode->getAttribute('name'),
|
|
$optionNode->getAttribute('image'),
|
|
$set,
|
|
trim($optionNode->textContent),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Visibility conditions
|
|
$visibility = $node->getElementsByTagName('visibility');
|
|
if (count($visibility) > 0) {
|
|
foreach ($visibility->item(0)->childNodes as $testNode) {
|
|
if ($testNode->nodeType === XML_ELEMENT_NODE) {
|
|
/** @var \DOMElement $testNode */
|
|
$conditions = [];
|
|
foreach ($testNode->getElementsByTagName('condition') as $condNode) {
|
|
if ($condNode instanceof \DOMElement) {
|
|
$conditions[] = [
|
|
'field' => $condNode->getAttribute('field'),
|
|
'type' => $condNode->getAttribute('type'),
|
|
'value' => $this->decorateWithSettings($module, trim($condNode->textContent)),
|
|
];
|
|
}
|
|
}
|
|
|
|
$property->addVisibilityTest(
|
|
$testNode->getAttribute('type'),
|
|
$testNode->getAttribute('message'),
|
|
$conditions,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Player compat
|
|
$playerCompat = $node->getElementsByTagName('playerCompatibility');
|
|
if (count($playerCompat) > 0) {
|
|
$playerCompat = $playerCompat->item(0);
|
|
if ($playerCompat->nodeType === XML_ELEMENT_NODE) {
|
|
/** @var \DOMElement $playerCompat */
|
|
$playerCompatibility = new PlayerCompatibility();
|
|
$playerCompatibility->message = $playerCompat->textContent;
|
|
if ($playerCompat->hasAttribute('windows')) {
|
|
$playerCompatibility->windows = $playerCompat->getAttribute('windows');
|
|
}
|
|
if ($playerCompat->hasAttribute('android')) {
|
|
$playerCompatibility->android = $playerCompat->getAttribute('android');
|
|
}
|
|
if ($playerCompat->hasAttribute('linux')) {
|
|
$playerCompatibility->linux = $playerCompat->getAttribute('linux');
|
|
}
|
|
if ($playerCompat->hasAttribute('webos')) {
|
|
$playerCompatibility->webos = $playerCompat->getAttribute('webos');
|
|
}
|
|
if ($playerCompat->hasAttribute('tizen')) {
|
|
$playerCompatibility->tizen = $playerCompat->getAttribute('tizen');
|
|
}
|
|
if ($playerCompat->hasAttribute('chromeos')) {
|
|
$playerCompatibility->chromeos = $playerCompat->getAttribute('chromeos');
|
|
}
|
|
$property->playerCompatibility = $playerCompatibility;
|
|
}
|
|
}
|
|
|
|
// Custom popover
|
|
$property->customPopOver = __($this->getFirstValueOrDefaultFromXmlNode($node, 'customPopOver'));
|
|
|
|
$properties[] = $property;
|
|
}
|
|
}
|
|
|
|
// Set the default values
|
|
$params = $this->getSanitizer($defaultValues);
|
|
foreach ($properties as $property) {
|
|
$property->setDefaultByType($params);
|
|
}
|
|
|
|
return $properties;
|
|
}
|
|
|
|
/**
|
|
* Take a value and decorate it with any module/global settings
|
|
* @param Module|null $module
|
|
* @param string|null $value
|
|
* @return string|null
|
|
*/
|
|
private function decorateWithSettings(?Module $module, ?string $value): ?string
|
|
{
|
|
// If we're not empty, then try and do any variable substitutions
|
|
if (!empty($value)) {
|
|
if ($module !== null
|
|
&& Str::startsWith($value, '%')
|
|
&& Str::endsWith($value, '%')
|
|
) {
|
|
$value = $module->getSetting(str_replace('%', '', $value));
|
|
} else if (Str::startsWith($value, '#')
|
|
&& Str::endsWith($value, '#')
|
|
) {
|
|
$value = $this->getConfig()->getSetting(str_replace('#', '', $value));
|
|
}
|
|
}
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* @param \DOMNode[]|\DOMNodeList $propertyGroupNodes
|
|
* @return array
|
|
*/
|
|
private function parsePropertyGroups($propertyGroupNodes): array
|
|
{
|
|
if ($propertyGroupNodes instanceof \DOMNodeList) {
|
|
// Property nodes are the parent node
|
|
if (count($propertyGroupNodes) <= 0) {
|
|
return [];
|
|
}
|
|
$propertyGroupNodes = $propertyGroupNodes->item(0)->childNodes;
|
|
}
|
|
|
|
$propertyGroups = [];
|
|
foreach ($propertyGroupNodes as $propertyGroupNode) {
|
|
/** @var \DOMNode $propertyGroupNode */
|
|
if ($propertyGroupNode instanceof \DOMElement) {
|
|
$propertyGroup = new PropertyGroup();
|
|
$propertyGroup->id = $propertyGroupNode->getAttribute('id');
|
|
$propertyGroup->expanded = $propertyGroupNode->getAttribute('expanded') === 'true';
|
|
$propertyGroup->title = __($this->getFirstValueOrDefaultFromXmlNode($propertyGroupNode, 'title'));
|
|
$propertyGroup->helpText = __($this->getFirstValueOrDefaultFromXmlNode($propertyGroupNode, 'helpText'));
|
|
$propertyGroups[] = $propertyGroup;
|
|
}
|
|
}
|
|
|
|
return $propertyGroups;
|
|
}
|
|
|
|
/**
|
|
* @param \DOMNodeList $elementsNodes
|
|
* @return \Xibo\Widget\Definition\Property[]
|
|
*/
|
|
private function parseElements(\DOMNodeList $elementsNodes): array
|
|
{
|
|
$elements = [];
|
|
foreach ($elementsNodes as $elementNode) {
|
|
/** @var \DOMNode $elementNode */
|
|
if ($elementNode instanceof \DOMElement) {
|
|
$element = new Element();
|
|
$element->id = $elementNode->getAttribute('id');
|
|
$element->elementGroupId = $elementNode->getAttribute('elementGroupId');
|
|
foreach ($elementNode->childNodes as $childNode) {
|
|
if ($childNode instanceof \DOMElement) {
|
|
if ($childNode->nodeName === 'top') {
|
|
$element->top = doubleval($childNode->textContent);
|
|
} else if ($childNode->nodeName === 'left') {
|
|
$element->left = doubleval($childNode->textContent);
|
|
} else if ($childNode->nodeName === 'width') {
|
|
$element->width = doubleval($childNode->textContent);
|
|
} else if ($childNode->nodeName === 'height') {
|
|
$element->height = doubleval($childNode->textContent);
|
|
} else if ($childNode->nodeName === 'layer') {
|
|
$element->layer = intval($childNode->textContent);
|
|
} else if ($childNode->nodeName === 'rotation') {
|
|
$element->rotation = intval($childNode->textContent);
|
|
} else if ($childNode->nodeName === 'defaultProperties') {
|
|
foreach ($childNode->childNodes as $defaultPropertyNode) {
|
|
if ($defaultPropertyNode instanceof \DOMElement) {
|
|
$element->properties[] = [
|
|
'id' => $defaultPropertyNode->getAttribute('id'),
|
|
'value' => trim($defaultPropertyNode->textContent)
|
|
];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
$elements[] = $element;
|
|
}
|
|
}
|
|
|
|
return $elements;
|
|
}
|
|
|
|
/**
|
|
* @param \DOMNodeList $elementGroupsNodes
|
|
* @return \Xibo\Widget\Definition\Property[]
|
|
*/
|
|
private function parseElementGroups (\DOMNodeList $elementGroupsNodes): array
|
|
{
|
|
$elementGroups = [];
|
|
foreach ($elementGroupsNodes as $elementGroupsNode) {
|
|
/** @var \DOMNode $elementNode */
|
|
if ($elementGroupsNode instanceof \DOMElement) {
|
|
$elementGroup = new ElementGroup();
|
|
$elementGroup->id = $elementGroupsNode->getAttribute('id');
|
|
foreach ($elementGroupsNode->childNodes as $childNode) {
|
|
if ($childNode instanceof \DOMElement) {
|
|
if ($childNode->nodeName === 'top') {
|
|
$elementGroup->top = doubleval($childNode->textContent);
|
|
} else if ($childNode->nodeName === 'left') {
|
|
$elementGroup->left = doubleval($childNode->textContent);
|
|
} else if ($childNode->nodeName === 'width') {
|
|
$elementGroup->width = doubleval($childNode->textContent);
|
|
} else if ($childNode->nodeName === 'height') {
|
|
$elementGroup->height = doubleval($childNode->textContent);
|
|
} else if ($childNode->nodeName === 'layer') {
|
|
$elementGroup->layer = intval($childNode->textContent);
|
|
} else if ($childNode->nodeName === 'title') {
|
|
$elementGroup->title = $childNode->textContent;
|
|
} else if ($childNode->nodeName === 'slot') {
|
|
$elementGroup->slot = intval($childNode->textContent);
|
|
} else if ($childNode->nodeName === 'pinSlot') {
|
|
$elementGroup->pinSlot = boolval($childNode->textContent);
|
|
}
|
|
}
|
|
}
|
|
$elementGroups[] = $elementGroup;
|
|
}
|
|
}
|
|
|
|
return $elementGroups;
|
|
}
|
|
|
|
/**
|
|
* @param \DOMNodeList $legacyTypeNodes
|
|
* @return \Xibo\Widget\Definition\LegacyType[]
|
|
*/
|
|
private function parseLegacyTypes(\DOMNodeList $legacyTypeNodes): array
|
|
{
|
|
$legacyTypes = [];
|
|
foreach ($legacyTypeNodes as $node) {
|
|
/** @var \DOMNode $node */
|
|
if ($node instanceof \DOMElement) {
|
|
$legacyType = new LegacyType();
|
|
$legacyType->name = trim($node->textContent);
|
|
$legacyType->condition = $node->getAttribute('condition');
|
|
|
|
$legacyTypes[] = $legacyType;
|
|
}
|
|
}
|
|
|
|
return $legacyTypes;
|
|
}
|
|
|
|
/**
|
|
* Parse assets
|
|
* @param \DOMNode[]|\DOMNodeList $assetNodes
|
|
* @return \Xibo\Widget\Definition\Asset[]
|
|
*/
|
|
private function parseAssets($assetNodes): array
|
|
{
|
|
if ($assetNodes instanceof \DOMNodeList) {
|
|
// Asset nodes are the parent node
|
|
if (count($assetNodes) <= 0) {
|
|
return [];
|
|
}
|
|
$assetNodes = $assetNodes->item(0)->childNodes;
|
|
}
|
|
|
|
$assets = [];
|
|
foreach ($assetNodes as $node) {
|
|
if ($node->nodeType === XML_ELEMENT_NODE) {
|
|
/** @var \DOMElement $node */
|
|
$assetId = $node->getAttribute('id');
|
|
|
|
if (!array_key_exists($assetId, $this->assetCache)) {
|
|
$asset = new Asset();
|
|
$asset->id = $assetId;
|
|
$asset->alias = $node->getAttribute('alias');
|
|
$asset->path = $node->getAttribute('path');
|
|
$asset->mimeType = $node->getAttribute('mimeType');
|
|
$asset->type = $node->getAttribute('type');
|
|
$asset->cmsOnly = $node->getAttribute('cmsOnly') === 'true';
|
|
$asset->autoInclude = $node->getAttribute('autoInclude') !== 'false';
|
|
$asset->assetNo = count($this->assetCache) + 1;
|
|
$this->assetCache[$assetId] = $asset;
|
|
}
|
|
|
|
$assets[] = $this->assetCache[$assetId];
|
|
}
|
|
}
|
|
|
|
return $assets;
|
|
}
|
|
|
|
/**
|
|
* Parse extends
|
|
* @param \DOMNodeList $nodes
|
|
* @return \Xibo\Widget\Definition\Asset[]
|
|
*/
|
|
private function getExtends(\DOMNodeList $nodes): array
|
|
{
|
|
$extends = [];
|
|
foreach ($nodes as $node) {
|
|
if ($node->nodeType === XML_ELEMENT_NODE) {
|
|
/** @var \DOMElement $node */
|
|
$extend = new Extend();
|
|
$extend->template = trim($node->textContent);
|
|
$extend->override = $node->getAttribute('override');
|
|
$extend->with = $node->getAttribute('with');
|
|
$extend->escapeHtml = $node->getAttribute('escapeHtml') !== 'false';
|
|
$extends[] = $extend;
|
|
}
|
|
}
|
|
|
|
return $extends;
|
|
}
|
|
|
|
/**
|
|
* Get the first node value
|
|
* @param \DOMDocument|\DOMElement $xml The XML document
|
|
* @param string $nodeName The no name
|
|
* @param string|null $default A default value is none is present
|
|
* @return string|null
|
|
*/
|
|
private function getFirstValueOrDefaultFromXmlNode($xml, string $nodeName, $default = null): ?string
|
|
{
|
|
foreach ($xml->getElementsByTagName($nodeName) as $node) {
|
|
/** @var \DOMNode $node */
|
|
if ($node->nodeType === XML_ELEMENT_NODE) {
|
|
return $node->textContent;
|
|
}
|
|
}
|
|
|
|
return $default;
|
|
}
|
|
}
|