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

300
lib/Entity/Action.php Normal file
View File

@@ -0,0 +1,300 @@
<?php
/*
* Copyright (C) 2022 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\Entity;
use Carbon\Carbon;
use Xibo\Helper\DateFormatHelper;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\InvalidArgumentException;
/**
* Class Action
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class Action implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The Action Id")
* @var int
*/
public $actionId;
/**
* @SWG\Property(description="The Owner Id")
* @var int
*/
public $ownerId;
/**
* @SWG\Property(description="The Action trigger type")
* @var string
*/
public $triggerType;
/**
* @SWG\Property(description="The Action trigger code")
* @var string
*/
public $triggerCode;
/**
* @SWG\Property(description="The Action type")
* @var string
*/
public $actionType;
/**
* @SWG\Property(description="The Action source (layout, region or widget)")
* @var string
*/
public $source;
/**
* @SWG\Property(description="The Action source Id (layoutId, regionId or widgetId)")
* @var int
*/
public $sourceId;
/**
* @SWG\Property(description="The Action target (region)")
* @var string
*/
public $target;
/**
* @SWG\Property(description="The Action target Id (regionId)")
* @var int
*/
public $targetId;
/**
* @SWG\Property(description="Widget ID that will be loaded as a result of navigate to Widget Action type")
* @var int
*/
public $widgetId;
/**
* @SWG\Property(description="Layout Code identifier")
* @var string
*/
public $layoutCode;
/**
* @SWG\Property(description="Layout Id associated with this Action")
* @var int
*/
public $layoutId;
/** @var \Xibo\Factory\PermissionFactory */
private $permissionFactory;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
public function __clone()
{
$this->hash = null;
$this->actionId = null;
}
/**
* Get the Id
* @return int
*/
public function getId()
{
return $this->actionId;
}
/**
* Get the OwnerId
* @return int
*/
public function getOwnerId()
{
return $this->ownerId;
}
/**
* Sets the Owner
* @param int $ownerId
*/
public function setOwner($ownerId)
{
$this->ownerId = $ownerId;
}
/**
* @return string
*/
public function __toString()
{
return sprintf('ActionId %d, Trigger Type %s, Trigger Code %s, Action Type %s, Source %s, SourceId %s, Target %s, TargetId %d', $this->actionId, $this->triggerType, $this->triggerCode, $this->actionType, $this->source, $this->sourceId, $this->target, $this->targetId);
}
/**
* @throws InvalidArgumentException
*/
public function validate()
{
// on add we expect only layoutId, actionType, target and targetId
if ($this->layoutId == null) {
throw new InvalidArgumentException(__('No layoutId specified'), 'layoutId');
}
if (!in_array($this->actionType, ['next', 'previous', 'navLayout', 'navWidget'])) {
throw new InvalidArgumentException(__('Invalid action type'), 'actionType');
}
if (!in_array(strtolower($this->source), ['layout', 'region', 'widget'])) {
throw new InvalidArgumentException(__('Invalid source'), 'source');
}
if (!in_array(strtolower($this->target), ['region', 'screen'])) {
throw new InvalidArgumentException(__('Invalid target'), 'target');
}
if ($this->target == 'region' && $this->targetId == null) {
throw new InvalidArgumentException(__('Please select a Region'), 'targetId');
}
if ($this->triggerType === 'webhook' && $this->triggerCode === null) {
throw new InvalidArgumentException(__('Please provide trigger code'), 'triggerCode');
}
if ($this->triggerType === 'keyPress' && $this->triggerCode === null) {
throw new InvalidArgumentException(__('Please provide trigger key'), 'triggerKey');
}
if (!in_array($this->triggerType, ['touch', 'webhook', 'keyPress'])) {
throw new InvalidArgumentException(__('Invalid trigger type'), 'triggerType');
}
if ($this->actionType === 'navLayout' && $this->layoutCode == '') {
throw new InvalidArgumentException(__('Please enter Layout code'), 'layoutCode');
}
if ($this->actionType === 'navWidget' && $this->widgetId == null) {
throw new InvalidArgumentException(__('Please create a Widget to be loaded'), 'widgetId');
}
}
/**
* @param array $options
* @throws InvalidArgumentException
*/
public function save($options = [])
{
$options = array_merge([
'validate' => true,
'notifyLayout' => false
], $options);
$this->getLog()->debug('Saving ' . $this);
if ($options['validate']) {
$this->validate();
}
if ($this->actionId == null || $this->actionId == 0) {
$this->add();
$this->loaded = true;
} else {
$this->update();
}
if ($options['notifyLayout'] && $this->layoutId != null) {
$this->notifyLayout($this->layoutId);
}
}
public function add()
{
$this->actionId = $this->getStore()->insert('INSERT INTO `action` (ownerId, triggerType, triggerCode, actionType, source, sourceId, target, targetId, widgetId, layoutCode, layoutId) VALUES (:ownerId, :triggerType, :triggerCode, :actionType, :source, :sourceId, :target, :targetId, :widgetId, :layoutCode, :layoutId)', [
'ownerId' => $this->ownerId,
'triggerType' => $this->triggerType,
'triggerCode' => $this->triggerCode,
'actionType' => $this->actionType,
'source' => $this->source,
'sourceId' => $this->sourceId,
'target' => $this->target,
'targetId' => $this->targetId,
'widgetId' => $this->widgetId,
'layoutCode' => $this->layoutCode,
'layoutId' => $this->layoutId
]);
}
public function update()
{
$this->getStore()->update('UPDATE `action` SET ownerId = :ownerId, triggerType = :triggerType, triggerCode = :triggerCode, actionType = :actionType, source = :source, sourceId = :sourceId, target = :target, targetId = :targetId, widgetId = :widgetId, layoutCode = :layoutCode, layoutId = :layoutId WHERE actionId = :actionId', [
'ownerId' => $this->ownerId,
'triggerType' => $this->triggerType,
'triggerCode' => $this->triggerCode,
'actionType' => $this->actionType,
'source' => $this->source,
'sourceId' => $this->sourceId,
'target' => $this->target,
'targetId' => $this->targetId,
'actionId' => $this->actionId,
'widgetId' => $this->widgetId,
'layoutCode' => $this->layoutCode,
'layoutId' => $this->layoutId
]);
}
public function delete()
{
$this->getStore()->update('DELETE FROM `action` WHERE actionId = :actionId', ['actionId' => $this->actionId]);
}
/**
* Notify the Layout (set to building)
* @param $layoutId
*/
public function notifyLayout($layoutId)
{
$this->getLog()->debug(sprintf('Saving Interactive Action ID %d triggered layout ID %d build', $this->actionId, $layoutId));
$this->getStore()->update('
UPDATE `layout` SET `status` = 3, `modifiedDT` = :modifiedDt WHERE layoutId = :layoutId
', [
'layoutId' => $layoutId,
'modifiedDt' => Carbon::now()->format(DateFormatHelper::getSystemFormat())
]);
}
}

458
lib/Entity/Application.php Normal file
View File

@@ -0,0 +1,458 @@
<?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\Entity;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use Xibo\Factory\ApplicationRedirectUriFactory;
use Xibo\Factory\ApplicationScopeFactory;
use Xibo\Helper\Random;
use Xibo\OAuth\ScopeEntity;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class Application
* @package Xibo\Entity
*
* @SWG\Definition
*/
class Application implements \JsonSerializable, ClientEntityInterface
{
use EntityTrait;
/**
* @SWG\Property(
* description="Application Key"
* )
* @var string
*/
public $key;
/**
* @SWG\Property(
* description="Private Secret Key"
* )
* @var string
*/
public $secret;
/**
* @SWG\Property(
* description="Application Name"
* )
* @var string
*/
public $name;
/**
* @SWG\Property(
* description="Application Owner"
* )
* @var string
*/
public $owner;
/**
* @SWG\Property(
* description="Application Session Expiry"
* )
* @var int
*/
public $expires;
/**
* @SWG\Property(
* description="The Owner of this Application"
* )
* @var int
*/
public $userId;
/**
* @SWG\Property(description="Flag indicating whether to allow the authorizationCode Grant Type")
* @var int
*/
public $authCode = 0;
/**
* @SWG\Property(description="Flag indicating whether to allow the clientCredentials Grant Type")
* @var int
*/
public $clientCredentials = 0;
/**
* @SWG\Property(description="Flag indicating whether this Application will be confidential or not (can it keep a secret?)")
* @var int
*/
public $isConfidential = 1;
/** * @var ApplicationRedirectUri[] */
public $redirectUris = [];
/** * @var ApplicationScope[] */
public $scopes = [];
/**
* @SWG\Property(description="Application description")
* @var string
*/
public $description;
/**
* @SWG\Property(description="Path to Application logo")
* @var string
*/
public $logo;
/**
* @SWG\Property(description="Path to Application Cover Image")
* @var string
*/
public $coverImage;
/**
* @SWG\Property(description="Company name associated with this Application")
* @var string
*/
public $companyName;
/**
* @SWG\Property(description="URL to Application terms")
* @var string
*/
public $termsUrl;
/**
* @SWG\Property(description="URL to Application privacy policy")
* @var string
*/
public $privacyUrl;
/** @var ApplicationRedirectUriFactory */
private $applicationRedirectUriFactory;
/** @var ApplicationScopeFactory */
private $applicationScopeFactory;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
* @param ApplicationRedirectUriFactory $applicationRedirectUriFactory
* @param ApplicationScopeFactory $applicationScopeFactory
*/
public function __construct($store, $log, $dispatcher, $applicationRedirectUriFactory, $applicationScopeFactory)
{
$this->setCommonDependencies($store, $log, $dispatcher);
$this->applicationRedirectUriFactory = $applicationRedirectUriFactory;
$this->applicationScopeFactory = $applicationScopeFactory;
}
public function __serialize(): array
{
return $this->jsonSerialize();
}
public function __unserialize(array $data): void
{
foreach ($data as $key => $value) {
$this->{$key} = $value;
}
}
/**
* @param ApplicationRedirectUri $redirectUri
*/
public function assignRedirectUri($redirectUri)
{
$this->load();
// Assert client id
$redirectUri->clientId = $this->key;
if (!in_array($redirectUri, $this->redirectUris)) {
$this->redirectUris[] = $redirectUri;
}
}
/**
* Unassign RedirectUri
* @param ApplicationRedirectUri $redirectUri
*/
public function unassignRedirectUri($redirectUri)
{
$this->load();
$this->redirectUris = array_udiff($this->redirectUris, [$redirectUri], function($a, $b) {
/**
* @var ApplicationRedirectUri $a
* @var ApplicationRedirectUri $b
*/
return $a->getId() - $b->getId();
});
}
/**
* @param ApplicationScope $scope
*/
public function assignScope($scope)
{
if (!in_array($scope, $this->scopes)) {
$this->scopes[] = $scope;
}
return $this;
}
/**
* @param ApplicationScope $scope
*/
public function unassignScope($scope)
{
$this->scopes = array_udiff($this->scopes, [$scope], function ($a, $b) {
/**
* @var ApplicationScope $a
* @var ApplicationScope $b
*/
return $a->getId() !== $b->getId();
});
}
/**
* Get the hash for password verify
* @return string
*/
public function getHash()
{
return password_hash($this->secret, PASSWORD_DEFAULT);
}
/**
* Load
* @return $this
*/
public function load()
{
if ($this->loaded || empty($this->key)) {
return $this;
}
// Redirects
$this->redirectUris = $this->applicationRedirectUriFactory->getByClientId($this->key);
// Get scopes
$this->scopes = $this->applicationScopeFactory->getByClientId($this->key);
$this->loaded = true;
return $this;
}
/**
* @return $this
*/
public function save()
{
if ($this->key == null || $this->key == '') {
// Make a new secret.
$this->resetSecret();
// Add
$this->add();
} else {
// Edit
$this->edit();
}
$this->getLog()->debug('Saving redirect uris: ' . json_encode($this->redirectUris));
foreach ($this->redirectUris as $redirectUri) {
$redirectUri->save();
}
$this->manageScopeAssignments();
return $this;
}
/**
* Delete
*/
public function delete()
{
$this->load();
foreach ($this->redirectUris as $redirectUri) {
$redirectUri->delete();
}
// Clear link table for this Application
$this->getStore()->update('DELETE FROM `oauth_lkclientuser` WHERE clientId = :id', ['id' => $this->key]);
// Clear out everything owned by this client
$this->getStore()->update('DELETE FROM `oauth_client_scopes` WHERE `clientId` = :id', ['id' => $this->key]);
$this->getStore()->update('DELETE FROM `oauth_clients` WHERE `id` = :id', ['id' => $this->key]);
}
/**
* Reset Secret
*/
public function resetSecret()
{
$this->secret = Random::generateString(254);
}
private function add()
{
// Make an ID
$this->key = Random::generateString(40);
// Simple Insert for now
$this->getStore()->insert('
INSERT INTO `oauth_clients` (`id`, `secret`, `name`, `userId`, `authCode`, `clientCredentials`, `isConfidential`, `description`, `logo`, `coverImage`, `companyName`, `termsUrl`, `privacyUrl`)
VALUES (:id, :secret, :name, :userId, :authCode, :clientCredentials, :isConfidential, :description, :logo, :coverImage, :companyName, :termsUrl, :privacyUrl)
', [
'id' => $this->key,
'secret' => $this->secret,
'name' => $this->name,
'userId' => $this->userId,
'authCode' => $this->authCode,
'clientCredentials' => $this->clientCredentials,
'isConfidential' => $this->isConfidential,
'description' => $this->description,
'logo' => $this->logo,
'coverImage' => $this->coverImage,
'companyName' => $this->companyName,
'termsUrl' => $this->termsUrl,
'privacyUrl' => $this->privacyUrl
]);
}
private function edit()
{
$this->getStore()->update('
UPDATE `oauth_clients` SET
`id` = :id,
`secret` = :secret,
`name` = :name,
`userId` = :userId,
`authCode` = :authCode,
`clientCredentials` = :clientCredentials,
`isConfidential` = :isConfidential,
`description` = :description,
`logo` = :logo,
`coverImage` = :coverImage,
`companyName` = :companyName,
`termsUrl` = :termsUrl,
`privacyUrl` = :privacyUrl
WHERE `id` = :id
', [
'id' => $this->key,
'secret' => $this->secret,
'name' => $this->name,
'userId' => $this->userId,
'authCode' => $this->authCode,
'clientCredentials' => $this->clientCredentials,
'isConfidential' => $this->isConfidential,
'description' => $this->description,
'logo' => $this->logo,
'coverImage' => $this->coverImage,
'companyName' => $this->companyName,
'termsUrl' => $this->termsUrl,
'privacyUrl' => $this->privacyUrl
]);
}
/**
* Compare the original assignments with the current assignments and delete any that are missing, add any new ones
*/
private function manageScopeAssignments()
{
$i = 0;
$params = ['clientId' => $this->key];
$unassignIn = '';
foreach ($this->scopes as $link) {
$this->getStore()->update('
INSERT INTO `oauth_client_scopes` (clientId, scopeId) VALUES (:clientId, :scopeId)
ON DUPLICATE KEY UPDATE scopeId = scopeId', [
'clientId' => $this->key,
'scopeId' => $link->id
]);
$i++;
$unassignIn .= ',:scopeId' . $i;
$params['scopeId' . $i] = $link->id;
}
// Unlink any NOT in the collection
$sql = 'DELETE FROM `oauth_client_scopes` WHERE clientId = :clientId AND scopeId NOT IN (\'0\'' . $unassignIn . ')';
$this->getStore()->update($sql, $params);
}
/** @inheritDoc */
public function getIdentifier()
{
return $this->key;
}
/** @inheritDoc */
public function getName()
{
return $this->name;
}
/** @inheritDoc */
public function getRedirectUri()
{
$count = count($this->redirectUris);
if ($count <= 0) {
return null;
} else if (count($this->redirectUris) == 1) {
return $this->redirectUris[0]->redirectUri;
} else {
return array_map(function($el) {
return $el->redirectUri;
}, $this->redirectUris);
}
}
/**
* @return \League\OAuth2\Server\Entities\ScopeEntityInterface[]
*/
public function getScopes()
{
$scopes = [];
foreach ($this->scopes as $applicationScope) {
$scope = new ScopeEntity();
$scope->setIdentifier($applicationScope->getId());
$scopes[] = $scope;
}
return $scopes;
}
/** @inheritDoc */
public function isConfidential()
{
return $this->isConfidential === 1;
}
}

View File

@@ -0,0 +1,123 @@
<?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\Entity;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class ApplicationRedirectUri
* @package Xibo\Entity
*/
class ApplicationRedirectUri implements \JsonSerializable
{
use EntityTrait;
/**
* @var int
*/
public $id;
/**
* @var string
*/
public $clientId;
/**
* @var string
*/
public $redirectUri;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
public function __serialize(): array
{
return $this->jsonSerialize();
}
public function __unserialize(array $data): void
{
foreach ($data as $key => $value) {
$this->{$key} = $value;
}
}
/**
* Get Id
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* Save
*/
public function save()
{
if ($this->id == null)
$this->add();
else
$this->edit();
}
public function delete()
{
$this->getStore()->update('DELETE FROM `oauth_client_redirect_uris` WHERE `id` = :id', ['id' => $this->id]);
}
private function add()
{
$this->id = $this->getStore()->insert('
INSERT INTO `oauth_client_redirect_uris` (`client_id`, `redirect_uri`)
VALUES (:clientId, :redirectUri)
', [
'clientId' => $this->clientId,
'redirectUri' => $this->redirectUri
]);
}
private function edit()
{
$this->getStore()->update('
UPDATE `oauth_client_redirect_uris`
SET `redirect_uri` = :redirectUri
WHERE `id` = :id
',[
'id' => $this->id,
'redirectUri' => $this->redirectUri
]);
}
}

View File

@@ -0,0 +1,98 @@
<?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\Entity;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Application Request
* @SWG\Definition()
*/
class ApplicationRequest implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The request ID")
* @var int
*/
public $requestId;
/**
* @SWG\Property(description="The user ID")
* @var int
*/
public $userId;
/**
* @SWG\Property(description="The application ID")
* @var string
*/
public $applicationId;
/**
* @SWG\Property(description="The request route")
* @var string
*/
public $url;
/**
* @SWG\Property(description="The request method")
* @var string
*/
public $method;
/**
* @SWG\Property(description="The request start time")
* @var string
*/
public $startTime;
/**
* @SWG\Property(description="The request end time")
* @var string
*/
public $endTime;
/**
* @SWG\Property(description="The request duration")
* @var int
*/
public $duration;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param EventDispatcherInterface $dispatcher
*/
public function __construct(
StorageServiceInterface $store,
LogServiceInterface $log,
EventDispatcherInterface $dispatcher
) {
$this->setCommonDependencies($store, $log, $dispatcher);
}
}

View File

@@ -0,0 +1,111 @@
<?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\Entity;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class ApplicationScope
* @package Xibo\Entity
*/
class ApplicationScope implements \JsonSerializable
{
use EntityTrait;
/**
* @var string
*/
public $id;
/**
* @var string
*/
public $description;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
public function __serialize(): array
{
return $this->jsonSerialize();
}
public function __unserialize(array $data): void
{
foreach ($data as $key => $value) {
$this->{$key} = $value;
}
}
/**
* Get Id
* @return string
*/
public function getId()
{
return $this->id;
}
/**
* Check whether this scope has permission for this route
* @param string $method
* @param string $requestedRoute
* @return bool
*/
public function checkRoute(string $method, string $requestedRoute): bool
{
$routes = $this->getStore()->select('
SELECT `route`
FROM `oauth_scope_routes`
WHERE `scopeId` = :scope
AND `method` LIKE :method
', [
'scope' => $this->getId(),
'method' => '%' . $method . '%',
]);
$this->getLog()->debug('checkRoute: there are ' . count($routes) . ' potential routes for the scope '
. $this->getId() . ' with ' . $method);
// We need to look through each route and run the regex against our requested route.
$grantAccess = false;
foreach ($routes as $route) {
$regexResult = preg_match($route['route'], $requestedRoute);
if ($regexResult === 1) {
$grantAccess = true;
break;
}
}
return $grantAccess;
}
}

108
lib/Entity/AuditLog.php Normal file
View File

@@ -0,0 +1,108 @@
<?php
/*
* Copyright (C) 2022-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\Entity;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class AuditLog
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class AuditLog implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The Log Id")
* @var int
*/
public $logId;
/**
* @SWG\Property(description="The Log Date")
* @var int
*/
public $logDate;
/**
* @SWG\Property(description="The userId of the User that took this action")
* @var int
*/
public $userId;
/**
* @SWG\Property(description="Message describing the action taken")
* @var string
*/
public $message;
/**
* @SWG\Property(description="The effected entity")
* @var string
*/
public $entity;
/**
* @SWG\Property(description="The effected entityId")
* @var int
*/
public $entityId;
/**
* @SWG\Property(description="A JSON representation of the object after it was changed")
* @var string
*/
public $objectAfter;
/**
* @SWG\Property(description="The User Name of the User that took this action")
* @var string
*/
public $userName;
/**
* @SWG\Property(description="The IP Address of the User that took this action")
* @var string
*/
public $ipAddress;
/**
* @SWG\Property(description="Session history id.")
* @var int
*/
public $sessionHistoryId;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
}

89
lib/Entity/Bandwidth.php Normal file
View File

@@ -0,0 +1,89 @@
<?php
/*
* Copyright (C) 2022 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\Entity;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\DeadlockException;
/**
* Class Bandwidth
* @package Xibo\Entity
*
*/
class Bandwidth
{
use EntityTrait;
public static $REGISTER = 1;
public static $RF = 2;
public static $SCHEDULE = 3;
public static $GETFILE = 4;
public static $GETRESOURCE = 5;
public static $MEDIAINVENTORY = 6;
public static $NOTIFYSTATUS = 7;
public static $SUBMITSTATS = 8;
public static $SUBMITLOG = 9;
public static $REPORTFAULT = 10;
public static $SCREENSHOT = 11;
public static $GET_DATA = 12;
public static $GET_DEPENDENCY = 13;
public $displayId;
public $type;
public $size;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
public function save()
{
try {
// This runs on the "isolated" connection because we do not want a failure here to impact the
// main transaction we've just completed (we log bandwidth at the end).
// Running on a separate transaction is cleaner than committing what we already have (debatable)
$this->getStore()->updateWithDeadlockLoop('
INSERT INTO `bandwidth` (Month, Type, DisplayID, Size)
VALUES (:month, :type, :displayId, :size)
ON DUPLICATE KEY UPDATE Size = Size + :size2
', [
'month' => strtotime(date('m') . '/02/' . date('Y') . ' 00:00:00'),
'type' => $this->type,
'displayId' => $this->displayId,
'size' => $this->size,
'size2' => $this->size
], 'isolated', false, true);
} catch (DeadlockException $deadlockException) {
$this->getLog()->error('Deadlocked inserting bandwidth');
}
}
}

1031
lib/Entity/Campaign.php Normal file

File diff suppressed because it is too large Load Diff

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\Entity;
/**
* Campaign Progress
*/
class CampaignProgress implements \JsonSerializable
{
/** @var int */
public $daysIn = 0;
/** @var int */
public $daysTotal = 0;
/** @var float */
public $targetPerDay = 0.0;
/** @var float */
public $progressTime = 0.0;
/** @var float */
public $progressTarget = 0.0;
/** @inheritDoc */
public function jsonSerialize(): array
{
return [
'daysIn' => $this->daysIn,
'daysTotal' => $this->daysTotal,
'targetPerDay' => $this->targetPerDay,
'progressTime' => $this->progressTime,
'progressTarget' => $this->progressTarget,
];
}
}

353
lib/Entity/Command.php Normal file
View File

@@ -0,0 +1,353 @@
<?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\Entity;
use Respect\Validation\Validator as v;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\InvalidArgumentException;
/**
* Class Command
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class Command implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(
* description="Command Id"
* )
* @var int
*/
public $commandId;
/**
* @SWG\Property(
* description="Command Name"
* )
* @var string
*/
public $command;
/**
* @SWG\Property(
* description="Unique Code"
* )
* @var string
*/
public $code;
/**
* @SWG\Property(
* description="Description"
* )
* @var string
*/
public $description;
/**
* @SWG\Property(
* description="User Id"
* )
* @var int
*/
public $userId;
/**
* @SWG\Property(
* description="Command String"
* )
* @var string
*/
public $commandString;
/**
* @SWG\Property(
* description="Validation String"
* )
* @var string
*/
public $validationString;
/**
* @SWG\Property(
* description="DisplayProfileId if specific to a Display Profile"
* )
* @var int
*/
public $displayProfileId;
/**
* @SWG\Property(
* description="Command String specific to the provided DisplayProfile"
* )
* @var string
*/
public $commandStringDisplayProfile;
/**
* @SWG\Property(
* description="Validation String specific to the provided DisplayProfile"
* )
* @var string
*/
public $validationStringDisplayProfile;
/**
* @SWG\Property(
* description="A comma separated list of player types this command is available on"
* )
* @var string
*/
public $availableOn;
/**
* @SWG\Property(
* description="Define if execution of this command should create an alert on success, failure, always or never."
* )
* @var string
*/
public $createAlertOn;
/**
* @SWG\Property(
* description="Create Alert On specific to the provided DisplayProfile."
* )
*/
public $createAlertOnDisplayProfile;
/**
* @SWG\Property(description="A comma separated list of groups/users with permissions to this Command")
* @var string
*/
public $groupsWithPermissions;
/**
* Command constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
/**
* Get Id
* @return int
*/
public function getId()
{
return $this->commandId;
}
/**
* Get OwnerId
* @return int
*/
public function getOwnerId()
{
return $this->userId;
}
/**
* @return string
*/
public function getCommandString()
{
return empty($this->commandStringDisplayProfile) ? $this->commandString : $this->commandStringDisplayProfile;
}
/**
* @return string
*/
public function getValidationString()
{
return empty($this->validationStringDisplayProfile)
? $this->validationString
: $this->validationStringDisplayProfile;
}
/**
* @return string
*/
public function getCreateAlertOn(): string
{
return empty($this->createAlertOnDisplayProfile)
? $this->createAlertOn
: $this->createAlertOnDisplayProfile;
}
/**
* @return array
*/
public function getAvailableOn()
{
return empty($this->availableOn) ? [] : explode(',', $this->availableOn);
}
/**
* @param string $type Player Type
* @return bool
*/
public function isAvailableOn($type)
{
$availableOn = $this->getAvailableOn();
return count($availableOn) <= 0 || in_array($type, $availableOn);
}
/**
* @return bool
*/
public function isReady()
{
return !empty($this->getCommandString());
}
/**
* Validate
* @throws InvalidArgumentException
*/
public function validate()
{
if (!v::stringType()->notEmpty()->length(1, 254)->validate($this->command)) {
throw new InvalidArgumentException(
__('Please enter a command name between 1 and 254 characters'),
'command'
);
}
if (!v::alpha('_')->NoWhitespace()->notEmpty()->length(1, 50)->validate($this->code)) {
throw new InvalidArgumentException(
__('Please enter a code between 1 and 50 characters containing only alpha characters and no spaces'),
'code'
);
}
if (!v::stringType()->length(0, 1000)->validate($this->description)) {
throw new InvalidArgumentException(
__('Please enter a description between 1 and 1000 characters'),
'description'
);
}
}
/**
* Save
* @param array $options
*
* @throws InvalidArgumentException
*/
public function save($options = [])
{
$options = array_merge($options, ['validate' => true]);
if ($options['validate']) {
$this->validate();
}
if ($this->commandId == null) {
$this->add();
} else {
$this->edit();
}
}
/**
* Delete
*/
public function delete()
{
$this->getStore()->update(
'DELETE FROM `command` WHERE `commandId` = :commandId',
['commandId' => $this->commandId]
);
}
private function add()
{
$this->commandId = $this->getStore()->insert('
INSERT INTO `command` (
`command`,
`code`,
`description`,
`userId`,
`commandString`,
`validationString`,
`availableOn`,
`createAlertOn`
)
VALUES (
:command,
:code,
:description,
:userId,
:commandString,
:validationString,
:availableOn,
:createAlertOn
)
', [
'command' => $this->command,
'code' => $this->code,
'description' => $this->description,
'userId' => $this->userId,
'commandString' => $this->commandString,
'validationString' => $this->validationString,
'availableOn' => $this->availableOn,
'createAlertOn' => $this->createAlertOn
]);
}
private function edit()
{
$this->getStore()->update('
UPDATE `command` SET
`command` = :command,
`code` = :code,
`description` = :description,
`userId` = :userId,
`commandString` = :commandString,
`validationString` = :validationString,
`availableOn` = :availableOn,
`createAlertOn` = :createAlertOn
WHERE `commandId` = :commandId
', [
'command' => $this->command,
'code' => $this->code,
'description' => $this->description,
'userId' => $this->userId,
'commandId' => $this->commandId,
'commandString' => $this->commandString,
'validationString' => $this->validationString,
'availableOn' => $this->availableOn,
'createAlertOn' => $this->createAlertOn
]);
}
}

135
lib/Entity/Connector.php Normal file
View File

@@ -0,0 +1,135 @@
<?php
/*
* Copyright (c) 2022 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\Entity;
use Xibo\Connector\ConnectorInterface;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\InvalidArgumentException;
/**
* Represents the database object for a Connector
*
* @SWG\Definition()
*/
class Connector implements \JsonSerializable
{
use EntityTrait;
// Status properties
public $isInstalled = true;
public $isSystem = true;
// Database properties
public $connectorId;
public $className;
public $settings;
public $isEnabled;
public $isVisible;
// Decorated properties
public $title;
public $description;
public $thumbnail;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
/**
* @param \Xibo\Connector\ConnectorInterface $connector
* @return $this
*/
public function decorate(ConnectorInterface $connector): Connector
{
$this->title = $connector->getTitle();
$this->description = $connector->getDescription();
$this->thumbnail = $connector->getThumbnail();
if (empty($this->thumbnail)) {
$this->thumbnail = 'theme/default/img/connectors/placeholder.png';
}
return $this;
}
public function save()
{
if ($this->connectorId == null || $this->connectorId == 0) {
$this->add();
} else {
$this->edit();
}
}
private function add()
{
$this->connectorId = $this->getStore()->insert('
INSERT INTO `connectors` (`className`, `isEnabled`, `isVisible`, `settings`)
VALUES (:className, :isEnabled, :isVisible, :settings)
', [
'className' => $this->className,
'isEnabled' => $this->isEnabled,
'isVisible' => $this->isVisible,
'settings' => json_encode($this->settings)
]);
}
private function edit()
{
$this->getStore()->update('
UPDATE `connectors` SET
`className` = :className,
`isEnabled` = :isEnabled,
`isVisible` = :isVisible,
`settings` = :settings
WHERE connectorId = :connectorId
', [
'connectorId' => $this->connectorId,
'className' => $this->className,
'isEnabled' => $this->isEnabled,
'isVisible' => $this->isVisible,
'settings' => json_encode($this->settings)
]);
}
/**
* @return void
* @throws \Xibo\Support\Exception\InvalidArgumentException
*/
public function delete()
{
if ($this->isSystem) {
throw new InvalidArgumentException(__('Sorry we cannot delete a system connector.'), 'isSystem');
}
$this->getStore()->update('DELETE FROM `connectors` WHERE connectorId = :connectorId', [
'connectorId' => $this->connectorId
]);
}
}

1412
lib/Entity/DataSet.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,521 @@
<?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\Entity;
use Illuminate\Support\Str;
use Respect\Validation\Validator as v;
use Xibo\Factory\DataSetColumnFactory;
use Xibo\Factory\DataSetColumnTypeFactory;
use Xibo\Factory\DataTypeFactory;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
use Xibo\Widget\Definition\Sql;
/**
* Class DataSetColumn
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class DataSetColumn implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The ID of this DataSetColumn")
* @var int
*/
public $dataSetColumnId;
/**
* @SWG\Property(description="The ID of the DataSet that this Column belongs to")
* @var int
*/
public $dataSetId;
/**
* @SWG\Property(description="The Column Heading")
* @var string
*/
public $heading;
/**
* @SWG\Property(description="The ID of the DataType for this Column")
* @var int
*/
public $dataTypeId;
/**
* @SWG\Property(description="The ID of the ColumnType for this Column")
* @var int
*/
public $dataSetColumnTypeId;
/**
* @SWG\Property(description="Comma separated list of valid content for drop down columns")
* @var string
*/
public $listContent;
/**
* @SWG\Property(description="The order this column should be displayed")
* @var int
*/
public $columnOrder;
/**
* @SWG\Property(description="A MySQL formula for this column")
* @var string
*/
public $formula;
/**
* @SWG\Property(description="The data type for this Column")
* @var string
*/
public $dataType;
/**
* @SWG\Property(description="The data field of the remote DataSet as a JSON-String")
* @var string
*/
public $remoteField;
/**
* @SWG\Property(description="Does this column show a filter on the data entry page?")
* @var string
*/
public $showFilter = 0;
/**
* @SWG\Property(description="Does this column allow a sorting on the data entry page?")
* @var string
*/
public $showSort = 0;
/**
* @SWG\Property(description="The column type for this Column")
* @var string
*/
public $dataSetColumnType;
/**
* @SWG\Property(description="Help text that should be displayed when entering data for this Column.")
* @var string
*/
public $tooltip;
/**
* @SWG\Property(description="Flag indicating whether value must be provided for this Column.")
* @var int
*/
public $isRequired = 0;
/**
* @SWG\Property(description="Date format of dates in the source for remote DataSet.")
* @var string
*/
public $dateFormat;
/** @var DataSetColumnFactory */
private $dataSetColumnFactory;
/** @var DataTypeFactory */
private $dataTypeFactory;
/** @var DataSetColumnTypeFactory */
private $dataSetColumnTypeFactory;
/**
* The prior dataset column id, when cloning
* @var int
*/
public $priorDatasetColumnId;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
* @param DataSetColumnFactory $dataSetColumnFactory
* @param DataTypeFactory $dataTypeFactory
* @param DataSetColumnTypeFactory $dataSetColumnTypeFactory
*/
public function __construct($store, $log, $dispatcher, $dataSetColumnFactory, $dataTypeFactory, $dataSetColumnTypeFactory)
{
$this->excludeProperty('priorDatasetColumnId');
$this->setCommonDependencies($store, $log, $dispatcher);
$this->dataSetColumnFactory = $dataSetColumnFactory;
$this->dataTypeFactory = $dataTypeFactory;
$this->dataSetColumnTypeFactory = $dataSetColumnTypeFactory;
}
/**
* Clone
*/
public function __clone()
{
$this->priorDatasetColumnId = $this->dataSetColumnId;
$this->dataSetColumnId = null;
$this->dataSetId = null;
}
/**
* List Content Array
* @return array
*/
public function listContentArray()
{
return explode(',', $this->listContent);
}
/**
* Validate
* @throws InvalidArgumentException
*/
public function validate($options = []): void
{
$options = array_merge([
'testFormulas' => true,
'allowSpacesInHeading' => false,
], $options);
if ($this->dataSetId == 0 || $this->dataSetId == '') {
throw new InvalidArgumentException(__('Missing dataSetId'), 'dataSetId');
}
if ($this->dataTypeId == 0 || $this->dataTypeId == '') {
throw new InvalidArgumentException(__('Missing dataTypeId'), 'dataTypeId');
}
if ($this->dataSetColumnTypeId == 0 || $this->dataSetColumnTypeId == '') {
throw new InvalidArgumentException(__('Missing dataSetColumnTypeId'), 'dataSetColumnTypeId');
}
if ($this->heading == '') {
throw new InvalidArgumentException(__('Please provide a column heading.'), 'heading');
}
// Column heading should not allow reserved/disallowed SQL words
if (Str::contains($this->heading, Sql::DISALLOWED_KEYWORDS, true)) {
throw new InvalidArgumentException(
sprintf(
__('Headings cannot contain reserved words, such as %s'),
implode(', ', Sql::DISALLOWED_KEYWORDS),
),
'heading',
);
}
// We allow spaces here for backwards compatibility, but only on import and edit.
$additionalCharacters = $options['allowSpacesInHeading'] ? ' ' : '';
if (!v::stringType()->alnum($additionalCharacters)->validate($this->heading)
|| strtolower($this->heading) == 'id'
) {
throw new InvalidArgumentException(sprintf(
__('Please provide an alternative column heading %s can not be used.'),
$this->heading
), 'heading');
}
if ($this->dataSetColumnTypeId == 2 && $this->formula == '') {
throw new InvalidArgumentException(__('Please enter a valid formula'), 'formula');
}
// Make sure this column name is unique
$columns = $this->dataSetColumnFactory->getByDataSetId($this->dataSetId);
foreach ($columns as $column) {
if ($column->heading == $this->heading
&& ($this->dataSetColumnId == null || $column->dataSetColumnId != $this->dataSetColumnId)
) {
throw new InvalidArgumentException(
__('A column already exists with this name, please choose another'),
'heading',
);
}
}
// Check the actual values
try {
$this->dataTypeFactory->getById($this->dataTypeId);
} catch (NotFoundException $e) {
throw new InvalidArgumentException(__('Provided Data Type doesn\'t exist'), 'datatype');
}
try {
$dataSetColumnType = $this->dataSetColumnTypeFactory->getById($this->dataSetColumnTypeId);
// If we are a remote column, validate we have a field
if (strtolower($dataSetColumnType->dataSetColumnType) === 'remote'
&& ($this->remoteField === '' || $this->remoteField === null)) {
throw new InvalidArgumentException(
__('Remote field is required when the column type is set to Remote'),
'remoteField',
);
}
} catch (NotFoundException) {
throw new InvalidArgumentException(
__('Provided DataSet Column Type doesn\'t exist'),
'dataSetColumnTypeId',
);
}
// Should we validate the list content?
if ($this->dataSetColumnId != 0 && $this->listContent != '') {
// Look up all DataSet data in this table to make sure that the existing data is covered by the list content
$list = $this->listContentArray();
// Add an empty field
$list[] = '';
// We can check this is valid by building up a NOT IN sql statement, if we get results we know it's not good
$select = '';
$dbh = $this->getStore()->getConnection('isolated');
for ($i=0; $i < count($list); $i++) {
if (!empty($list[$i])) {
$list_val = $dbh->quote($list[$i]);
$select .= $list_val . ',';
}
}
$select = rtrim($select, ',');
// $select has been quoted in the for loop
// always test the original value of the column (we won't have changed the actualised table yet)
$SQL = 'SELECT id FROM `dataset_' . $this->dataSetId . '` WHERE `' . $this->getOriginalValue('heading') . '` NOT IN (' . $select . ')';//phpcs:ignore
$sth = $dbh->prepare($SQL);
$sth->execute();
if ($sth->fetch()) {
throw new InvalidArgumentException(
__('New list content value is invalid as it does not include values for existing data'),
'listcontent'
);
}
}
// if formula dataSetType is set and formula is not empty, try to execute the SQL to validate it - we're
// ignoring client side formulas here.
if ($options['testFormulas']
&& $this->dataSetColumnTypeId == 2
&& $this->formula != ''
&& !str_starts_with($this->formula, '$')
) {
try {
$count = 0;
$formula = str_ireplace(
Sql::DISALLOWED_KEYWORDS,
'',
htmlspecialchars_decode($this->formula, ENT_QUOTES),
$count
);
if ($count > 0) {
throw new InvalidArgumentException(__('Formula contains disallowed keywords.'));
}
$formula = str_replace('[DisplayId]', 0, $formula);
$formula = str_replace('[DisplayGeoLocation]', "ST_GEOMFROMTEXT('POINT(51.504 -0.104)')", $formula);
$this->getStore()->select('
SELECT *
FROM (
SELECT `id`, ' . $formula . ' AS `' . $this->heading . '`
FROM `dataset_' . $this->dataSetId . '`
) dataset
', [], 'isolated');
} catch (\Exception $e) {
$this->getLog()->debug('Formula validation failed with following message ' . $e->getMessage());
throw new InvalidArgumentException(__('Provided formula is invalid'), 'formula');
}
}
}
/**
* Save
* @param array $options
* @throws InvalidArgumentException
*/
public function save($options = [])
{
$options = array_merge([
'validate' => true,
'rebuilding' => false,
], $options);
if ($options['validate'] && !$options['rebuilding']) {
$this->validate($options);
}
if ($this->dataSetColumnId == 0) {
$this->add();
} else {
$this->edit($options);
}
}
/**
* Delete
*/
public function delete(bool $isDeletingDataset = false): void
{
$this->getStore()->update('DELETE FROM `datasetcolumn` WHERE DataSetColumnID = :dataSetColumnId', ['dataSetColumnId' => $this->dataSetColumnId]);
// Delete column (unless remote, or dropping the whole dataset)
if (!$isDeletingDataset && $this->dataSetColumnTypeId !== 2) {
$this->getStore()->update('ALTER TABLE `dataset_' . $this->dataSetId . '` DROP `' . $this->heading . '`', []);
}
}
/**
* Add
*/
private function add()
{
$this->dataSetColumnId = $this->getStore()->insert('
INSERT INTO `datasetcolumn` (DataSetID, Heading, DataTypeID, ListContent, ColumnOrder, DataSetColumnTypeID, Formula, RemoteField, `showFilter`, `showSort`, `tooltip`, `isRequired`, `dateFormat`)
VALUES (:dataSetId, :heading, :dataTypeId, :listContent, :columnOrder, :dataSetColumnTypeId, :formula, :remoteField, :showFilter, :showSort, :tooltip, :isRequired, :dateFormat)
', [
'dataSetId' => $this->dataSetId,
'heading' => $this->heading,
'dataTypeId' => $this->dataTypeId,
'listContent' => $this->listContent,
'columnOrder' => $this->columnOrder,
'dataSetColumnTypeId' => $this->dataSetColumnTypeId,
'formula' => $this->formula,
'remoteField' => $this->remoteField,
'showFilter' => $this->showFilter,
'showSort' => $this->showSort,
'tooltip' => $this->tooltip,
'isRequired' => $this->isRequired,
'dateFormat' => $this->dateFormat
]);
// Add Column to Underlying Table
if (($this->dataSetColumnTypeId == 1) || ($this->dataSetColumnTypeId == 3)) {
// Use a separate connection for DDL (it operates outside transactions)
$this->getStore()->update('ALTER TABLE `dataset_' . $this->dataSetId . '` ADD `' . $this->heading . '` ' . $this->sqlDataType() . ' NULL', [], 'isolated', false, false);
}
}
/**
* Edit
* @param array $options
* @throws InvalidArgumentException
*/
private function edit($options)
{
$params = [
'dataSetId' => $this->dataSetId,
'heading' => $this->heading,
'dataTypeId' => $this->dataTypeId,
'listContent' => $this->listContent,
'columnOrder' => $this->columnOrder,
'dataSetColumnTypeId' => $this->dataSetColumnTypeId,
'formula' => $this->formula,
'dataSetColumnId' => $this->dataSetColumnId,
'remoteField' => $this->remoteField,
'showFilter' => $this->showFilter,
'showSort' => $this->showSort,
'tooltip' => $this->tooltip,
'isRequired' => $this->isRequired,
'dateFormat' => $this->dateFormat
];
$sql = '
UPDATE `datasetcolumn` SET
dataSetId = :dataSetId,
Heading = :heading,
ListContent = :listContent,
ColumnOrder = :columnOrder,
DataTypeID = :dataTypeId,
DataSetColumnTypeID = :dataSetColumnTypeId,
Formula = :formula,
RemoteField = :remoteField,
`showFilter` = :showFilter,
`showSort` = :showSort,
`tooltip` = :tooltip,
`isRequired` = :isRequired,
`dateFormat` = :dateFormat
WHERE dataSetColumnId = :dataSetColumnId
';
$this->getStore()->update($sql, $params);
try {
if ($options['rebuilding'] && ($this->dataSetColumnTypeId == 1 || $this->dataSetColumnTypeId == 3)) {
$this->getStore()->update('ALTER TABLE `dataset_' . $this->dataSetId . '` ADD `' . $this->heading . '` ' . $this->sqlDataType() . ' NULL', [], 'isolated', false, false);
} else if (($this->dataSetColumnTypeId == 1 || $this->dataSetColumnTypeId == 3)
&& ($this->hasPropertyChanged('heading') || $this->hasPropertyChanged('dataTypeId'))) {
$sql = 'ALTER TABLE `dataset_' . $this->dataSetId . '` CHANGE `' . $this->getOriginalValue('heading') . '` `' . $this->heading . '` ' . $this->sqlDataType() . ' NULL DEFAULT NULL';
$this->getStore()->update($sql, [], 'isolated', false, false);
}
} catch (\PDOException $PDOException) {
$this->getLog()->error('Unable to change DataSetColumn because ' . $PDOException->getMessage());
throw new InvalidArgumentException(__('Existing data is incompatible with your new configuration'), 'dataSetData');
}
}
/**
* Get the SQL Data Type for this Column Definition
* @return string
*/
private function sqlDataType()
{
$dataType = null;
switch ($this->dataTypeId) {
case 2:
$dataType = 'DOUBLE';
break;
case 3:
$dataType = 'DATETIME';
break;
case 5:
$dataType = 'INT';
break;
case 1:
case 6:
$dataType = 'TEXT';
break;
case 4:
default:
$dataType = 'VARCHAR(1000)';
}
return $dataType;
}
}

View File

@@ -0,0 +1,59 @@
<?php
/*
* Copyright (c) 2022 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\Entity;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class DataSetColumnType
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class DataSetColumnType implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The ID for this DataSetColumnType")
* @var int
*/
public $dataSetColumnTypeId;
/**
* @SWG\Property(description="The name for this DataSetColumnType")
* @var string
*/
public $dataSetColumnType;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
}

164
lib/Entity/DataSetRss.php Normal file
View File

@@ -0,0 +1,164 @@
<?php
/*
* Copyright (c) 2022 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\Entity;
use Xibo\Helper\Random;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class DataSetRss
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class DataSetRss implements \JsonSerializable
{
use EntityTrait;
public $id;
public $dataSetId;
public $titleColumnId;
public $summaryColumnId;
public $contentColumnId;
public $publishedDateColumnId;
public $psk;
public $title;
public $author;
public $sort;
public $filter;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
/**
* @return array|mixed
*/
public function getFilter()
{
return ($this->filter == '') ? ['filter' => '', 'useFilteringClause' => 0, 'filterClauses' => []] : json_decode($this->filter, true);
}
/**
* @return array|mixed
*/
public function getSort()
{
return ($this->sort == '') ? ['sort' => '', 'useOrderingClause' => 0, 'orderClauses' => []] : json_decode($this->sort, true);
}
/**
* Save
*/
public function save()
{
if ($this->id == null) {
$this->add();
$this->audit($this->id, 'Added', []);
} else {
$this->edit();
$this->audit($this->id, 'Saved');
}
}
/**
* @return $this
* @throws \Exception
*/
public function setNewPsk()
{
$this->psk = Random::generateString(12);
return $this;
}
/**
* Delete
*/
public function delete()
{
$this->getStore()->update('DELETE FROM `datasetrss` WHERE id = :id', ['id' => $this->id]);
$this->audit($this->id, 'Deleted');
}
private function add()
{
$this->id = $this->getStore()->insert('
INSERT INTO datasetrss (dataSetId, psk, title, author, titleColumnId, summaryColumnId, contentColumnId, publishedDateColumnId, sort, filter) VALUES
(:dataSetId, :psk, :title, :author, :titleColumnId, :summaryColumnId, :contentColumnId, :publishedDateColumnId, :sort, :filter)
', [
'dataSetId' => $this->dataSetId,
'psk' => $this->psk,
'title' => $this->title,
'author' => $this->author,
'titleColumnId' => $this->titleColumnId,
'summaryColumnId' => $this->summaryColumnId,
'contentColumnId' => $this->contentColumnId,
'publishedDateColumnId' => $this->publishedDateColumnId,
'sort' => $this->sort,
'filter' => $this->filter
]);
}
private function edit()
{
$this->getStore()->update('
UPDATE `datasetrss` SET
psk = :psk,
title = :title,
author = :author,
titleColumnId = :titleColumnId,
summaryColumnId = :summaryColumnId,
contentColumnId = :contentColumnId,
publishedDateColumnId = :publishedDateColumnId,
sort = :sort,
filter = :filter
WHERE id = :id
', [
'id' => $this->id,
'psk' => $this->psk,
'title' => $this->title,
'author' => $this->author,
'titleColumnId' => $this->titleColumnId,
'summaryColumnId' => $this->summaryColumnId,
'contentColumnId' => $this->contentColumnId,
'publishedDateColumnId' => $this->publishedDateColumnId,
'sort' => $this->sort,
'filter' => $this->filter
]);
}
}

61
lib/Entity/DataType.php Normal file
View File

@@ -0,0 +1,61 @@
<?php
/*
* Copyright (c) 2022 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\Entity;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class DataType
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class DataType implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The ID for this DataType")
* @var int
*/
public $dataTypeId;
/**
* @SWG\Property(description="The Name for this DataType")
* @var string
*/
public $dataType;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
}

357
lib/Entity/DayPart.php Normal file
View File

@@ -0,0 +1,357 @@
<?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\Entity;
use Carbon\Carbon;
use Respect\Validation\Validator as v;
use Xibo\Event\DayPartDeleteEvent;
use Xibo\Factory\ScheduleFactory;
use Xibo\Service\DisplayNotifyServiceInterface;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
/**
* Class DayPart
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class DayPart implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The ID of this Daypart")
* @var int
*/
public $dayPartId;
public $name;
public $description;
public $isRetired;
public $userId;
public $startTime;
public $endTime;
public $exceptions;
/**
* @SWG\Property(description="A readonly flag determining whether this DayPart is always")
* @var int
*/
public $isAlways = 0;
/**
* @SWG\Property(description="A readonly flag determining whether this DayPart is custom")
* @var int
*/
public $isCustom = 0;
/** @var Carbon $adjustedStart Adjusted start datetime */
public $adjustedStart;
/** @var Carbon Adjusted end datetime */
public $adjustedEnd;
private $timeHash;
/** @var ScheduleFactory */
private $scheduleFactory;
/** @var DisplayNotifyServiceInterface */
private $displayNotifyService;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
/**
* @param ScheduleFactory $scheduleFactory
* @param \Xibo\Service\DisplayNotifyServiceInterface $displayNotifyService
* @return $this
*/
public function setScheduleFactory($scheduleFactory, DisplayNotifyServiceInterface $displayNotifyService)
{
$this->scheduleFactory = $scheduleFactory;
$this->displayNotifyService = $displayNotifyService;
return $this;
}
/**
* Calculate time hash
* @return string
*/
private function calculateTimeHash()
{
$hash = $this->startTime . $this->endTime;
foreach ($this->exceptions as $exception) {
$hash .= $exception['day'] . $exception['start'] . $exception['end'];
}
return md5($hash);
}
public function isSystemDayPart(): bool
{
return ($this->isAlways || $this->isCustom);
}
/**
* @return int
*/
public function getId()
{
return $this->dayPartId;
}
/**
* @return int
*/
public function getOwnerId()
{
return $this->userId;
}
/**
* Sets the Owner
* @param int $ownerId
*/
public function setOwner($ownerId)
{
$this->userId = $ownerId;
}
/**
* @throws InvalidArgumentException
*/
public function validate()
{
$this->getLog()->debug('Validating daypart ' . $this->name);
if (!v::stringType()->notEmpty()->validate($this->name))
throw new InvalidArgumentException(__('Name cannot be empty'), 'name');
// Check the start/end times are in the correct format (H:i)
if ((strlen($this->startTime) != 8 && strlen($this->startTime) != 5) || (strlen($this->endTime) != 8 && strlen($this->endTime) != 5))
throw new InvalidArgumentException(__('Start/End time are empty or in an incorrect format'), 'start/end time');
foreach ($this->exceptions as $exception) {
if ((strlen($exception['start']) != 8 && strlen($exception['start']) != 5) || (strlen($exception['end']) != 8 && strlen($exception['end']) != 5))
throw new InvalidArgumentException(sprintf(__('Exception Start/End time for %s are empty or in an incorrect format'), $exception['day']), 'exception start/end time');
}
}
/**
* Load
* @return $this
*/
public function load()
{
$this->timeHash = $this->calculateTimeHash();
return $this;
}
/**
* @param \Carbon\Carbon $date
* @return void
*/
public function adjustForDate(Carbon $date)
{
// Matching exceptions?
// we use a lookup because the form control uses the below date abbreviations
$dayOfWeekLookup = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
foreach ($this->exceptions as $exception) {
if ($exception['day'] === $dayOfWeekLookup[$date->dayOfWeekIso]) {
$this->adjustedStart = $date->copy()->setTimeFromTimeString($exception['start']);
$this->adjustedEnd = $date->copy()->setTimeFromTimeString($exception['end']);
if ($this->adjustedStart >= $this->adjustedEnd) {
$this->adjustedEnd->addDay();
}
return;
}
}
// No matching exceptions.
$this->adjustedStart = $date->copy()->setTimeFromTimeString($this->startTime);
$this->adjustedEnd = $date->copy()->setTimeFromTimeString($this->endTime);
if ($this->adjustedStart >= $this->adjustedEnd) {
$this->adjustedEnd->addDay();
}
}
/**
* Save
* @param array $options
* @throws InvalidArgumentException
* @throws GeneralException
* @throws NotFoundException
*/
public function save($options = [])
{
$options = array_merge([
'validate' => true,
'recalculateHash' => true
], $options);
if ($options['validate']) {
$this->validate();
}
if ($this->dayPartId == 0) {
$this->add();
} else {
// Update
$this->update();
// When we change user on reassignAllTo, we do save dayPart,
// however it will not have required childObjectDependencies to run the below checks
// it is also not needed to run them when we just changed the owner.
if ($options['recalculateHash']) {
// Compare the time hash with a new time hash to see if we need to update associated schedules
if ($this->timeHash != $this->calculateTimeHash()) {
$this->handleEffectedSchedules();
} else {
$this->getLog()->debug('Daypart hash identical, no need to update schedules. ' . $this->timeHash . ' vs ' . $this->calculateTimeHash());
}
}
}
}
/**
* Delete
*/
public function delete()
{
if ($this->isSystemDayPart()) {
throw new InvalidArgumentException('Cannot delete system dayParts');
}
$this->getDispatcher()->dispatch(new DayPartDeleteEvent($this), DayPartDeleteEvent::$NAME);
// Delete all events using this daypart
$schedules = $this->scheduleFactory->getByDayPartId($this->dayPartId);
foreach ($schedules as $schedule) {
$schedule->delete();
}
// Delete the daypart
$this->getStore()->update('DELETE FROM `daypart` WHERE dayPartId = :dayPartId', ['dayPartId' => $this->dayPartId]);
}
/**
* Add
*/
private function add()
{
$this->dayPartId = $this->getStore()->insert('
INSERT INTO `daypart` (`name`, `description`, `isRetired`, `userId`, `startTime`, `endTime`, `exceptions`)
VALUES (:name, :description, :isRetired, :userId, :startTime, :endTime, :exceptions)
', [
'name' => $this->name,
'description' => $this->description,
'isRetired' => $this->isRetired,
'userId' => $this->userId,
'startTime' => $this->startTime,
'endTime' => $this->endTime,
'exceptions' => json_encode(is_array($this->exceptions) ? $this->exceptions : [])
]);
}
/**
* Update
*/
private function update()
{
$this->getStore()->update('
UPDATE `daypart`
SET `name` = :name,
`description` = :description,
`isRetired` = :isRetired,
`userId` = :userId,
`startTime` = :startTime,
`endTime` = :endTime,
`exceptions` = :exceptions
WHERE `daypart`.dayPartId = :dayPartId
', [
'dayPartId' => $this->dayPartId,
'name' => $this->name,
'description' => $this->description,
'isRetired' => $this->isRetired,
'userId' => $this->userId,
'startTime' => $this->startTime,
'endTime' => $this->endTime,
'exceptions' => json_encode(is_array($this->exceptions) ? $this->exceptions : [])
]);
}
/**
* Handles schedules effected by an update
* @throws NotFoundException
* @throws GeneralException
*/
private function handleEffectedSchedules()
{
$now = Carbon::now()->format('U');
// Get all schedules that use this dayPart and exist after the current time.
$schedules = $this->scheduleFactory->query(null, ['dayPartId' => $this->dayPartId, 'futureSchedulesFrom' => $now]);
$this->getLog()->debug('Daypart update effects ' . count($schedules) . ' schedules.');
foreach ($schedules as $schedule) {
/** @var Schedule $schedule */
$schedule
->setDisplayNotifyService($this->displayNotifyService)
->load();
// Is this schedule a recurring event?
if ($schedule->recurrenceType != '' && $schedule->fromDt < $now) {
$this->getLog()->debug('Schedule is for a recurring event which has already recurred');
// Split the scheduled event, adjusting only the recurring end date on the original event
$newSchedule = clone $schedule;
$schedule->recurrenceRange = $now;
$schedule->save();
// Adjusting the fromdt on the new event
$newSchedule->fromDt = Carbon::now()->addDay()->format('U');
$newSchedule->save();
} else {
$this->getLog()->debug('Schedule is for a single event');
// Update just this single event to have the new date/time
$schedule->save();
}
}
}
}

1576
lib/Entity/Display.php Normal file

File diff suppressed because it is too large Load Diff

225
lib/Entity/DisplayEvent.php Normal file
View File

@@ -0,0 +1,225 @@
<?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\Entity;
use Carbon\Carbon;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class DisplayEvent
* @package Xibo\Entity
*/
class DisplayEvent implements \JsonSerializable
{
use EntityTrait;
public $displayEventId;
public $displayId;
public $eventDate;
public $start;
public $end;
public $eventTypeId;
public $refId;
public $detail;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param EventDispatcherInterface $dispatcher
*/
public function __construct(
StorageServiceInterface $store,
LogServiceInterface $log,
EventDispatcherInterface $dispatcher
) {
$this->setCommonDependencies($store, $log, $dispatcher);
}
/**
* Save displayevent
* @return void
*/
public function save(): void
{
if ($this->displayEventId == null) {
$this->add();
} else {
$this->edit();
}
}
/**
* Add a new displayevent
* @return void
*/
private function add(): void
{
$this->displayEventId = $this->getStore()->insert('
INSERT INTO `displayevent` (eventDate, start, end, displayID, eventTypeId, refId, detail)
VALUES (:eventDate, :start, :end, :displayId, :eventTypeId, :refId, :detail)
', [
'eventDate' => Carbon::now()->format('U'),
'start' => $this->start,
'end' => $this->end,
'displayId' => $this->displayId,
'eventTypeId' => $this->eventTypeId,
'refId' => $this->refId,
'detail' => $this->detail,
]);
}
/**
* Edit displayevent
* @return void
*/
private function edit(): void
{
$this->getStore()->update('
UPDATE displayevent
SET end = :end,
displayId = :displayId,
eventTypeId = :eventTypeId,
refId = :refId,
detail = :detail
WHERE displayEventId = :displayEventId
', [
'displayEventId' => $this->displayEventId,
'end' => $this->end,
'displayId' => $this->displayId,
'eventTypeId' => $this->eventTypeId,
'refId' => $this->refId,
'detail' => $this->detail,
]);
}
/**
* Record end date for specified display and event type.
* @param int $displayId
* @param int|null $date
* @param int $eventTypeId
* @return void
*/
public function eventEnd(int $displayId, int $eventTypeId = 1, string $detail = null, ?int $date = null): void
{
$this->getLog()->debug(
sprintf(
'displayEvent : end display alert for eventType %s and displayId %d',
$this->getEventNameFromId($eventTypeId),
$displayId
)
);
$this->getStore()->update(
"UPDATE `displayevent` SET `end` = :toDt, `detail` = CONCAT_WS('. ', NULLIF(`detail`, ''), :detail)
WHERE displayId = :displayId
AND `end` IS NULL
AND eventTypeId = :eventTypeId",
[
'toDt' => $date ?? Carbon::now()->format('U'),
'displayId' => $displayId,
'eventTypeId' => $eventTypeId,
'detail' => $detail,
]
);
}
/**
* Record end date for specified display, event type and refId
* @param int $displayId
* @param int $eventTypeId
* @param int $refId
* @param int|null $date
* @return void
*/
public function eventEndByReference(int $displayId, int $eventTypeId, int $refId, string $detail = null, ?int $date = null): void
{
$this->getLog()->debug(
sprintf(
'displayEvent : end display alert for refId %d, displayId %d and eventType %s',
$refId,
$displayId,
$this->getEventNameFromId($eventTypeId),
)
);
// When updating the event end, concatenate the end message to the current message
$this->getStore()->update(
"UPDATE `displayevent` SET
`end` = :toDt,
`detail` = CONCAT_WS('. ', NULLIF(`detail`, ''), :detail)
WHERE displayId = :displayId
AND `end` IS NULL
AND eventTypeId = :eventTypeId
AND refId = :refId",
[
'toDt' => $date ?? Carbon::now()->format('U'),
'displayId' => $displayId,
'eventTypeId' => $eventTypeId,
'refId' => $refId,
'detail' => $detail,
]
);
}
/**
* Match event type string from log to eventTypeId in database.
* @param string $eventType
* @return int
*/
public function getEventIdFromString(string $eventType): int
{
return match ($eventType) {
'Display Up/down' => 1,
'App Start' => 2,
'Power Cycle' => 3,
'Network Cycle' => 4,
'TV Monitoring' => 5,
'Player Fault' => 6,
'Command' => 7,
default => 8
};
}
/**
* Match eventTypeId from database to string event name.
* @param int $eventTypeId
* @return string
*/
public function getEventNameFromId(int $eventTypeId): string
{
return match ($eventTypeId) {
1 => __('Display Up/down'),
2 => __('App Start'),
3 => __('Power Cycle'),
4 => __('Network Cycle'),
5 => __('TV Monitoring'),
6 => __('Player Fault'),
7 => __('Command'),
default => __('Other')
};
}
}

1115
lib/Entity/DisplayGroup.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,611 @@
<?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\Entity;
use Carbon\Carbon;
use Respect\Validation\Validator as v;
use Xibo\Factory\CommandFactory;
use Xibo\Factory\DisplayProfileFactory;
use Xibo\Service\ConfigServiceInterface;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
/**
* Class DisplayProfile
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class DisplayProfile implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The ID of this Display Profile")
* @var int
*/
public $displayProfileId;
/**
* @SWG\Property(description="The name of this Display Profile")
* @var string
*/
public $name;
/**
* @SWG\Property(description="The player type that this Display Profile is for")
* @var string
*/
public $type;
/**
* @SWG\Property(description="The configuration options for this Profile")
* @var string[]
*/
public $config;
/**
* @SWG\Property(description="A flag indicating if this profile should be used as the Default for the client type")
* @var int
*/
public $isDefault;
/**
* @SWG\Property(description="The userId of the User that owns this profile")
* @var int
*/
public $userId;
/**
* @SWG\Property(description="The default configuration options for this Profile")
* @var string[]
*/
public $configDefault;
/**
* Commands associated with this profile.
* @var Command[]
*/
public $commands = [];
public $isCustom;
/** @var string the client type */
private $clientType;
/** @var array Combined configuration */
private $configCombined = [];
/**
* @var ConfigServiceInterface
*/
private $configService;
/**
* @var CommandFactory
*/
private $commandFactory;
/**
* @var DisplayProfileFactory
*/
private $displayProfileFactory;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
* @param ConfigServiceInterface $config
* @param CommandFactory $commandFactory
* @param DisplayProfileFactory $displayProfileFactory
*/
public function __construct($store, $log, $dispatcher, $config, $commandFactory, $displayProfileFactory)
{
$this->setCommonDependencies($store, $log, $dispatcher);
$this->configService = $config;
$this->commandFactory = $commandFactory;
$this->displayProfileFactory = $displayProfileFactory;
}
public function __clone()
{
$this->displayProfileId = null;
$this->commands = [];
$this->isDefault = 0;
}
/**
* Get Id
* @return int
*/
public function getId()
{
return $this->displayProfileId;
}
/**
* @return int
*/
public function getOwnerId()
{
return $this->userId;
}
/**
* Get Setting
* @param $setting
* @param null $default
* @param bool $fromDefault
* @return mixed
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function getSetting($setting, $default = null, $fromDefault = false): mixed
{
$this->load();
$configs = ($fromDefault) ? $this->configDefault : $this->getProfileConfig();
foreach ($configs as $config) {
if ($config['name'] == $setting || $config['name'] == ucfirst($setting)) {
$default = $config['value'] ?? ($config['default'] ?? $default);
break;
}
}
return $default;
}
/**
* Set setting
* @param $setting
* @param $value
* @param boolean $ownConfig if provided will set the values on this object and not on the member config object
* @param array|null $config
* @return $this
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function setSetting($setting, $value, $ownConfig = true, &$config = null)
{
$this->load();
$found = false;
// Get the setting from default
// Which object do we operate on.
if ($ownConfig) {
$config = $this->config;
$default = $this->getSetting($setting, null, true);
} else {
// we are editing Display object, as such we want the $default to come from display profile assigned to our display
$default = $this->getSetting($setting, null, false);
}
// Check to see if we have this setting already
for ($i = 0; $i < count($config); $i++) {
if ($config[$i]['name'] == $setting || $config[$i]['name'] == ucfirst($setting)) {
// We found the setting - is the value different to the default?
if ($value !== $default) {
$config[$i]['value'] = $value;
$config[$i]['name'] = lcfirst($setting);
} else {
// the value is the same as the default - unset it
$this->getLog()->debug('Setting [' . $setting . '] identical to the default, unsetting.');
unset($config[$i]);
$config = array_values($config);
}
$found = true;
break;
}
}
if (!$found && $value !== $default) {
$this->getLog()->debug('Setting [' . $setting . '] not yet in the profile config, and different to the default. ' . var_export($value, true) . ' --- ' . var_export($default, true));
// The config option isn't in our array yet, so add it
$config[] = [
'name' => lcfirst($setting),
'value' => $value
];
}
if ($ownConfig) {
// Reset our object
$this->config = $config;
// Reload our combined array
$this->configCombined = $this->mergeConfigs($this->configDefault, $this->config);
}
return $this;
}
/**
* Merge two configs
* @param $default
* @param $override
* @return array
*/
private function mergeConfigs($default, $override): array
{
foreach ($default as &$defaultItem) {
for ($i = 0; $i < count($override); $i++) {
if ($defaultItem['name'] == $override[$i]['name']) {
// merge
$defaultItem = array_merge($defaultItem, $override[$i]);
break;
}
}
}
// Merge the remainder
return $default;
}
/**
* @param $clientType
*/
public function setClientType($clientType)
{
$this->clientType = $clientType;
}
/**
* @return bool
*/
public function isCustom(): bool
{
return $this->isCustom;
}
/**
* Get the client type
* @return string
*/
public function getClientType()
{
return (empty($this->clientType)) ? $this->type : $this->clientType;
}
/**
* Assign Command
* @param Command $command
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function assignCommand($command)
{
$this->load([]);
$assigned = false;
foreach ($this->commands as $alreadyAssigned) {
/* @var Command $alreadyAssigned */
if ($alreadyAssigned->getId() == $command->getId()) {
$alreadyAssigned->commandString = $command->commandString;
$alreadyAssigned->validationString = $command->validationString;
$alreadyAssigned->createAlertOn = $command->createAlertOn;
$assigned = true;
break;
}
}
if (!$assigned) {
$this->commands[] = $command;
}
}
/**
* Unassign Command
* @param Command $command
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function unassignCommand($command)
{
$this->load([]);
$this->commands = array_udiff($this->commands, [$command], function ($a, $b) {
/**
* @var Command $a
* @var Command $b
*/
return $a->getId() - $b->getId();
});
}
/**
* Sets the Owner
* @param int $ownerId
*/
public function setOwner($ownerId)
{
$this->userId = $ownerId;
}
/**
* Load
* @param array $options
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function load($options = []): void
{
$this->getLog()->debug('load: Loading display profile, type: ' . $this->clientType
. ' id: ' . $this->displayProfileId);
$options = array_merge([
'loadConfig' => true,
'loadCommands' => true
], $options);
if ($this->loaded) {
return;
}
// Load in our default config from this class, based on the client type we are
$this->configDefault = $this->displayProfileFactory->loadForType($this->getClientType());
// Get our combined config
$this->configCombined = [];
if ($options['loadConfig']) {
if (!is_array($this->config) && !empty($this->config)) {
$this->config = json_decode($this->config, true);
}
// handle cases when config is empty
if (empty($this->config)) {
$this->config = [];
}
$this->getLog()->debug('Config loaded: ' . json_encode($this->config, JSON_PRETTY_PRINT));
// Populate our combined config accordingly
$this->configCombined = $this->mergeConfigs($this->configDefault, $this->config);
}
$this->getLog()->debug('Config Combined is: ' . json_encode($this->configCombined, JSON_PRETTY_PRINT));
// Load any commands
if ($options['loadCommands']) {
$this->commands = $this->commandFactory->getByDisplayProfileId($this->displayProfileId, $this->type);
}
// We are loaded
$this->loaded = true;
}
/**
* Validate
* @throws InvalidArgumentException
*/
public function validate()
{
if (!v::stringType()->notEmpty()->validate($this->name))
throw new InvalidArgumentException(__('Missing name'), 'name');
if (!v::stringType()->notEmpty()->validate($this->type))
throw new InvalidArgumentException(__('Missing type'), 'type');
for ($j = 0; $j < count($this->config); $j++) {
if ($this->config[$j]['name'] == 'MaxConcurrentDownloads' && $this->config[$j]['value'] <= 0 && $this->type = 'windows') {
throw new InvalidArgumentException(__('Concurrent downloads must be a positive number'), 'MaxConcurrentDownloads');
}
if ($this->config[$j]['name'] == 'maxRegionCount' && !v::intType()->min(0)->validate($this->config[$j]['value'])) {
throw new InvalidArgumentException(__('Maximum Region Count must be a positive number'), 'maxRegionCount');
}
}
// Check there is only 1 default (including this one)
$sql = '
SELECT COUNT(*) AS cnt
FROM `displayprofile`
WHERE `type` = :type
AND isdefault = 1
';
$params = ['type' => $this->type];
if ($this->displayProfileId != 0) {
$sql .= ' AND displayprofileid <> :displayProfileId ';
$params['displayProfileId'] = $this->displayProfileId;
}
$count = $this->getStore()->select($sql, $params);
if ($count[0]['cnt'] + $this->isDefault > 1) {
throw new InvalidArgumentException(__('Only 1 default per display type is allowed.'), 'isDefault');
}
}
/**
* Save
* @param bool $validate
* @throws InvalidArgumentException
*/
public function save($validate = true)
{
if ($validate)
$this->validate();
if ($this->displayProfileId == null || $this->displayProfileId == 0)
$this->add();
else
$this->edit();
$this->manageAssignments();
}
/**
* Delete
* @throws InvalidArgumentException
*/
public function delete()
{
$this->commands = [];
$this->manageAssignments();
if ($this->getStore()->exists('SELECT displayId FROM display WHERE displayProfileId = :displayProfileId', ['displayProfileId' => $this->displayProfileId]) ) {
throw new InvalidArgumentException(__('This Display Profile is currently assigned to one or more Displays'), 'displayProfileId');
}
if ($this->isDefault === 1) {
throw new InvalidArgumentException(__('Cannot delete default Display Profile.'), 'isDefault');
}
$this->getStore()->update('DELETE FROM `displayprofile` WHERE displayprofileid = :displayProfileId', ['displayProfileId' => $this->displayProfileId]);
}
/**
* Manage Assignments
*/
private function manageAssignments()
{
$this->getLog()->debug('Managing Assignment for Display Profile: %d. %d commands.', $this->displayProfileId, count($this->commands));
// Link
foreach ($this->commands as $command) {
/* @var Command $command */
$this->getStore()->update('
INSERT INTO `lkcommanddisplayprofile` (
`commandId`,
`displayProfileId`,
`commandString`,
`validationString`,
`createAlertOn`
)
VALUES (
:commandId,
:displayProfileId,
:commandString,
:validationString,
:createAlertOn
)
ON DUPLICATE KEY UPDATE
commandString = :commandString2,
validationString = :validationString2,
createAlertOn = :createAlertOn2
', [
'commandId' => $command->commandId,
'displayProfileId' => $this->displayProfileId,
'commandString' => $command->commandString,
'validationString' => $command->validationString,
'createAlertOn' => $command->createAlertOn,
'commandString2' => $command->commandString,
'validationString2' => $command->validationString,
'createAlertOn2' => $command->createAlertOn
]);
}
// Unlink
$params = ['displayProfileId' => $this->displayProfileId];
$sql = 'DELETE FROM `lkcommanddisplayprofile`
WHERE `displayProfileId` = :displayProfileId AND `commandId` NOT IN (0';
$i = 0;
foreach ($this->commands as $command) {
/* @var Command $command */
$i++;
$sql .= ',:commandId' . $i;
$params['commandId' . $i] = $command->commandId;
}
$sql .= ')';
$this->getStore()->update($sql, $params);
}
private function add()
{
$this->displayProfileId = $this->getStore()->insert('
INSERT INTO `displayprofile` (`name`, type, config, isdefault, userid, isCustom)
VALUES (:name, :type, :config, :isDefault, :userId, :isCustom)
', [
'name' => $this->name,
'type' => $this->type,
'config' => ($this->config == '') ? '[]' : json_encode($this->config),
'isDefault' => $this->isDefault,
'userId' => $this->userId,
'isCustom' => $this->isCustom ?? 0
]);
}
private function edit()
{
$this->getStore()->update('
UPDATE `displayprofile`
SET `name` = :name, type = :type, config = :config, isdefault = :isDefault, isCustom = :isCustom
WHERE displayprofileid = :displayProfileId', [
'name' => $this->name,
'type' => $this->type,
'config' => ($this->config == '') ? '[]' : json_encode($this->config),
'isDefault' => $this->isDefault,
'isCustom' => $this->isCustom ?? 0,
'displayProfileId' => $this->displayProfileId
]);
}
/**
* @return array
*/
public function getProfileConfig(): array
{
return $this->configCombined;
}
public function getCustomEditTemplate()
{
if ($this->isCustom()) {
return $this->displayProfileFactory->getCustomEditTemplate($this->getClientType());
} else {
$this->getLog()->error(
'Attempting to get Custom Edit template for Display Profile ' .
$this->getClientType() . ' that is not custom'
);
return null;
}
}
public function handleCustomFields($sanitizedParams, $config = null, $display = null)
{
return $this->displayProfileFactory->handleCustomFields($this, $sanitizedParams, $config, $display);
}
/**
* Does this display profile has elevated log level?
* @return bool
* @throws NotFoundException
*/
public function isElevatedLogging(): bool
{
$elevatedUntil = $this->getSetting('elevateLogsUntil', 0);
$this->getLog()->debug(sprintf(
'Testing whether this display profile has elevated log level. %d vs %d.',
$elevatedUntil,
Carbon::now()->format('U')
));
return (!empty($elevatedUntil) && $elevatedUntil >= Carbon::now()->format('U'));
}
}

View File

@@ -0,0 +1,61 @@
<?php
/*
* Copyright (c) 2022 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\Entity;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class DisplayType
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class DisplayType implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The ID for this DisplayType")
* @var int
*/
public $displayTypeId;
/**
* @SWG\Property(description="The Name for this DisplayType")
* @var string
*/
public $displayType;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
}

437
lib/Entity/EntityTrait.php Normal file
View File

@@ -0,0 +1,437 @@
<?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\Entity;
use Carbon\Carbon;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Xibo\Helper\DateFormatHelper;
use Xibo\Helper\ObjectVars;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class EntityTrait
* used by all entities
* @package Xibo\Entity
*/
trait EntityTrait
{
private $hash = null;
private $loaded = false;
private $permissionsClass = null;
private $canChangeOwner = true;
public $buttons = [];
private $jsonExclude = ['buttons', 'jsonExclude', 'originalValues', 'jsonInclude', 'datesToFormat'];
/** @var array Original values hydrated */
protected $originalValues = [];
/** @var array Unmatched properties */
private $unmatchedProperties = [];
/**
* @var StorageServiceInterface
*/
private $store;
/**
* @var LogServiceInterface
*/
private $log;
/** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface */
private $dispatcher;
/**
* Set common dependencies.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param EventDispatcherInterface $dispatcher
* @return $this
*/
protected function setCommonDependencies($store, $log, $dispatcher)
{
$this->store = $store;
$this->log = $log;
$this->dispatcher = $dispatcher;
return $this;
}
/**
* Get Store
* @return StorageServiceInterface
*/
protected function getStore()
{
return $this->store;
}
/**
* Get Log
* @return LogServiceInterface
*/
protected function getLog()
{
return $this->log;
}
/**
* @return \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
public function getDispatcher(): EventDispatcherInterface
{
if ($this->dispatcher === null) {
$this->getLog()->error('getDispatcher: [entity] No dispatcher found, returning an empty one');
$this->dispatcher = new EventDispatcher();
}
return $this->dispatcher;
}
/**
* Hydrate an entity with properties
*
* @param array $properties
* @param array $options
*
* @return self
*/
public function hydrate(array $properties, $options = [])
{
$intProperties = (array_key_exists('intProperties', $options)) ? $options['intProperties'] : [];
$doubleProperties = (array_key_exists('doubleProperties', $options)) ? $options['doubleProperties'] : [];
$stringProperties = (array_key_exists('stringProperties', $options)) ? $options['stringProperties'] : [];
$htmlStringProperties = (array_key_exists('htmlStringProperties', $options))
? $options['htmlStringProperties'] : [];
foreach ($properties as $prop => $val) {
// Parse the property
if ((stripos(strrev($prop), 'dI') === 0 || in_array($prop, $intProperties))
&& !in_array($prop, $stringProperties)
) {
$val = intval($val);
} else if (in_array($prop, $doubleProperties)) {
$val = doubleval($val);
} else if (in_array($prop, $stringProperties) && $val !== null) {
$val = htmlspecialchars($val);
} else if (in_array($prop, $htmlStringProperties)) {
$val = htmlentities($val);
}
if (property_exists($this, $prop)) {
$this->{$prop} = $val;
$this->originalValues[$prop] = $val;
} else {
$this->unmatchedProperties[$prop] = $val;
}
}
return $this;
}
/**
* Reset originals to current values
*/
public function setOriginals()
{
foreach ($this->jsonSerialize() as $key => $value) {
$this->originalValues[$key] = $value;
}
}
/**
* Get the original value of a property
* @param string $property
* @return null|mixed
*/
public function getOriginalValue($property)
{
return (isset($this->originalValues[$property])) ? $this->originalValues[$property] : null;
}
/**
* @param string $property
* @param mixed $value
* @return $this
*/
public function setOriginalValue(string $property, $value)
{
$this->originalValues[$property] = $value;
return $this;
}
/**
* Has the provided property been changed from its original value
* @param string $property
* @return bool
*/
public function hasPropertyChanged($property)
{
if (!property_exists($this, $property))
return true;
return $this->getOriginalValue($property) != $this->{$property};
}
/**
* @param $property
* @return bool
*/
public function propertyOriginallyExisted($property)
{
return array_key_exists($property, $this->originalValues);
}
/**
* Get all changed properties for this entity
* @param bool $jsonEncodeArrays
* @return array
*/
public function getChangedProperties($jsonEncodeArrays = false)
{
$changedProperties = [];
foreach ($this->jsonSerialize() as $key => $value) {
if (!is_array($value) && !is_object($value) && $this->propertyOriginallyExisted($key) && $this->hasPropertyChanged($key)) {
if (isset($this->datesToFormat) && in_array($key, $this->datesToFormat)) {
$original = empty($this->getOriginalValue($key))
? $this->getOriginalValue($key)
: Carbon::createFromTimestamp($this->getOriginalValue($key))->format(DateFormatHelper::getSystemFormat());
$new = empty($value)
? $value
: Carbon::createFromTimestamp($value)->format(DateFormatHelper::getSystemFormat());
$changedProperties[$key] = $original . ' > ' . $new;
} else {
$changedProperties[$key] = $this->getOriginalValue($key) . ' > ' . $value;
}
}
if (is_array($value) && $jsonEncodeArrays && $this->propertyOriginallyExisted($key) && $this->hasPropertyChanged($key)) {
$changedProperties[$key] = json_encode($this->getOriginalValue($key)) . ' > ' . json_encode($value);
}
}
return $changedProperties;
}
/**
* Get an unmatched property
* @param string $property
* @param mixed $default The default value to return if the unmatched property doesn't exist.
* @return null|mixed
*/
public function getUnmatchedProperty(string $property, mixed $default = null): mixed
{
return $this->unmatchedProperties[$property] ?? $default;
}
/**
* @param string $property
* @param mixed $value
* @return $this
*/
public function setUnmatchedProperty(string $property, mixed $value)
{
$this->unmatchedProperties[$property] = $value;
return $this;
}
/**
* Json Serialize
*/
public function jsonSerialize(): array
{
$properties = ObjectVars::getObjectVars($this);
$json = [];
foreach ($properties as $key => $value) {
if (!in_array($key, $this->jsonExclude)) {
$json[$key] = $value;
}
}
// Output unmatched properties too?
if (!in_array('unmatchedProperties', $this->jsonExclude)) {
foreach ($this->unmatchedProperties as $key => $value) {
if (!in_array($key, $this->jsonExclude)) {
$json[$key] = $value;
}
}
}
return $json;
}
public function jsonForAudit(): array
{
$properties = ObjectVars::getObjectVars($this);
$json = [];
foreach ($properties as $key => $value) {
if (in_array($key, $this->jsonInclude)) {
$json[$key] = $value;
}
}
return $json;
}
/**
* To Array
* @param bool $jsonEncodeArrays
* @return array
*/
public function toArray($jsonEncodeArrays = false)
{
$objectAsJson = $this->jsonSerialize();
foreach ($objectAsJson as $key => $value) {
if (isset($this->datesToFormat) && in_array($key, $this->datesToFormat)) {
$objectAsJson[$key] = Carbon::createFromTimestamp($value)->format(DateFormatHelper::getSystemFormat());
}
if ($jsonEncodeArrays) {
if (is_array($value)) {
$objectAsJson[$key] = json_encode($value);
}
}
}
return $objectAsJson;
}
/**
* Add a property to the excluded list
* @param string $property
*/
public function excludeProperty($property)
{
$this->jsonExclude[] = $property;
}
/**
* Remove a property from the excluded list
* @param string $property
*/
public function includeProperty($property)
{
$this->jsonExclude = array_diff($this->jsonExclude, [$property]);
}
/**
* Get the Permissions Class
* @return string
*/
public function permissionsClass()
{
return ($this->permissionsClass == null) ? get_class($this) : $this->permissionsClass;
}
/**
* Set the Permissions Class
* @param string $class
*/
protected function setPermissionsClass($class)
{
$this->permissionsClass = $class;
}
/**
* Can the owner change?
* @return bool
*/
public function canChangeOwner()
{
return $this->canChangeOwner && method_exists($this, 'setOwner');
}
/**
* @param bool $bool Can the owner be changed?
*/
protected function setCanChangeOwner($bool)
{
$this->canChangeOwner = $bool;
}
/**
* @param $entityId
* @param $message
* @param null $changedProperties
* @param bool $jsonEncodeArrays
* @throws \Xibo\Support\Exception\NotFoundException
*/
protected function audit($entityId, $message, $changedProperties = null, $jsonEncodeArrays = false)
{
$class = substr(get_class($this), strrpos(get_class($this), '\\') + 1);
if ($changedProperties === null) {
// No properties provided, so we should work them out
// If we have originals, then get changed, otherwise get the current object state
$changedProperties = (count($this->originalValues) <= 0)
? $this->toArray($jsonEncodeArrays)
: $this->getChangedProperties($jsonEncodeArrays);
} else if ($changedProperties !== false && count($changedProperties) <= 0) {
// Only audit if properties have been provided
return;
}
$this->getLog()->audit($class, $entityId, $message, $changedProperties);
}
/**
* Compare two arrays, both keys and values.
*
* @param $array1
* @param $array2
* @param bool $compareValues
* @return array
*/
public function compareMultidimensionalArrays($array1, $array2, $compareValues = true)
{
$result = [];
// go through arrays, compare keys and values
// the compareValues flag is there for tag unlink - we're interested only in array keys
foreach ($array1 as $key => $value) {
if (!is_array($array2) || !array_key_exists($key, $array2)) {
$result[$key] = $value;
continue;
}
if ($value != $array2[$key] && $compareValues) {
$result[$key] = $value;
}
}
return $result;
}
public function updateFolders($table)
{
$this->getStore()->update('UPDATE `'. $table .'` SET permissionsFolderId = :permissionsFolderId, folderId = :folderId WHERE folderId = :oldFolderId', [
'permissionsFolderId' => $this->permissionsFolderId,
'folderId' => $this->folderId,
'oldFolderId' => $this->getOriginalValue('folderId')
]);
}
}

471
lib/Entity/Folder.php Normal file
View File

@@ -0,0 +1,471 @@
<?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\Entity;
use Respect\Validation\Validator as v;
use Xibo\Factory\FolderFactory;
use Xibo\Factory\PermissionFactory;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\InvalidArgumentException;
/**
* Class Folder
* @package Xibo\Entity
* @SWG\Definition()
*/
class Folder implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The ID of this Folder")
* @var int
*/
public $id;
/**
* @SWG\Property(description="The type of folder (home or root)")
* @var string
*/
public $type;
/**
* @SWG\Property(description="The name of this Folder")
* @var string
*/
public $text;
/**
* @SWG\Property(description="The folderId of the parent of this Folder")
* @var int
*/
public $parentId;
/**
* @SWG\Property(description="Flag indicating whether this is root Folder")
* @var int
*/
public $isRoot;
/**
* @SWG\Property(description="An array of children folderIds")
* @var string
*/
public $children;
public $permissionsFolderId;
/** @var FolderFactory */
private $folderFactory;
/**
* @var PermissionFactory
*/
private $permissionFactory;
private $permissions = [];
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
* @param FolderFactory $folderFactory
* @param PermissionFactory $permissionFactory
*/
public function __construct($store, $log, $dispatcher, $folderFactory, $permissionFactory)
{
$this->setCommonDependencies($store, $log, $dispatcher);
$this->setPermissionsClass('Xibo\Entity\Folder');
$this->folderFactory = $folderFactory;
$this->permissionFactory = $permissionFactory;
}
public function getId()
{
return $this->id;
}
public function getPermissionFolderId()
{
return $this->permissionsFolderId;
}
/**
* When you set ACL on a folder the permissionsFolderId on the folder record is set to null
* any objects inside this folder get the permissionsFolderId set to this folderId
* @return int
*/
public function getPermissionFolderIdOrThis(): int
{
return $this->permissionsFolderId == null ? $this->id : $this->permissionsFolderId;
}
/**
* Get Owner Id
* @return int
*/
public function getOwnerId()
{
return -1;
}
public function getParentId()
{
return $this->parentId;
}
public function isRoot(): bool
{
return $this->isRoot === 1;
}
public function getChildren()
{
return explode(',', $this->children);
}
/**
* @throws InvalidArgumentException
*/
public function validate()
{
if (!v::stringType()->notEmpty()->length(1, 254)->validate($this->text)) {
throw new InvalidArgumentException(__('Folder needs to have a name, between 1 and 254 characters.'), 'folderName');
}
if (empty($this->parentId)) {
throw new InvalidArgumentException(__('Folder needs a specified parent Folder id'), 'parentId');
}
}
public function load()
{
if ($this->loaded || $this->id == null) {
return;
}
// Permissions
$this->permissions = $this->permissionFactory->getByObjectId(get_class($this), $this->id);
$this->loaded = true;
}
/**
* @param bool $validate
* @throws InvalidArgumentException
*/
public function save($validate = true)
{
if ($validate) {
$this->validate();
}
if ($this->id == null || $this->id == 0) {
$this->add();
} else {
$this->edit();
}
}
public function delete()
{
foreach ($this->permissions as $permission) {
/* @var Permission $permission */
$permission->delete();
}
$this->manageChildren('delete');
$this->getStore()->update('DELETE FROM `folder` WHERE folderId = :folderId', [
'folderId' => $this->id
]);
}
private function add()
{
$parent = $this->folderFactory->getById($this->parentId);
$this->id = $this->getStore()->insert('INSERT INTO `folder` (folderName, parentId, isRoot, permissionsFolderId) VALUES (:folderName, :parentId, :isRoot, :permissionsFolderId)',
[
'folderName' => $this->text,
'parentId' => $this->parentId,
'isRoot' => 0,
'permissionsFolderId' => ($parent->permissionsFolderId == null) ? $this->parentId : $parent->permissionsFolderId
]);
$this->manageChildren('add');
}
private function edit()
{
$this->getStore()->update('UPDATE `folder` SET folderName = :folderName, parentId = :parentId WHERE folderId = :folderId',
[
'folderId' => $this->id,
'folderName' => $this->text,
'parentId' => $this->parentId
]);
}
/**
* Manages folder tree structure
*
* If mode delete is passed then it will remove selected folder and all its children down the tree
* Then update children property on parent accordingly
*
* On add mode we just add this folder id to parent children property
*
* @param $mode
* @throws \Xibo\Support\Exception\NotFoundException
*/
private function manageChildren($mode)
{
$parent = $this->folderFactory->getById($this->parentId);
$parentChildren = array_filter(explode(',', $parent->children ?? ''));
$children = array_filter(explode(',', $this->children ?? ''));
if ($mode === 'delete') {
// remove this folder from children of the parent
foreach ($parentChildren as $index => $child) {
if ((int)$child === (int)$this->id) {
unset($parentChildren[$index]);
}
}
// remove this folder children
foreach ($children as $child) {
$childObject = $this->folderFactory->getById($child);
$childObject->manageChildren('delete');
$this->getStore()->update('DELETE FROM `folder` WHERE folderId = :folderId', [
'folderId' => $childObject->id
]);
}
} else {
$parentChildren[] = $this->id;
}
$updatedChildren = implode(',', array_filter($parentChildren));
$this->getStore()->update('UPDATE `folder` SET children = :children WHERE folderId = :folderId', [
'folderId' => $this->parentId,
'children' => $updatedChildren
]);
}
/**
* Manages folder permissions
*
* When permissions are added on folder, this starts new ACL from that folder and is cascaded down to all folders under this folder
* permissionsFolderId is also updated on all relevant objects that are in this folder or under this folder in folder tree structure
*
* When permissions are removed from a folder, this sets the permissionsFolderId to parent folderId (or parent permissionsFolderId)
* same is cascaded down the folder tree and all relevant objects
*
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function managePermissions()
{
// this function happens after permissions are inserted into permission table
// with that we can look up if there are any permissions for edited folder and act accordingly.
$permissionExists = $this->getStore()->exists('SELECT permissionId FROM permission INNER JOIN permissionentity ON permission.entityId = permissionentity.entityId WHERE objectId = :folderId AND permissionentity.entity = :folderEntity', [
'folderId' => $this->id,
'folderEntity' => 'Xibo\Entity\Folder'
]);
if ($permissionExists) {
// if we added/edited permission on this folder, then new ACL starts here, cascade this folderId as permissionFolderId to all children
$this->getStore()->update('UPDATE `folder` SET permissionsFolderId = NULL WHERE folderId = :folderId', [
'folderId' => $this->id
]);
$permissionFolderId = $this->id;
} else {
// if there are no permissions for this folder, basically reset the permissions on this folder and its children
if ($this->id === 1 && $this->isRoot()) {
$permissionFolderId = 1;
} else {
$parent = $this->folderFactory->getById($this->parentId);
$permissionFolderId = ($parent->permissionsFolderId == null) ? $parent->id : $parent->permissionsFolderId;
}
$this->getStore()->update('UPDATE `folder` SET permissionsFolderId = :permissionsFolderId WHERE folderId = :folderId', [
'folderId' => $this->id,
'permissionsFolderId' => $permissionFolderId
]);
}
$this->updateChildObjects($permissionFolderId, $this->id);
$this->manageChildPermissions($permissionFolderId);
}
/**
* Helper recursive function to make sure all folders under the edited parent folder have correct permissionsFolderId set on them
* along with all relevant objects in those folders.
*
*
* @param $permissionFolderId
* @throws \Xibo\Support\Exception\NotFoundException
*/
private function manageChildPermissions($permissionFolderId)
{
$children = array_filter(explode(',', $this->children ?? ''));
foreach ($children as $child) {
$this->updateChildObjects($permissionFolderId, $child);
$childObject = $this->folderFactory->getById($child);
$childObject->manageChildPermissions($permissionFolderId);
}
}
private function updateChildObjects($permissionFolderId, $folderId)
{
$this->getStore()->update('UPDATE `folder` SET permissionsFolderId = :permissionsFolderId WHERE parentId = :folderId', [
'permissionsFolderId' => $permissionFolderId,
'folderId' => $folderId
]);
$this->getStore()->update('UPDATE `media` SET permissionsFolderId = :permissionsFolderId WHERE folderId = :folderId', [
'permissionsFolderId' => $permissionFolderId,
'folderId' => $folderId
]);
$this->getStore()->update('UPDATE `campaign` SET permissionsFolderId = :permissionsFolderId WHERE folderId = :folderId', [
'permissionsFolderId' => $permissionFolderId,
'folderId' => $folderId
]);
$this->getStore()->update('UPDATE `displaygroup` SET permissionsFolderId = :permissionsFolderId WHERE folderId = :folderId', [
'permissionsFolderId' => $permissionFolderId,
'folderId' => $folderId
]);
$this->getStore()->update('UPDATE `dataset` SET permissionsFolderId = :permissionsFolderId WHERE folderId = :folderId', [
'permissionsFolderId' => $permissionFolderId,
'folderId' => $folderId
]);
$this->getStore()->update('UPDATE `playlist` SET permissionsFolderId = :permissionsFolderId WHERE folderId = :folderId', [
'permissionsFolderId' => $permissionFolderId,
'folderId' => $folderId
]);
$this->getStore()->update('UPDATE `menu_board` SET permissionsFolderId = :permissionsFolderId WHERE folderId = :folderId', [
'permissionsFolderId' => $permissionFolderId,
'folderId' => $folderId
]);
$this->getStore()->update('UPDATE `syncgroup` SET permissionsFolderId = :permissionsFolderId WHERE folderId = :folderId', [
'permissionsFolderId' => $permissionFolderId,
'folderId' => $folderId
]);
}
/**
* Update old parent, new parent records with adjusted children
* Update current folders records with new parent, permissionsFolderId
* Recursively go through the current folder's children folder and objects and adjust permissionsFolderId if needed.
* @param int $oldParentFolder
* @param int $newParentFolder
*/
public function updateFoldersAfterMove(int $oldParentFolderId, int $newParentFolderId)
{
$oldParentFolder = $this->folderFactory->getById($oldParentFolderId, 0);
$newParentFolder = $this->folderFactory->getById($newParentFolderId, 0);
// new parent folder that adopted this folder, adjust children
$newParentChildren = array_filter(explode(',', $newParentFolder->children));
$newParentChildren[] = $this->id;
$newParentUpdatedChildren = implode(',', array_filter($newParentChildren));
$this->getStore()->update('UPDATE `folder` SET children = :children WHERE folderId = :folderId', [
'folderId' => $newParentFolder->id,
'children' => $newParentUpdatedChildren
]);
// old parent that gave this folder for adoption, adjust children
$oldParentChildren = array_filter(explode(',', $oldParentFolder->children));
foreach ($oldParentChildren as $index => $child) {
if ((int)$child === $this->id) {
unset($oldParentChildren[$index]);
}
}
$oldParentUpdatedChildren = implode(',', array_filter($oldParentChildren));
$this->getStore()->update('UPDATE `folder` SET children = :children WHERE folderId = :folderId', [
'folderId' => $oldParentFolder->id,
'children' => $oldParentUpdatedChildren
]);
// if we had permissions set on this folder, then permissionsFolderId stays as it was
if ($this->getPermissionFolderId() !== null) {
$this->permissionsFolderId = $newParentFolder->getPermissionFolderIdOrThis();
$this->manageChildPermissions($this->permissionsFolderId);
}
$this->getStore()->update('UPDATE `folder` SET parentId = :parentId, permissionsFolderId = :permissionsFolderId WHERE folderId = :folderId', [
'parentId' => $newParentFolder->id,
'permissionsFolderId' => $this->permissionsFolderId,
'folderId' => $this->id
]);
}
/**
* We do not allow moving a parent Folder inside of one of its sub-folders
* If that's what was requested, throw an error
* @param int $newParentFolderId
* @return bool
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function isTheSameBranch(int $newParentFolderId): bool
{
$children = array_filter(explode(',', $this->children ?? ''));
$found = false;
foreach ($children as $child) {
if ((int)$child === $newParentFolderId) {
$found = true;
break;
}
$childObject = $this->folderFactory->getById($child);
$childObject->isTheSameBranch($newParentFolderId);
}
return $found;
}
/**
* Check if this folder is used as Home Folder for any existing Users
* @return bool
*/
public function isHome(): bool
{
$userIds = $this->getStore()->select('SELECT userId FROM `user` WHERE `user`.homeFolderId = :folderId', [
'folderId' => $this->id
]);
return count($userIds) > 0;
}
}

208
lib/Entity/Font.php Normal file
View File

@@ -0,0 +1,208 @@
<?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\Entity;
use Carbon\Carbon;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Xibo\Helper\DateFormatHelper;
use Xibo\Service\ConfigServiceInterface;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Factory\FontFactory;
use Xibo\Support\Exception\DuplicateEntityException;
/**
* Class Font
* @package Xibo\Entity
* @SWG\Definition()
*/
class Font
{
use EntityTrait;
/**
* @SWG\Property(description="The Font ID")
* @var int
*/
public $id;
/**
* @SWG\Property(description="The Font created date")
* @var string
*/
public $createdAt;
/**
* @SWG\Property(description="The Font modified date")
* @var string
*/
public $modifiedAt;
/**
* @SWG\Property(description="The name of the user that modified this font last")
* @var string
*/
public $modifiedBy;
/**
* @SWG\Property(description="The Font name")
* @var string
*/
public $name;
/**
* @SWG\Property(description="The Font file name")
* @var string
*/
public $fileName;
/**
* @SWG\Property(description="The Font family name")
* @var string
*/
public $familyName;
/**
* @SWG\Property(description="The Font file size in bytes")
* @var int
*/
public $size;
/**
* @SWG\Property(description="A MD5 checksum of the stored font file")
* @var string
*/
public $md5;
/** @var ConfigServiceInterface */
private $config;
/** @var FontFactory */
private $fontFactory;
/**
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher, $config, $fontFactory)
{
$this->setCommonDependencies($store, $log, $dispatcher);
$this->config = $config;
$this->fontFactory = $fontFactory;
}
public function getFilePath()
{
return $this->config->getSetting('LIBRARY_LOCATION') . 'fonts/' . $this->fileName;
}
/**
* @throws DuplicateEntityException
*/
public function save($options = [])
{
$options = array_merge($options, ['validate' => true]);
if ($options['validate']) {
$this->validate();
}
if ($this->id === null || $this->id === 0) {
$this->add();
$this->audit($this->id, 'Added font', [
'mediaId' => $this->id,
'name' => $this->name,
'fileName' => $this->fileName,
]);
} else {
$this->edit();
}
}
private function add()
{
$this->id = $this->getStore()->insert('
INSERT INTO `fonts` (`createdAt`, `modifiedAt`, modifiedBy, name, fileName, familyName, size, md5)
VALUES (:createdAt, :modifiedAt, :modifiedBy, :name, :fileName, :familyName, :size, :md5)
', [
'createdAt' => Carbon::now()->format(DateFormatHelper::getSystemFormat()),
'modifiedAt' => Carbon::now()->format(DateFormatHelper::getSystemFormat()),
'modifiedBy' => $this->modifiedBy,
'name' => $this->name,
'fileName' => $this->fileName,
'familyName' => $this->familyName,
'size' => $this->size,
'md5' => $this->md5
]);
}
private function edit()
{
$this->getStore()->update('UPDATE `fonts` SET modifiedAt = :modifiedAt, modifiedBy = :modifiedBy, name = :name WHERE id = :id', [
'modifiedAt' => Carbon::now()->format(DateFormatHelper::getSystemFormat()),
'modifiedBy' => $this->modifiedBy,
'name' => $this->name,
'id' => $this->id
]);
}
public function delete()
{
// delete record
$this->getStore()->update('DELETE FROM `fonts` WHERE id = :id', [
'id' => $this->id
]);
// delete file
$libraryLocation = $this->config->getSetting('LIBRARY_LOCATION');
if (file_exists($libraryLocation . 'fonts/' . $this->fileName)) {
unlink($libraryLocation . 'fonts/' . $this->fileName);
}
}
/**
* Ensures no duplicate fonts are created
*
* @throws DuplicateEntityException
*/
private function validate(): void
{
// Prevents uploading the same file under a different name
if ($this->fontFactory->query(null, ['md5' => $this->md5])) {
throw new DuplicateEntityException(__('This font file already exists in the library.'));
}
// Keeps unique file storage
if ($this->fontFactory->query(null, ['fileName' => $this->fileName])) {
throw new DuplicateEntityException(__('Similar font filename already exists in the library.'));
}
// Prevents multiple fonts with same name in UI
if ($this->fontFactory->query(null, ['name' => $this->name])) {
throw new DuplicateEntityException(__('Similar font name already exists in the library.'));
}
}
}

55
lib/Entity/HelpLink.php Normal file
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\Entity;
/**
* A simple help link, used by the help service.
*/
class HelpLink implements \JsonSerializable
{
public $title;
public $summary;
public $url;
public $isAllowWhiteLabel;
/**
* @param $array
*/
public function __construct($array)
{
$this->title = $array['title'] ?? '';
$this->summary = $array['summary'] ?? '';
$this->url = $array['url'] ?? '';
$this->isAllowWhiteLabel = $array['isAllowWhiteLabel'] ?? true;
}
public function jsonSerialize(): array
{
return [
'title' => $this->title,
'summary' => $this->summary,
'url' => $this->url,
'isAllowWhiteLabel' => $this->isAllowWhiteLabel,
];
}
}

52
lib/Entity/Homepage.php Normal file
View File

@@ -0,0 +1,52 @@
<?php
/*
* Copyright (C) 2020 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\Entity;
/**
* Class Homepage
* @package Xibo\Entity
*/
class Homepage implements \JsonSerializable
{
use EntityTrait;
public $homepage;
public $feature;
public $title;
public $description;
/**
* Homepage constructor.
* @param $homepage
* @param $feature
* @param $title
* @param $description
*/
public function __construct($homepage, $feature, $title, $description)
{
$this->homepage = $homepage;
$this->feature = $feature;
$this->title = $title;
$this->description = $description;
}
}

3132
lib/Entity/Layout.php Normal file

File diff suppressed because it is too large Load Diff

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\Entity;
/**
* @SWG\Definition("Layout linked to a Campaign")
*/
class LayoutOnCampaign implements \JsonSerializable
{
use EntityTrait;
public $lkCampaignLayoutId;
public $campaignId;
public $layoutId;
public $displayOrder;
public $dayPartId;
public $daysOfWeek;
public $geoFence;
/**
* @SWG\Property(description="The Layout name (readonly)")
* @var string
*/
public $layout;
/**
* @SWG\Property(description="The Layout campaignId (readonly)")
* @var string
*/
public $layoutCampaignId;
/**
* @SWG\Property(description="The owner id (readonly))")
* @var integer
*/
public $ownerId;
/**
* @SWG\Property(description="The duration (readonly))")
* @var integer
*/
public $duration;
/**
* @SWG\Property(description="The dayPart (readonly)")
* @var string
*/
public $dayPart;
}

120
lib/Entity/LogEntry.php Normal file
View File

@@ -0,0 +1,120 @@
<?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\Entity;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class LogEntry
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class LogEntry implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The Log ID")
* @var int
*/
public $logId;
/**
* @SWG\Property(description="A unique run number for a set of Log Messages.")
* @var string
*/
public $runNo;
/**
* @SWG\Property(description="A timestamp representing the CMS date this log message occured")
* @var int
*/
public $logDate;
/**
* @SWG\Property(description="The Channel that generated this message. WEB/API/MAINT/TEST")
* @var string
*/
public $channel;
/**
* @SWG\Property(description="The requested route")
* @var string
*/
public $page;
/**
* @SWG\Property(description="The request method, GET/POST/PUT/DELETE")
* @var string
*/
public $function;
/**
* @SWG\Property(description="The log message")
* @var string
*/
public $message;
/**
* @SWG\Property(description="The display ID this message relates to or NULL for CMS")
* @var int
*/
public $displayId;
/**
* @SWG\Property(description="The Log Level")
* @var string
*/
public $type;
/**
* @SWG\Property(description="The display this message relates to or CMS for CMS.")
* @var string
*/
public $display;
/**
* @SWG\Property(description="Session history id.")
* @var int
*/
public $sessionHistoryId;
/**
* @SWG\Property(description="User id.")
* @var int
*/
public $userId;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
}

1040
lib/Entity/Media.php Normal file

File diff suppressed because it is too large Load Diff

388
lib/Entity/MenuBoard.php Normal file
View File

@@ -0,0 +1,388 @@
<?php
/*
* Copyright (c) 2022 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\Entity;
use Carbon\Carbon;
use Respect\Validation\Validator as v;
use Stash\Interfaces\PoolInterface;
use Xibo\Factory\MenuBoardCategoryFactory;
use Xibo\Factory\PermissionFactory;
use Xibo\Helper\SanitizerService;
use Xibo\Service\ConfigServiceInterface;
use Xibo\Service\DisplayNotifyServiceInterface;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
/**
* @SWG\Definition()
*/
class MenuBoard implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The Menu Board Id")
* @var int
*/
public $menuId;
/**
* @SWG\Property(description="The Menu Board name")
* @var string
*/
public $name;
/**
* @SWG\Property(description="The Menu Board description")
* @var string
*/
public $description;
/**
* @SWG\Property(description="The Menu Board code identifier")
* @var string
*/
public $code;
/**
* @SWG\Property(description="The Menu Board owner Id")
* @var int
*/
public $userId;
public $owner;
/**
* @SWG\Property(description="The Menu Board last modified date")
* @var int
*/
public $modifiedDt;
/**
* @SWG\Property(description="The Id of the Folder this Menu Board belongs to")
* @var string
*/
public $folderId;
/**
* @SWG\Property(description="The id of the Folder responsible for providing permissions for this Menu Board")
* @var int
*/
public $permissionsFolderId;
/**
* @SWG\Property(description="A comma separated list of Groups/Users that have permission to this menu Board")
* @var string
*/
public $groupsWithPermissions;
/** @var SanitizerService */
private $sanitizerService;
/** @var PoolInterface */
private $pool;
/** @var ConfigServiceInterface */
private $config;
/**
* @var Permission[]
*/
private $permissions = [];
private $categories;
/** @var PermissionFactory */
private $permissionFactory;
/** @var MenuBoardCategoryFactory */
private $menuBoardCategoryFactory;
/** @var DisplayNotifyServiceInterface */
private $displayNotifyService;
private $datesToFormat = ['modifiedDt'];
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
* @param SanitizerService $sanitizerService
* @param PoolInterface $pool
* @param ConfigServiceInterface $config
* @param PermissionFactory $permissionFactory
* @param MenuBoardCategoryFactory $menuBoardCategoryFactory
* @param DisplayNotifyServiceInterface $displayNotifyService
*/
public function __construct(
$store,
$log,
$dispatcher,
$sanitizerService,
$pool,
$config,
$permissionFactory,
$menuBoardCategoryFactory,
$displayNotifyService
) {
$this->setCommonDependencies($store, $log, $dispatcher);
$this->sanitizerService = $sanitizerService;
$this->config = $config;
$this->pool = $pool;
$this->permissionFactory = $permissionFactory;
$this->menuBoardCategoryFactory = $menuBoardCategoryFactory;
$this->displayNotifyService = $displayNotifyService;
}
/**
* @param $array
* @return \Xibo\Support\Sanitizer\SanitizerInterface
*/
protected function getSanitizer($array)
{
return $this->sanitizerService->getSanitizer($array);
}
public function __clone()
{
$this->menuId = null;
}
/**
* @return string
*/
public function __toString()
{
return sprintf('MenuId %d, Name %s, Description %s, Code %s', $this->menuId, $this->name, $this->description, $this->code);
}
/**
* Get the Id
* @return int
*/
public function getId()
{
return $this->menuId;
}
public function getPermissionFolderId()
{
return $this->permissionsFolderId;
}
/**
* Get the OwnerId
* @return int
*/
public function getOwnerId()
{
return $this->userId;
}
/**
* Sets the Owner
* @param int $ownerId
*/
public function setOwner($ownerId)
{
$this->userId = $ownerId;
}
/**
* @param array $options
* @return MenuBoard
* @throws NotFoundException
*/
public function load($options = [])
{
$options = array_merge([
'loadPermissions' => true,
'loadCategories' => false
], $options);
// If we are already loaded, then don't do it again
if ($this->menuId == null || $this->loaded) {
return $this;
}
// Permissions
if ($options['loadPermissions']) {
$this->permissions = $this->permissionFactory->getByObjectId('MenuBoard', $this->menuId);
}
if ($options['loadCategories']) {
$this->categories = $this->menuBoardCategoryFactory->getByMenuId($this->menuId);
}
$this->loaded = true;
return $this;
}
/**
* @throws InvalidArgumentException
*/
public function validate()
{
if (!v::stringType()->notEmpty()->validate($this->name)) {
throw new InvalidArgumentException(__('Name cannot be empty'), 'name');
}
}
/**
* Save this Menu Board
* @param array $options
* @throws InvalidArgumentException
*/
public function save($options = [])
{
$options = array_merge([
'validate' => true,
'audit' => true
], $options);
if ($options['audit']) {
$this->getLog()->debug('Saving ' . $this);
}
if ($options['validate']) {
$this->validate();
}
if ($this->menuId == null || $this->menuId == 0) {
$this->add();
$this->loaded = true;
} else {
$this->update();
}
// We've been touched
$this->setActive();
// Notify Displays?
$this->notify();
}
/**
* Is this MenuBoard active currently
* @return bool
*/
public function isActive()
{
$cache = $this->pool->getItem('/menuboard/accessed/' . $this->menuId);
return $cache->isHit();
}
/**
* Indicate that this MenuBoard has been accessed recently
* @return $this
*/
public function setActive()
{
$this->getLog()->debug('Setting ' . $this->menuId . ' as active');
$cache = $this->pool->getItem('/menuboard/accessed/' . $this->menuId);
$cache->set('true');
$cache->expiresAfter(intval($this->config->getSetting('REQUIRED_FILES_LOOKAHEAD')) * 1.5);
$this->pool->saveDeferred($cache);
return $this;
}
/**
* Get the Display Notify Service
* @return DisplayNotifyServiceInterface
*/
public function getDisplayNotifyService(): DisplayNotifyServiceInterface
{
return $this->displayNotifyService->init();
}
/**
* Notify displays of this campaign change
*/
public function notify()
{
$this->getLog()->debug('MenuBoard ' . $this->menuId . ' wants to notify');
$this->getDisplayNotifyService()->collectNow()->notifyByMenuBoardId($this->menuId);
}
private function add()
{
$this->menuId = $this->getStore()->insert(
'INSERT INTO `menu_board` (name, description, code, userId, modifiedDt, folderId, permissionsFolderId) VALUES (:name, :description, :code, :userId, :modifiedDt, :folderId, :permissionsFolderId)',
[
'name' => $this->name,
'description' => $this->description,
'code' => $this->code,
'userId' => $this->userId,
'modifiedDt' => Carbon::now()->format('U'),
'folderId' => ($this->folderId == null) ? 1 : $this->folderId,
'permissionsFolderId' => ($this->permissionsFolderId == null) ? 1 : $this->permissionsFolderId
]
);
}
private function update()
{
$this->getStore()->update(
'UPDATE `menu_board` SET name = :name, description = :description, code = :code, userId = :userId, modifiedDt = :modifiedDt, folderId = :folderId, permissionsFolderId = :permissionsFolderId WHERE menuId = :menuId',
[
'menuId' => $this->menuId,
'name' => $this->name,
'description' => $this->description,
'code' => $this->code,
'userId' => $this->userId,
'modifiedDt' => Carbon::now()->format('U'),
'folderId' => $this->folderId,
'permissionsFolderId' => $this->permissionsFolderId
]
);
}
/**
* Delete Menu Board
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\DuplicateEntityException
*/
public function delete()
{
$this->load(['loadCategories' => true]);
// Delete all permissions
foreach ($this->permissions as $permission) {
/* @var Permission $permission */
$permission->delete();
}
// Delete all
/** @var MenuBoardCategory $category */
foreach ($this->categories as $category) {
$category->delete();
}
$this->getStore()->update('DELETE FROM `menu_board` WHERE menuId = :menuId', ['menuId' => $this->menuId]);
}
}

View File

@@ -0,0 +1,263 @@
<?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\Entity;
use Respect\Validation\Validator as v;
use Xibo\Factory\MenuBoardCategoryFactory;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
use Xibo\Widget\DataType\ProductCategory;
/**
* @SWG\Definition()
*/
class MenuBoardCategory implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The Menu Board Category Id")
* @var int
*/
public $menuCategoryId;
/**
* @SWG\Property(description="The Menu Board Id")
* @var int
*/
public $menuId;
/**
* @SWG\Property(description="The Menu Board Category name")
* @var string
*/
public $name;
/**
* @SWG\Property(description="The Menu Board Category description")
* @var string
*/
public $description;
/**
* @SWG\Property(description="The Menu Board Category code identifier")
* @var string
*/
public $code;
/**
* @SWG\Property(description="The Menu Board Category associated mediaId")
* @var int
*/
public $mediaId;
private $products;
/** @var MenuBoardCategoryFactory */
private $menuCategoryFactory;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
* @param MenuBoardCategoryFactory $menuCategoryFactory
*/
public function __construct($store, $log, $dispatcher, $menuCategoryFactory)
{
$this->setCommonDependencies($store, $log, $dispatcher);
$this->menuCategoryFactory = $menuCategoryFactory;
}
public function __clone()
{
$this->menuCategoryId = null;
}
/**
* @return string
*/
public function __toString()
{
return sprintf(
'MenuCategoryId %d MenuId %d, Name %s, Media %d, Code %s',
$this->menuCategoryId,
$this->menuId,
$this->name,
$this->mediaId,
$this->code
);
}
/**
* Convert this to a product category
* @return ProductCategory
*/
public function toProductCategory(): ProductCategory
{
$productCategory = new ProductCategory();
$productCategory->name = $this->name;
$productCategory->description = $this->description;
$productCategory->image = $this->mediaId;
return $productCategory;
}
/**
* Get the Id
* @return int
*/
public function getId()
{
return $this->menuCategoryId;
}
/**
* @param array $options
* @return MenuBoardCategory
* @throws NotFoundException
*/
public function load($options = [])
{
$options = array_merge([
'loadProducts' => false
], $options);
// If we are already loaded, then don't do it again
if ($this->menuId == null || $this->loaded) {
return $this;
}
if ($options['loadProducts']) {
$this->products = $this->getProducts();
}
$this->loaded = true;
return $this;
}
/**
* @throws InvalidArgumentException
*/
public function validate()
{
if (!v::stringType()->notEmpty()->validate($this->name)) {
throw new InvalidArgumentException(__('Name cannot be empty'), 'name');
}
}
/**
* @param array|null $sort The sort order to be applied
* @return MenuBoardProduct[]
*/
public function getProducts($sort = null): array
{
return $this->menuCategoryFactory->getProductData($sort, [
'menuCategoryId' => $this->menuCategoryId
]);
}
/**
* @param array|null $sort The sort order to be applied
* @return MenuBoardProduct[]
*/
public function getAvailableProducts($sort = null): array
{
return $this->menuCategoryFactory->getProductData($sort, [
'menuCategoryId' => $this->menuCategoryId,
'availability' => 1
]);
}
/**
* Save this Menu Board Category
* @param array $options
* @throws InvalidArgumentException
*/
public function save($options = [])
{
$options = array_merge([
'validate' => true,
], $options);
$this->getLog()->debug('Saving ' . $this);
if ($options['validate']) {
$this->validate();
}
if ($this->menuCategoryId == null || $this->menuCategoryId == 0) {
$this->add();
$this->loaded = true;
} else {
$this->update();
}
}
private function add(): void
{
$this->menuCategoryId = $this->getStore()->insert('
INSERT INTO `menu_category` (`name`, `menuId`, `mediaId`, `code`, `description`)
VALUES (:name, :menuId, :mediaId, :code, :description)
', [
'name' => $this->name,
'mediaId' => $this->mediaId,
'menuId' => $this->menuId,
'code' => $this->code,
'description' => $this->description,
]);
}
private function update(): void
{
$this->getStore()->update('
UPDATE `menu_category`
SET `name` = :name, `mediaId` = :mediaId, `code` = :code, `description` = :description
WHERE `menuCategoryId` = :menuCategoryId
', [
'menuCategoryId' => $this->menuCategoryId,
'name' => $this->name,
'mediaId' => $this->mediaId,
'code' => $this->code,
'description' => $this->description,
]);
}
/**
* Delete Menu Board
* @throws NotFoundException
*/
public function delete()
{
$this->load(['loadProducts' => true]);
/** @var MenuBoardProduct $product */
foreach ($this->products as $product) {
$product->delete();
}
$this->getStore()->update('DELETE FROM `menu_category` WHERE menuCategoryId = :menuCategoryId', ['menuCategoryId' => $this->menuCategoryId]);
}
}

View File

@@ -0,0 +1,327 @@
<?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\Entity;
use Respect\Validation\Validator as v;
use Xibo\Factory\MenuBoardProductOptionFactory;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Widget\DataType\Product;
/**
* @SWG\Definition()
*/
class MenuBoardProduct implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The Menu Board Product Id")
* @var int
*/
public $menuProductId;
/**
* @SWG\Property(description="The Menu Board Category Id")
* @var int
*/
public $menuCategoryId;
/**
* @SWG\Property(description="The Menu Board Id")
* @var int
*/
public $menuId;
/**
* @SWG\Property(description="The Menu Board Category name")
* @var string
*/
public $name;
/**
* @SWG\Property(description="The Menu Board Product price")
* @var double
*/
public $price;
/**
* @SWG\Property(description="The Menu Board Product description")
* @var string
*/
public $description;
/**
* @SWG\Property(description="The Menu Board Product code identifier")
* @var string
*/
public $code;
/**
* @SWG\Property(description="The Menu Board Product display order, used for sorting")
* @var int
*/
public $displayOrder;
/**
* @SWG\Property(description="The Menu Board Product availability")
* @var int
*/
public $availability;
/**
* @SWG\Property(description="The Menu Board Product allergy information")
* @var string
*/
public $allergyInfo;
/**
* @SWG\Property(description="The Menu Board Product allergy information")
* @var int
*/
public $calories;
/**
* @SWG\Property(description="The Menu Board Product associated mediaId")
* @var int
*/
public $mediaId;
/**
* @SWG\Property(description="The Menu Board Product array of options", @SWG\Items(type="string"))
* @var MenuBoardProductOption[]
*/
public $productOptions;
/**
* @var MenuBoardProductOptionFactory
*/
private $menuBoardProductOptionFactory;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
* @param MenuBoardProductOptionFactory $menuBoardProductOptionFactory
*/
public function __construct($store, $log, $dispatcher, $menuBoardProductOptionFactory)
{
$this->setCommonDependencies($store, $log, $dispatcher);
$this->menuBoardProductOptionFactory = $menuBoardProductOptionFactory;
}
/**
* Get the Id
* @return int
*/
public function getId(): int
{
return $this->menuProductId;
}
/**
* @throws InvalidArgumentException
*/
public function validate(): void
{
if (!v::stringType()->notEmpty()->validate($this->name)) {
throw new InvalidArgumentException(__('Name cannot be empty'), 'name');
}
if (!empty($this->calories) && !v::intType()->min(0)->max(32767)->validate($this->calories)) {
throw new InvalidArgumentException(
__('Calories must be a whole number between 0 and 32767'),
'calories'
);
}
}
/**
* @return string
*/
public function __toString()
{
return sprintf(
'MenuProductId %d, MenuCategoryId %d, MenuId %d, Name %s, Price %s, Media %d, Code %s',
$this->menuProductId,
$this->menuCategoryId,
$this->menuId,
$this->name,
$this->price,
$this->mediaId,
$this->code
);
}
/**
* Convert this to a Product
* @return Product
*/
public function toProduct(): Product
{
$product = new Product();
$product->name = $this->name;
$product->price = $this->price;
$product->description = $this->description;
$product->availability = $this->availability;
$product->allergyInfo = $this->allergyInfo;
$product->calories = $this->calories;
$product->image = $this->mediaId;
foreach (($this->productOptions ?? []) as $productOption) {
$product->productOptions[] = [
'name' => $productOption->option,
'value' => $productOption->value,
];
}
return $product;
}
/**
* Save this Menu Board Product
* @param array $options
* @throws InvalidArgumentException
*/
public function save($options = [])
{
$options = array_merge([
'validate' => true,
], $options);
$this->getLog()->debug('Saving ' . $this);
if ($options['validate']) {
$this->validate();
}
if ($this->menuProductId == null || $this->menuProductId == 0) {
$this->add();
} else {
$this->update();
}
}
/**
* Add Menu Board Product
*/
private function add(): void
{
$this->menuProductId = $this->getStore()->insert('
INSERT INTO `menu_product` (
`menuCategoryId`,
`menuId`,
`name`,
`price`,
`description`,
`mediaId`,
`displayOrder`,
`availability`,
`allergyInfo`,
`calories`,
`code`
)
VALUES (
:menuCategoryId,
:menuId,
:name,
:price,
:description,
:mediaId,
:displayOrder,
:availability,
:allergyInfo,
:calories,
:code
)
', [
'menuCategoryId' => $this->menuCategoryId,
'menuId' => $this->menuId,
'name' => $this->name,
'price' => $this->price,
'description' => $this->description,
'mediaId' => $this->mediaId,
'displayOrder' => $this->displayOrder,
'availability' => $this->availability,
'allergyInfo' => $this->allergyInfo,
'calories' => $this->calories,
'code' => $this->code,
]);
}
/**
* Update Menu Board Product
*/
private function update(): void
{
$this->getStore()->update('
UPDATE `menu_product` SET
`name` = :name,
`price` = :price,
`description` = :description,
`mediaId` = :mediaId,
`displayOrder` = :displayOrder,
`availability` = :availability,
`allergyInfo` = :allergyInfo,
`calories` = :calories,
`code` = :code
WHERE `menuProductId` = :menuProductId
', [
'name' => $this->name,
'price' => $this->price,
'description' => $this->description,
'mediaId' => $this->mediaId,
'displayOrder' => $this->displayOrder,
'availability' => $this->availability,
'allergyInfo' => $this->allergyInfo,
'calories' => $this->calories,
'code' => $this->code,
'menuProductId' => $this->menuProductId
]);
}
/**
* Delete Menu Board Product
*/
public function delete()
{
$this->removeOptions();
$this->getStore()->update('DELETE FROM `menu_product` WHERE menuProductId = :menuProductId', ['menuProductId' => $this->menuProductId]);
}
/**
* @return MenuBoardProductOption[]
*/
public function getOptions()
{
$options = $this->menuBoardProductOptionFactory->getByMenuProductId($this->menuProductId);
return $options;
}
public function removeOptions()
{
$this->getStore()->update('DELETE FROM `menu_product_options` WHERE menuProductId = :menuProductId', ['menuProductId' => $this->menuProductId]);
}
}

View File

@@ -0,0 +1,130 @@
<?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\Entity;
use Respect\Validation\Validator as v;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\InvalidArgumentException;
/**
* @SWG\Definition()
*/
class MenuBoardProductOption implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The Menu Product ID that this Option belongs to")
* @var int
*/
public $menuProductId;
/**
* @SWG\Property(description="The option name")
* @var string
*/
public $option;
/**
* @SWG\Property(description="The option value")
* @var string
*/
public $value;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
public function __clone()
{
$this->menuProductId = null;
}
public function __toString()
{
return sprintf('ProductOption %s with value %s', $this->option, $this->value);
}
/**
* @throws InvalidArgumentException
*/
public function validate()
{
if (!v::stringType()->notEmpty()->validate($this->option)
&& v::floatType()->notEmpty()->validate($this->value)
) {
throw new InvalidArgumentException(__('Each value needs a corresponding option'), 'option');
}
if (!v::floatType()->notEmpty()->validate($this->value)
&& v::stringType()->notEmpty()->validate($this->option)
) {
throw new InvalidArgumentException(__('Each option needs a corresponding value'), 'value');
}
}
/**
* @param array $options
* @throws InvalidArgumentException
*/
public function save($options = [])
{
$options = array_merge([
'validate' => true,
], $options);
$this->getLog()->debug('Saving ' . $this);
if ($options['validate']) {
$this->validate();
}
$this->getStore()->insert(
'INSERT INTO `menu_product_options` (`menuProductId`, `option`, `value`) VALUES (:menuProductId, :option, :value) ON DUPLICATE KEY UPDATE `value` = :value2',
[
'menuProductId' => $this->menuProductId,
'option' => $this->option,
'value' => $this->value,
'value2' => $this->value
]
);
}
public function delete()
{
$this->getStore()->update(
'DELETE FROM `menu_product_options` WHERE `menuProductId` = :menuProductId AND `option` = :option',
[
'menuProductId' => $this->menuProductId,
'option' => $this->option
]
);
}
}

649
lib/Entity/Module.php Normal file
View File

@@ -0,0 +1,649 @@
<?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\Entity;
use Respect\Validation\Validator as v;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Xibo\Factory\ModuleFactory;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Widget\Definition\LegacyType;
use Xibo\Widget\Provider\DataProvider;
use Xibo\Widget\Provider\WidgetCompatibilityInterface;
use Xibo\Widget\Provider\WidgetProviderInterface;
use Xibo\Widget\Provider\WidgetValidatorInterface;
/**
* Class Module
* @package Xibo\Entity
* @SWG\Definition()
*/
class Module implements \JsonSerializable
{
use EntityTrait;
use ModulePropertyTrait;
/**
* @SWG\Property(description="The ID of this Module")
* @var int
*/
public $moduleId;
/**
* @SWG\Property(description="Module Name")
* @var string
*/
public $name;
/**
* @SWG\Property(description="Module Author")
* @var string
*/
public $author;
/**
* @SWG\Property(description="Description of the Module")
* @var string
*/
public $description;
/**
* @SWG\Property(description="An icon to use in the toolbar")
* @var string
*/
public $icon;
/**
* @SWG\Property(description="The type code for this module")
* @var string
*/
public $type;
/**
* @SWG\Property(description="Legacy type codes for this module")
* @var LegacyType[]
*/
public $legacyTypes;
/**
* @SWG\Property(description="The data type of the data expected to be returned by this modules data provider")
* @var string
*/
public $dataType;
/**
* @SWG\Property(description="The group details for this module")
* @var string[]
*/
public $group;
/**
* @SWG\Property(description="The cache key used when requesting data")
* @var string
*/
public $dataCacheKey;
/**
* @SWG\Property(description="Is fallback data allowed for this module? Only applicable for a Data Widget")
* @var int
*/
public $fallbackData;
/**
* @SWG\Property(description="Is specific to a Layout or can be uploaded to the Library?")
* @var int
*/
public $regionSpecific;
/**
* @SWG\Property(description="The schema version of the module")
* @var int
*/
public $schemaVersion;
/**
* @SWG\Property(description="The compatibility class of the module")
* @var string
*/
public $compatibilityClass = null;
/**
* @SWG\Property(description="A flag indicating whether the module should be excluded from the Layout Editor")
* @var string
*/
public $showIn = 'both';
/**
* @SWG\Property(description="A flag indicating whether the module is assignable to a Layout")
* @var int
*/
public $assignable;
/**
* @SWG\Property(description="Does this module have a thumbnail to render?")
* @var int
*/
public $hasThumbnail;
/**
* @SWG\Property(description="This is the location to a module's thumbnail")
* @var string
*/
public $thumbnail;
/** @var int The width of the zone */
public $startWidth;
/** @var int The height of the zone */
public $startHeight;
/**
* @SWG\Property(description="Should be rendered natively by the Player or via the CMS (native|html)")
* @var string
*/
public $renderAs;
/**
* @SWG\Property(description="Class Name including namespace")
* @var string
*/
public $class;
/**
* @SWG\Property(description="Validator class name including namespace")
* @var string[]
*/
public $validatorClass = [];
/** @var \Xibo\Widget\Definition\Stencil|null Stencil for this modules preview */
public $preview;
/** @var \Xibo\Widget\Definition\Stencil|null Stencil for this modules HTML cache */
public $stencil;
/**
* @SWG\Property(description="Properties to display in the property panel and supply to stencils")
* @var \Xibo\Widget\Definition\Property[]|null
*/
public $properties;
/** @var \Xibo\Widget\Definition\Asset[]|null */
public $assets;
/**
* @SWG\Property(description="JavaScript function run when a module is initialised, before data is returned")
* @var string
*/
public $onInitialize;
/**
* @SWG\Property(description="Data Parser run against each data item applicable when a dataType is present")
* @var string
*/
public $onParseData;
/**
* @SWG\Property(description="A load function to run when the widget first fetches data")
* @var string
*/
public $onDataLoad;
/**
* @SWG\Property(description="JavaScript function run when a module is rendered, after data has been returned")
* @var string
*/
public $onRender;
/**
* @SWG\Property(description="JavaScript function run when a module becomes visible")
* @var string
*/
public $onVisible;
/**
* @SWG\Property(description="Optional sample data item, only applicable when a dataType is present")
* @var string
*/
public $sampleData;
// <editor-fold desc="Properties recorded in the database">
/**
* @SWG\Property(description="A flag indicating whether this module is enabled")
* @var int
*/
public $enabled;
/**
* @SWG\Property(description="A flag indicating whether the Layout designer should render a preview of this module")
* @var int
*/
public $previewEnabled;
/**
* @SWG\Property(
* description="The default duration for Widgets of this Module when the user has not set a duration."
* )
* @var int
*/
public $defaultDuration;
/**
* @SWG\Property(description="An array of additional module specific settings")
* @var \Xibo\Widget\Definition\Property[]
*/
public $settings = [];
/**
* @SWG\Property(description="An array of additional module specific group properties")
* @var \Xibo\Widget\Definition\PropertyGroup[]
*/
public $propertyGroups = [];
/**
* @SWG\Property(
* description="An array of required elements",
* type="array",
* @SWG\Items(type="string")
* )
* @var string[]
*/
public $requiredElements = [];
/**
* @SWG\Property()
* @var bool $isInstalled Is this module installed?
*/
public $isInstalled;
/**
* @SWG\Property()
* @var bool $isError Does this module have any errors?
*/
public $isError;
/**
* @SWG\Property()
* @var string[] $errors An array of errors this module has.
*/
public $errors;
// </editor-fold>
public $allowPreview;
/** @var ModuleFactory */
private $moduleFactory;
/** @var WidgetProviderInterface */
private $widgetProvider;
/** @var WidgetCompatibilityInterface */
private $widgetCompatibility;
/** @var WidgetValidatorInterface[] */
private $widgetValidators = [];
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
* @param \Xibo\Factory\ModuleFactory $moduleFactory
*/
public function __construct(
StorageServiceInterface $store,
LogServiceInterface $log,
EventDispatcherInterface $dispatcher,
ModuleFactory $moduleFactory
) {
$this->setCommonDependencies($store, $log, $dispatcher);
$this->moduleFactory = $moduleFactory;
}
/**
* @return string
*/
public function __toString()
{
return sprintf('%s - %s', $this->type, $this->name);
}
/**
* Is a template expected?
* @return bool
*/
public function isTemplateExpected(): bool
{
return (!empty($this->dataType));
}
/**
* Is a template expected?
* @return bool
*/
public function isDataProviderExpected(): bool
{
return (!empty($this->dataType));
}
/**
* Does this module have required elements?
* @return bool
*/
public function hasRequiredElements(): bool
{
return count($this->requiredElements) > 0;
}
/**
* Get this module's widget provider, or null if there isn't one
* @return \Xibo\Widget\Provider\WidgetProviderInterface|null
*/
public function getWidgetProviderOrNull(): ?WidgetProviderInterface
{
return $this->widgetProvider;
}
/**
* @param \Xibo\Entity\Widget $widget
* @return \Xibo\Widget\Provider\DataProvider
*/
public function createDataProvider(Widget $widget): DataProvider
{
return $this->moduleFactory->createDataProvider($this, $widget);
}
/**
* Fetch duration of a file.
* @param string $file
* @return int
*/
public function fetchDurationOrDefaultFromFile(string $file): int
{
$this->getLog()->debug('fetchDurationOrDefaultFromFile: fetchDuration with file: ' . $file);
// If we don't have a file name, then we use the default duration of 0 (end-detect)
if (empty($file)) {
return 0;
} else {
$info = new \getID3();
$file = $info->analyze($file);
// Log error if duration is missing
if (!isset($file['playtime_seconds'])) {
$errorMessage = isset($file['error'])
? implode('; ', $file['error'])
: 'Unknown';
$this->getLog()->error('fetchDurationOrDefaultFromFile; Missing playtime_seconds in analyzed
file. Error: ' . $errorMessage);
}
return intval($file['playtime_seconds'] ?? $this->defaultDuration);
}
}
/**
* Calculate the duration of this Widget.
* @param Widget $widget
* @return int|null
*/
public function calculateDuration(Widget $widget): ?int
{
if ($this->widgetProvider === null && $this->regionSpecific === 1) {
// Take some default action to cover the majourity of region specific widgets
// Duration can depend on the number of items per page for some widgets
// this is a legacy way of working, and our preference is to use elements
$numItems = $widget->getOptionValue('numItems', 15);
if ($widget->getOptionValue('durationIsPerItem', 0) == 1 && $numItems > 1) {
// If we have paging involved then work out the page count.
$itemsPerPage = $widget->getOptionValue('itemsPerPage', 0);
if ($itemsPerPage > 0) {
$numItems = ceil($numItems / $itemsPerPage);
}
return $widget->calculatedDuration * $numItems;
} else {
return null;
}
} else if ($this->widgetProvider === null) {
return null;
}
$this->getLog()->debug('calculateDuration: using widget provider');
$durationProvider = $this->moduleFactory->createDurationProvider($this, $widget);
$this->widgetProvider->fetchDuration($durationProvider);
return $durationProvider->isDurationSet() ? $durationProvider->getDuration() : null;
}
/**
* Sets the widget provider for this module
* @param \Xibo\Widget\Provider\WidgetProviderInterface $widgetProvider
* @return $this
*/
public function setWidgetProvider(WidgetProviderInterface $widgetProvider): Module
{
$this->widgetProvider = $widgetProvider;
$this->widgetProvider
->setLog($this->getLog()->getLoggerInterface())
->setDispatcher($this->getDispatcher());
return $this;
}
/**
* Is a widget compatibility available
* @return bool
*/
public function isWidgetCompatibilityAvailable(): bool
{
return $this->widgetCompatibility !== null;
}
/**
* Get this module's widget compatibility, or null if there isn't one
* @return \Xibo\Widget\Provider\WidgetCompatibilityInterface|null
*/
public function getWidgetCompatibilityOrNull(): ?WidgetCompatibilityInterface
{
return $this->widgetCompatibility;
}
/**
* Sets the widget compatibility for this module
* @param WidgetCompatibilityInterface $widgetCompatibility
* @return $this
*/
public function setWidgetCompatibility(WidgetCompatibilityInterface $widgetCompatibility): Module
{
$this->widgetCompatibility = $widgetCompatibility;
$this->widgetCompatibility->setLog($this->getLog()->getLoggerInterface());
return $this;
}
public function addWidgetValidator(WidgetValidatorInterface $widgetValidator): Module
{
$this->widgetValidators[] = $widgetValidator;
return $this;
}
/**
* Get this module's widget validators
* @return \Xibo\Widget\Provider\WidgetValidatorInterface[]
*/
public function getWidgetValidators(): array
{
return $this->widgetValidators;
}
/**
* Get all properties which allow library references.
* @return \Xibo\Widget\Definition\Property[]
*/
public function getPropertiesAllowingLibraryRefs(): array
{
$props = [];
foreach ($this->properties as $property) {
if ($property->allowLibraryRefs) {
$props[] = $property;
}
}
return $props;
}
/**
* Get assets
* @return \Xibo\Widget\Definition\Asset[]
*/
public function getAssets(): array
{
return $this->assets;
}
/**
* Get a module setting
* If the setting does not exist, $default will be returned.
* If the setting exists, but is not set, the default value from the setting will be returned
* @param string $setting The setting
* @param mixed|null $default A default value if the setting does not exist
* @return mixed
*/
public function getSetting(string $setting, $default = null)
{
foreach ($this->settings as $property) {
if ($property->id === $setting) {
return $property->value ?? $property->default;
}
}
return $default;
}
/**
* @throws \Xibo\Support\Exception\InvalidArgumentException
*/
public function validate()
{
if (!v::intType()->validate($this->defaultDuration)) {
throw new InvalidArgumentException(__('Default Duration is a required field.'), 'defaultDuration');
}
}
/**
* @throws \Xibo\Support\Exception\InvalidArgumentException
*/
public function save()
{
$this->validate();
if (!$this->isInstalled) {
$this->add();
} else {
$this->edit();
}
}
private function add()
{
$this->moduleId = $this->getStore()->insert('
INSERT INTO `module` (
`moduleId`,
`enabled`,
`previewEnabled`,
`defaultDuration`,
`settings`
)
VALUES (
:moduleId,
:enabled,
:previewEnabled,
:defaultDuration,
:settings
)
', [
'moduleId' => $this->moduleId,
'enabled' => $this->enabled,
'previewEnabled' => $this->previewEnabled,
'defaultDuration' => $this->defaultDuration,
'settings' => $this->getSettingsForSaving()
]);
}
private function edit()
{
$this->getStore()->update('
UPDATE `module` SET
enabled = :enabled,
previewEnabled = :previewEnabled,
defaultDuration = :defaultDuration,
settings = :settings
WHERE moduleid = :moduleId
', [
'moduleId' => $this->moduleId,
'enabled' => $this->enabled,
'previewEnabled' => $this->previewEnabled,
'defaultDuration' => $this->defaultDuration,
'settings' => $this->getSettingsForSaving()
]);
}
/**
* @return string
*/
private function getSettingsForSaving(): string
{
$settings = [];
foreach ($this->settings as $setting) {
if ($setting->value !== null) {
$settings[$setting->id] = $setting->value;
}
}
return count($settings) > 0 ? json_encode($settings) : '[]';
}
/**
* @return array
*/
public function getSettingsForOutput(): array
{
$settings = [];
foreach ($this->settings as $setting) {
$settings[$setting->id] = $setting->value ?? $setting->default;
}
return $settings;
}
/**
* Delete this module
* @return void
*/
public function delete()
{
$this->getStore()->update('DELETE FROM `module` WHERE moduleId = :id', [
'id' => $this->moduleId
]);
}
}

View File

@@ -0,0 +1,208 @@
<?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\Entity;
use Xibo\Helper\DateFormatHelper;
/**
* A trait for common functionality in regard to properties on modules/module templates
*/
trait ModulePropertyTrait
{
/**
* @param Widget $widget
* @param bool $includeDefaults
* @param bool $reverseFilters Reverse filters?
* @return $this
*/
public function decorateProperties(Widget $widget, bool $includeDefaults = false, bool $reverseFilters = true)
{
foreach ($this->properties as $property) {
$property->value = $widget->getOptionValue($property->id, null);
// Should we include defaults?
if ($includeDefaults && $property->value === null) {
$property->value = $property->default;
}
if ($property->value !== null) {
if ($property->type === 'integer') {
$property->value = intval($property->value);
} else if ($property->type === 'double' || $property->type === 'number') {
$property->value = doubleval($property->value);
} else if ($property->type === 'checkbox') {
$property->value = intval($property->value);
}
}
if ($reverseFilters) {
$property->reverseFilters();
}
}
return $this;
}
/**
* @param array $properties
* @param bool $includeDefaults
* @return array
*/
public function decoratePropertiesByArray(array $properties, bool $includeDefaults = false): array
{
// Flatten the properties array so that we can reference it by key.
$keyedProperties = [];
foreach ($properties as $property) {
$keyedProperties[$property['id']] = $property['value'] ?? null;
}
$decoratedProperties = [];
foreach ($this->properties as $property) {
$decoratedProperty = $keyedProperties[$property->id] ?? null;
// Should we include defaults?
if ($includeDefaults && $decoratedProperty === null) {
$decoratedProperty = $property->default;
}
if ($decoratedProperty !== null) {
if ($property->type === 'integer') {
$decoratedProperty = intval($decoratedProperty);
} else if ($property->type === 'double' || $property->type === 'number') {
$decoratedProperty = doubleval($decoratedProperty);
} else if ($property->type === 'checkbox') {
$decoratedProperty = intval($decoratedProperty);
}
}
$decoratedProperty = $property->reverseFiltersOnValue($decoratedProperty);
// Add our decorated property
$decoratedProperties[$property->id] = $decoratedProperty;
}
return $decoratedProperties;
}
/**
* @param bool $decorateForOutput true if we should decorate for output to either the preview or player
* @param array|null $overrideValues a key/value array of values to use instead the stored property values
* @param bool $includeDefaults include default values
* @param bool $skipNullProperties skip null properties
* @return array
*/
public function getPropertyValues(
bool $decorateForOutput = true,
?array $overrideValues = null,
bool $includeDefaults = false,
bool $skipNullProperties = false,
): array {
$properties = [];
foreach ($this->properties as $property) {
$value = $overrideValues !== null ? ($overrideValues[$property->id] ?? null) : $property->value;
if ($includeDefaults && $value === null) {
$value = $property->default ?? null;
}
if ($skipNullProperties && $value === null) {
continue;
}
// TODO: should we cast values to their appropriate field formats.
if ($decorateForOutput) {
// Does this property have library references?
if ($property->allowLibraryRefs && !empty($value)) {
// Parse them out and replace for our special syntax.
// TODO: Can we improve this regex to ignore things we suspect are JavaScript array access?
$matches = [];
preg_match_all('/\[(.*?)\]/', $value, $matches);
foreach ($matches[1] as $match) {
// We ignore non-numbers and zero/negative integers
if (is_numeric($match) && intval($match) > 0) {
$value = str_replace(
'[' . $match . ']',
'[[mediaId=' . $match . ']]',
$value
);
}
}
}
// Do we need to parse out any translations? We only do this on output.
if ($property->parseTranslations && !empty($value)) {
$matches = [];
preg_match_all('/\|\|.*?\|\|/', $value, $matches);
foreach ($matches[0] as $sub) {
// Parse out the translatable string and substitute
$value = str_replace($sub, __(str_replace('||', '', $sub)), $value);
}
}
// Date format
if ($property->variant === 'dateFormat' && !empty($value)) {
$value = DateFormatHelper::convertPhpToMomentFormat($value);
}
// Media selector
if ($property->type === 'mediaSelector') {
$value = (!$value) ? '' : '[[mediaId=' . $value . ']]';
}
}
$properties[$property->id] = $value;
}
return $properties;
}
/**
* Gets the default value for a property
* @param string $id
* @return mixed
*/
public function getPropertyDefault(string $id): mixed
{
foreach ($this->properties as $property) {
if ($property->id === $id) {
return $property->default;
}
}
return null;
}
/**
* @throws \Xibo\Support\Exception\InvalidArgumentException|\Xibo\Support\Exception\ValueTooLargeException
*/
public function validateProperties(string $stage, $additionalProperties = []): void
{
// Go through all of our required properties, and validate that they are as they should be.
// provide a key/value state of all current properties
$properties = array_merge(
$this->getPropertyValues(false, null, true),
$additionalProperties,
);
foreach ($this->properties as $property) {
$property->validate($properties, $stage);
}
}
}

View File

@@ -0,0 +1,380 @@
<?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\Entity;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Xibo\Factory\ModuleTemplateFactory;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Widget\Definition\Asset;
/**
* Represents a module template
* @SWG\Definition()
*/
class ModuleTemplate implements \JsonSerializable
{
use EntityTrait;
use ModulePropertyTrait;
/** @var int The database ID */
public $id;
/**
* @SWG\Property()
* @var string The templateId
*/
public $templateId;
/**
* @SWG\Property()
* @var string Type of template (static|element|stencil)
*/
public $type;
/**
* @SWG\Property()
* @var \Xibo\Widget\Definition\Extend|null If this template extends another
*/
public $extends;
/**
* @SWG\Property()
* @var string The datatype of this template
*/
public $dataType;
/**
* @SWG\Property()
* @var string The title
*/
public $title;
/**
* @SWG\Property(description="Description of the Module Template")
* @var string
*/
public $description;
/**
* @SWG\Property()
* @var string Icon
*/
public $icon;
/**
* @SWG\Property()
* Thumbnail
* this is the location to a module template's thumbnail, which should be added to the installation
* relative to the module class file.
* @var string
*/
public $thumbnail;
/** @var int The width of the zone */
public $startWidth;
/** @var int The height of the zone */
public $startHeight;
/** @var bool Does this template have dimensions? */
public $hasDimensions;
/** @var bool Can this template be rotated? */
public $canRotate;
/**
* @SWG\Property(description="A flag indicating whether the template should be excluded from the Layout Editor")
* @var string
*/
public $showIn = 'both';
/**
* @SWG\Property()
* @var \Xibo\Widget\Definition\Property[]|null Properties
*/
public $properties;
/**
* @SWG\Property()
* @var bool Is Visible?
*/
public $isVisible = true;
/**
* @SWG\Property()
* @var bool Is Enabled?
*/
public $isEnabled = true;
/**
* @SWG\Property(description="An array of additional module specific group properties")
* @var \Xibo\Widget\Definition\PropertyGroup[]
*/
public $propertyGroups = [];
/**
* @SWG\Property()
* @var \Xibo\Widget\Definition\Stencil|null A stencil, if needed
*/
public $stencil;
/**
* @SWG\Property()
* @var Asset[]
*/
public $assets;
/** @var string A Renderer to run if custom rendering is required. */
public $onTemplateRender;
/** @var string JavaScript function run when the template becomes visible. */
public $onTemplateVisible;
/** @var string A data parser for elements */
public $onElementParseData;
/** @var bool $isError Does this module have any errors? */
public $isError;
/** @var string[] $errors An array of errors this module has. */
public $errors;
/** @var string $ownership Who owns this file? system|custom|user */
public $ownership;
/** @var int $ownerId User ID of the owner of this template */
public $ownerId;
/**
* @SWG\Property(description="A comma separated list of groups/users with permissions to this template")
* @var string
*/
public $groupsWithPermissions;
/** @var string $xml The XML used to build this template */
private $xml;
/** @var \DOMDocument The DOM Document for this templates XML */
private $document;
/** @var \Xibo\Factory\ModuleTemplateFactory */
private $moduleTemplateFactory;
/**
* Entity constructor.
* @param \Xibo\Storage\StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
* @param \Xibo\Factory\ModuleTemplateFactory $moduleTemplateFactory
* @param string $file The file this template resides in
*/
public function __construct(
StorageServiceInterface $store,
LogServiceInterface $log,
EventDispatcherInterface $dispatcher,
ModuleTemplateFactory $moduleTemplateFactory,
private readonly string $file
) {
$this->setCommonDependencies($store, $log, $dispatcher);
$this->setPermissionsClass('Xibo\Entity\ModuleTemplate');
$this->moduleTemplateFactory = $moduleTemplateFactory;
}
public function getId()
{
return $this->id;
}
public function getOwnerId()
{
return $this->ownerId;
}
public function __clone()
{
$this->id = null;
$this->templateId = null;
}
/**
* Get assets
* @return \Xibo\Widget\Definition\Asset[]
*/
public function getAssets(): array
{
return $this->assets;
}
/**
* Set XML for this Module Template
* @param string $xml
* @return void
*/
public function setXml(string $xml): void
{
$this->xml = $xml;
}
/**
* Get XML for this Module Template
* @return string
*/
public function getXml(): string
{
// for system templates
if ($this->file !== 'database') {
$xml = new \DOMDocument();
// load whole file to document
$xml->loadXML(file_get_contents($this->file));
// go through template tags
foreach ($xml->getElementsByTagName('template') as $templateXml) {
if ($templateXml instanceof \DOMElement) {
foreach ($templateXml->childNodes as $childNode) {
if ($childNode instanceof \DOMElement) {
// match the template to what was requested
// set the xml and return it.
if ($childNode->nodeName === 'id' && $childNode->nodeValue == $this->templateId) {
$this->setXml($xml->saveXML($templateXml));
}
}
}
}
}
}
return $this->xml;
}
/**
* Set Document
* @param \DOMDocument $document
* @return void
*/
public function setDocument(\DOMDocument $document): void
{
$this->document = $document;
}
/**
* Get this templates DOM document
* @return \DOMDocument
*/
public function getDocument(): \DOMDocument
{
if ($this->document === null) {
$this->document = new \DOMDocument();
$this->document->load($this->getXml());
}
return $this->document;
}
/**
* Save
* @return void
*/
public function save(): void
{
if ($this->file === 'database') {
if ($this->id === null) {
$this->add();
} else {
$this->edit();
}
}
}
/**
* Delete
* @return void
*/
public function delete(): void
{
if ($this->file === 'database') {
$this->getStore()->update('DELETE FROM module_templates WHERE id = :id', [
'id' => $this->id
]);
}
}
/**
* Invalidate this module template for any widgets that use it
* @return void
*/
public function invalidate(): void
{
// TODO: can we improve this via the event mechanism instead?
$this->getStore()->update('
UPDATE `widget` SET modifiedDt = :now
WHERE widgetId IN (
SELECT widgetId
FROM widgetoption
WHERE `option` = \'templateId\'
AND `value` = :templateId
)
', [
'now' => time(),
'templateId' => $this->templateId,
]);
}
/**
* Add
* @return void
*/
private function add(): void
{
$this->id = $this->getStore()->insert('
INSERT INTO `module_templates` (`templateId`, `dataType`, `xml`, `ownerId`)
VALUES (:templateId, :dataType, :xml, :ownerId)
', [
'templateId' => $this->templateId,
'dataType' => $this->dataType,
'xml' => $this->xml,
'ownerId' => $this->ownerId,
]);
}
/**
* Edit
* @return void
*/
private function edit(): void
{
$this->getStore()->update('
UPDATE `module_templates` SET
`templateId` = :templateId,
`dataType`= :dataType,
`enabled` = :enabled,
`xml` = :xml,
`ownerId` = :ownerId
WHERE `id` = :id
', [
'templateId' => $this->templateId,
'dataType' => $this->dataType,
'xml' => $this->xml,
'enabled' => $this->isEnabled ? 1 : 0,
'ownerId' => $this->ownerId,
'id' => $this->id,
]);
}
}

533
lib/Entity/Notification.php Normal file
View File

@@ -0,0 +1,533 @@
<?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\Entity;
use Xibo\Factory\DisplayGroupFactory;
use Xibo\Factory\UserGroupFactory;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\InvalidArgumentException;
/**
* Class Notification
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class Notification implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(
* description="The Notifcation ID"
* )
* @var int
*/
public $notificationId;
/**
* @SWG\Property(
* description="Create Date as Unix Timestamp"
* )
* @var int
*/
public $createDt;
/**
* @SWG\Property(
* description="Release Date as Unix Timestamp"
* )
* @var int
*/
public $releaseDt;
/**
* @SWG\Property(
* description="The subject line"
* )
* @var string
*/
public $subject;
/**
* @SWG\Property(
* description="The Notification type"
* )
* @var string
*/
public $type;
/**
* @SWG\Property(
* description="The HTML body of the notification"
* )
* @var string
*/
public $body;
/**
* @SWG\Property(
* description="Should the notification interrupt the CMS UI on navigate/login"
* )
* @var int
*/
public $isInterrupt = 0;
/**
* @SWG\Property(
* description="Flag for system notification"
* )
* @var int
*/
public $isSystem = 0;
/**
* @SWG\Property(
* description="The Owner User Id"
* )
* @var int
*/
public $userId;
/**
* @SWG\Property(
* description="Attachment filename"
* )
* @var string
*/
public $filename;
/**
* @SWG\Property(
* description="Attachment originalFileName"
* )
* @var string
*/
public $originalFileName;
/**
* @SWG\Property(
* description="Additional email addresses to which a saved report will be sent"
* )
* @var string
*/
public $nonusers;
/**
* @SWG\Property(
* description="User Group Notifications associated with this notification"
* )
* @var UserGroup[]
*/
public $userGroups = [];
/**
* @SWG\Property(
* description="Display Groups associated with this notification"
* )
* @var DisplayGroup[]
*/
public $displayGroups = [];
/** @var UserGroupFactory */
private $userGroupFactory;
/** @var DisplayGroupFactory */
private $displayGroupFactory;
/**
* Command constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
* @param UserGroupFactory $userGroupFactory
* @param DisplayGroupFactory $displayGroupFactory
*/
public function __construct($store, $log, $dispatcher, $userGroupFactory, $displayGroupFactory)
{
$this->setCommonDependencies($store, $log, $dispatcher);
$this->userGroupFactory = $userGroupFactory;
$this->displayGroupFactory = $displayGroupFactory;
}
/**
* Get Id
* @return int
*/
public function getId()
{
return $this->notificationId;
}
/**
* Get Owner
*/
public function getOwnerId()
{
return $this->userId;
}
/**
* Add User Group Notification
* @param UserGroup $userGroup
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function assignUserGroup($userGroup)
{
$this->load();
if (!in_array($userGroup, $this->userGroups)) {
$this->userGroups[] = $userGroup;
}
}
/**
* Add Display Group
* @param DisplayGroup $displayGroup
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function assignDisplayGroup($displayGroup)
{
$this->load();
if (!in_array($displayGroup, $this->displayGroups)) {
$this->displayGroups[] = $displayGroup;
}
}
/**
* Validate
* @throws InvalidArgumentException
*/
public function validate()
{
if (empty($this->subject)) {
throw new InvalidArgumentException(__('Please provide a subject'), 'subject');
}
if (empty($this->body)) {
throw new InvalidArgumentException(__('Please provide a body'), 'body');
}
}
/**
* Load
* @param array $options
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function load($options = [])
{
$options = array_merge([
'loadUserGroups' => true,
'loadDisplayGroups' => true,
], $options);
if ($this->loaded || $this->notificationId == null) {
return;
}
// Load the Display Groups and User Group Notifications
if ($options['loadUserGroups']) {
$this->userGroups = $this->userGroupFactory->getByNotificationId($this->notificationId);
}
if ($options['loadDisplayGroups']) {
$this->displayGroups = $this->displayGroupFactory->getByNotificationId($this->notificationId);
}
$this->loaded = true;
}
/**
* Save Notification
* @throws InvalidArgumentException
*/
public function save(): void
{
$this->validate();
$isNewRecord = false;
if ($this->notificationId == null) {
$isNewRecord = true;
$this->add();
} else {
$this->edit();
}
$this->manageAssignments($isNewRecord);
}
/**
* Delete Notification
*/
public function delete()
{
// Remove all links
$this->getStore()->update(
'DELETE FROM `lknotificationuser` WHERE `notificationId` = :notificationId',
['notificationId' => $this->notificationId]
);
$this->getStore()->update(
'DELETE FROM `lknotificationgroup` WHERE `notificationId` = :notificationId',
['notificationId' => $this->notificationId]
);
$this->getStore()->update(
'DELETE FROM `lknotificationdg` WHERE `notificationId` = :notificationId',
['notificationId' => $this->notificationId]
);
// Remove the notification
$this->getStore()->update(
'DELETE FROM `notification` WHERE `notificationId` = :notificationId',
['notificationId' => $this->notificationId]
);
}
/**
* Add to DB
*/
private function add()
{
$this->notificationId = $this->getStore()->insert('
INSERT INTO `notification` (
`subject`,
`body`,
`createDt`,
`releaseDt`,
`isInterrupt`,
`isSystem`,
`userId`,
`filename`,
`originalFileName`,
`nonusers`,
`type`
)
VALUES (
:subject,
:body,
:createDt,
:releaseDt,
:isInterrupt,
:isSystem,
:userId,
:filename,
:originalFileName,
:nonusers,
:type
)
', [
'subject' => $this->subject,
'body' => $this->body,
'createDt' => $this->createDt,
'releaseDt' => $this->releaseDt,
'isInterrupt' => $this->isInterrupt,
'isSystem' => $this->isSystem,
'userId' => $this->userId,
'filename' => $this->filename,
'originalFileName' => $this->originalFileName,
'nonusers' => $this->nonusers,
'type' => $this->type ?? 'custom'
]);
}
/**
* Update in DB
*/
private function edit()
{
$this->getStore()->update('
UPDATE `notification` SET `subject` = :subject,
`body` = :body,
`createDt` = :createDt,
`releaseDt` = :releaseDt,
`isInterrupt` = :isInterrupt,
`isSystem` = :isSystem,
`userId` = :userId,
`filename` = :filename,
`originalFileName` = :originalFileName,
`nonusers` = :nonusers,
`type` = :type
WHERE `notificationId` = :notificationId
', [
'subject' => $this->subject,
'body' => $this->body,
'createDt' => $this->createDt,
'releaseDt' => $this->releaseDt,
'isInterrupt' => $this->isInterrupt,
'isSystem' => $this->isSystem,
'userId' => $this->userId,
'filename' => $this->filename,
'originalFileName' => $this->originalFileName,
'nonusers' => $this->nonusers,
'type' => $this->type ?? 'custom',
'notificationId' => $this->notificationId
]);
}
/**
* Manage assignements in DB
*/
private function manageAssignments(bool $isNewRecord): void
{
$this->linkUserGroups();
// Only unlink if we're not new (otherwise there is no point as we can't have any links yet)
if (!$isNewRecord) {
$this->unlinkUserGroups();
}
$this->linkDisplayGroups();
if (!$isNewRecord) {
$this->unlinkDisplayGroups();
}
$this->manageRealisedUserLinks();
}
/**
* Manage the links in the User notification table
*/
private function manageRealisedUserLinks(bool $isNewRecord = false): void
{
if (!$isNewRecord) {
// Delete links that no longer exist
$this->getStore()->update('
DELETE FROM `lknotificationuser`
WHERE `notificationId` = :notificationId AND `userId` NOT IN (
SELECT `userId`
FROM `lkusergroup`
INNER JOIN `lknotificationgroup`
ON `lknotificationgroup`.groupId = `lkusergroup`.groupId
WHERE `lknotificationgroup`.notificationId = :notificationId2
) AND userId <> 0
', [
'notificationId' => $this->notificationId,
'notificationId2' => $this->notificationId
]);
}
// Pop in new links following from this adjustment
$this->getStore()->update('
INSERT INTO `lknotificationuser` (`notificationId`, `userId`, `read`, `readDt`, `emailDt`)
SELECT DISTINCT :notificationId, `userId`, 0, 0, 0
FROM `lkusergroup`
INNER JOIN `lknotificationgroup`
ON `lknotificationgroup`.groupId = `lkusergroup`.groupId
WHERE `lknotificationgroup`.notificationId = :notificationId2
ON DUPLICATE KEY UPDATE userId = `lknotificationuser`.userId
', [
'notificationId' => $this->notificationId,
'notificationId2' => $this->notificationId
]);
if ($this->isSystem) {
$this->getStore()->insert('
INSERT INTO `lknotificationuser` (`notificationId`, `userId`, `read`, `readDt`, `emailDt`)
VALUES (:notificationId, :userId, 0, 0, 0)
ON DUPLICATE KEY UPDATE userId = `lknotificationuser`.userId
', [
'notificationId' => $this->notificationId,
'userId' => $this->userId
]);
}
}
/**
* Link User Groups
*/
private function linkUserGroups()
{
foreach ($this->userGroups as $userGroup) {
/* @var UserGroup $userGroup */
$this->getStore()->update('INSERT INTO `lknotificationgroup` (notificationId, groupId) VALUES (:notificationId, :userGroupId) ON DUPLICATE KEY UPDATE groupId = groupId', [
'notificationId' => $this->notificationId,
'userGroupId' => $userGroup->groupId
]);
}
}
/**
* Unlink User Groups
*/
private function unlinkUserGroups()
{
// Unlink any userGroup that is NOT in the collection
$params = ['notificationId' => $this->notificationId];
$sql = 'DELETE FROM `lknotificationgroup` WHERE notificationId = :notificationId AND groupId NOT IN (0';
$i = 0;
foreach ($this->userGroups as $userGroup) {
/* @var UserGroup $userGroup */
$i++;
$sql .= ',:userGroupId' . $i;
$params['userGroupId' . $i] = $userGroup->groupId;
}
$sql .= ')';
$this->getStore()->update($sql, $params);
}
/**
* Link Display Groups
*/
private function linkDisplayGroups()
{
foreach ($this->displayGroups as $displayGroup) {
/* @var DisplayGroup $displayGroup */
$this->getStore()->update('INSERT INTO `lknotificationdg` (notificationId, displayGroupId) VALUES (:notificationId, :displayGroupId) ON DUPLICATE KEY UPDATE displayGroupId = displayGroupId', [
'notificationId' => $this->notificationId,
'displayGroupId' => $displayGroup->displayGroupId
]);
}
}
/**
* Unlink Display Groups
*/
private function unlinkDisplayGroups()
{
// Unlink any displayGroup that is NOT in the collection
$params = ['notificationId' => $this->notificationId];
$sql = 'DELETE FROM `lknotificationdg` WHERE notificationId = :notificationId AND displayGroupId NOT IN (0';
$i = 0;
foreach ($this->displayGroups as $displayGroup) {
/* @var DisplayGroup $displayGroup */
$i++;
$sql .= ',:displayGroupId' . $i;
$params['displayGroupId' . $i] = $displayGroup->displayGroupId;
}
$sql .= ')';
$this->getStore()->update($sql, $params);
}
}

209
lib/Entity/Permission.php Normal file
View File

@@ -0,0 +1,209 @@
<?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\Entity;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class Permission
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class Permission implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The ID of this Permission Record")
* @var int
*/
public $permissionId;
/**
* @SWG\Property(description="The Entity ID that this Permission refers to")
* @var int
*/
public $entityId;
/**
* @SWG\Property(description="The User Group ID that this permission refers to")
* @var int
*/
public $groupId;
/**
* @SWG\Property(description="The object ID that this permission refers to")
* @var int
*/
public $objectId;
/**
* @SWG\Property(description="A flag indicating whether the groupId refers to a user specific group")
* @var int
*/
public $isUser;
/**
* @SWG\Property(description="The entity name that this refers to")
* @var string
*/
public $entity;
/**
* @SWG\Property(description="Legacy for when the Object ID is a string")
* @var string
*/
public $objectIdString;
/**
* @SWG\Property(description="The group name that this refers to")
* @var string
*/
public $group;
/**
* @SWG\Property(description="A flag indicating whether view permission is granted")
* @var int
*/
public $view;
/**
* @SWG\Property(description="A flag indicating whether edit permission is granted")
* @var int
*/
public $edit;
/**
* @SWG\Property(description="A flag indicating whether delete permission is granted")
* @var int
*/
public $delete;
/**
* @SWG\Property(description="A flag indicating whether modify permission permission is granted.")
* @var int
*/
public $modifyPermissions;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
public function __clone()
{
$this->permissionId = null;
}
/**
* Save this permission
* @return void
*/
public function save(): void
{
if ($this->permissionId == 0) {
// Check there is something to add
if ($this->view != 0 || $this->edit != 0 || $this->delete != 0) {
$this->getLog()->debug(sprintf(
'save: Adding Permission for %s, %d. GroupId: %d - View = %d, Edit = %d, Delete = %d',
$this->entity,
$this->objectId,
$this->groupId,
$this->view,
$this->edit,
$this->delete,
));
$this->add();
}
} else {
$this->getLog()->debug(sprintf(
'save: Editing Permission for %s, %d. GroupId: %d - View = %d, Edit = %d, Delete = %d',
$this->entity,
$this->objectId,
$this->groupId,
$this->view,
$this->edit,
$this->delete,
));
// If all permissions are set to 0, then we delete the record to tidy up
if ($this->view == 0 && $this->edit == 0 && $this->delete == 0) {
$this->delete();
} else if (count($this->getChangedProperties()) > 0) {
// Something has changed, so run the update.
$this->update();
}
}
}
private function add()
{
$this->permissionId = $this->getStore()->insert('INSERT INTO `permission` (`entityId`, `groupId`, `objectId`, `view`, `edit`, `delete`) VALUES (:entityId, :groupId, :objectId, :view, :edit, :delete)', array(
'entityId' => $this->entityId,
'objectId' => $this->objectId,
'groupId' => $this->groupId,
'view' => $this->view,
'edit' => $this->edit,
'delete' => $this->delete,
));
}
private function update()
{
$this->getStore()->update('UPDATE `permission` SET `view` = :view, `edit` = :edit, `delete` = :delete WHERE `entityId` = :entityId AND `groupId` = :groupId AND `objectId` = :objectId', array(
'entityId' => $this->entityId,
'objectId' => $this->objectId,
'groupId' => $this->groupId,
'view' => $this->view,
'edit' => $this->edit,
'delete' => $this->delete,
));
}
public function delete()
{
$this->getLog()->debug(sprintf('Deleting Permission for %s, %d', $this->entity, $this->objectId));
$this->getStore()->update('DELETE FROM `permission` WHERE entityId = :entityId AND objectId = :objectId AND groupId = :groupId', array(
'entityId' => $this->entityId,
'objectId' => $this->objectId,
'groupId' => $this->groupId
));
}
public function deleteAll()
{
$this->getStore()->update('DELETE FROM `permission` WHERE entityId = :entityId AND objectId = :objectId', array(
'entityId' => $this->entityId,
'objectId' => $this->objectId,
));
}
}

119
lib/Entity/PlayerFault.php Normal file
View File

@@ -0,0 +1,119 @@
<?php
/*
* Copyright (c) 2022 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\Entity;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* @SWG\Definition()
*/
class PlayerFault implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The Fault Id")
* @var int
*/
public $playerFaultId;
/**
* @SWG\Property(description="The Display Id")
* @var int
*/
public $displayId;
/**
* @SWG\Property(description="The Date the error occured")
* @var string
*/
public $incidentDt;
/**
* @SWG\Property(description="The Date the error expires")
* @var string
*/
public $expires;
/**
* @SWG\Property(description="The Code associated with the fault")
* @var int
*/
public $code;
/**
* @SWG\Property(description="The Reason for the fault")
* @var string
*/
public $reason;
/**
* @SWG\Property(description="The Layout Id")
* @var int
*/
public $layoutId;
/**
* @SWG\Property(description="The Region Id")
* @var int
*/
public $regionId;
/**
* @SWG\Property(description="The Schedule Id")
* @var int
*/
public $scheduleId;
/**
* @SWG\Property(description="The Widget Id")
* @var int
*/
public $widgetId;
/**
* @SWG\Property(description="The Media Id")
* @var int
*/
public $mediaId;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
/**
* @return string
*/
public function __toString()
{
return sprintf('Player Fault Id %d, Code %d, Reason %s, Date %s', $this->playerFaultId, $this->code, $this->reason, $this->incidentDt);
}
}

View File

@@ -0,0 +1,466 @@
<?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\Entity;
use Carbon\Carbon;
use Slim\Http\ServerRequest;
use Symfony\Component\Filesystem\Filesystem;
use Xibo\Factory\MediaFactory;
use Xibo\Factory\PlayerVersionFactory;
use Xibo\Helper\DateFormatHelper;
use Xibo\Helper\HttpsDetect;
use Xibo\Service\ConfigServiceInterface;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\DuplicateEntityException;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
/**
* Class PlayerVersion
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class PlayerVersion implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="Version ID")
* @var int
*/
public $versionId;
/**
* @SWG\Property(description="Player type")
* @var string
*/
public $type;
/**
* @SWG\Property(description="Version number")
* @var string
*/
public $version;
/**
* @SWG\Property(description="Code number")
* @var int
*/
public $code;
/**
* @SWG\Property(description="Player version to show")
* @var string
*/
public $playerShowVersion;
/**
* @SWG\Property(description="The Player Version created date")
* @var string
*/
public $createdAt;
/**
* @SWG\Property(description="The Player Version modified date")
* @var string
*/
public $modifiedAt;
/**
* @SWG\Property(description="The name of the user that modified this Player Version last")
* @var string
*/
public $modifiedBy;
/**
* @SWG\Property(description="The Player Version file name")
* @var string
*/
public $fileName;
/**
* @SWG\Property(description="The Player Version file size in bytes")
* @var int
*/
public $size;
/**
* @SWG\Property(description="A MD5 checksum of the stored Player Version file")
* @var string
*/
public $md5;
/**
* @var ConfigServiceInterface
*/
private $config;
/**
* @var PlayerVersionFactory
*/
private $playerVersionFactory;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
* @param ConfigServiceInterface $config
* @param MediaFactory $mediaFactory
* @param PlayerVersionFactory $playerVersionFactory
*/
public function __construct($store, $log, $dispatcher, $config, $playerVersionFactory)
{
$this->setCommonDependencies($store, $log, $dispatcher);
$this->config = $config;
$this->playerVersionFactory = $playerVersionFactory;
}
/**
* Add
*/
private function add()
{
$this->versionId = $this->getStore()->insert('
INSERT INTO `player_software` (`player_type`, `player_version`, `player_code`, `playerShowVersion`,`createdAt`, `modifiedAt`, `modifiedBy`, `fileName`, `size`, `md5`)
VALUES (:type, :version, :code, :playerShowVersion, :createdAt, :modifiedAt, :modifiedBy, :fileName, :size, :md5)
', [
'type' => $this->type,
'version' => $this->version,
'code' => $this->code,
'playerShowVersion' => $this->playerShowVersion,
'createdAt' => Carbon::now()->format(DateFormatHelper::getSystemFormat()),
'modifiedAt' => Carbon::now()->format(DateFormatHelper::getSystemFormat()),
'modifiedBy' => $this->modifiedBy,
'fileName' => $this->fileName,
'size' => $this->size,
'md5' => $this->md5
]);
}
/**
* Edit
*/
private function edit()
{
$sql = '
UPDATE `player_software`
SET `player_version` = :version,
`player_code` = :code,
`playerShowVersion` = :playerShowVersion,
`modifiedAt` = :modifiedAt,
`modifiedBy` = :modifiedBy
WHERE versionId = :versionId
';
$params = [
'version' => $this->version,
'code' => $this->code,
'playerShowVersion' => $this->playerShowVersion,
'modifiedAt' => Carbon::now()->format(DateFormatHelper::getSystemFormat()),
'modifiedBy' => $this->modifiedBy,
'versionId' => $this->versionId
];
$this->getStore()->update($sql, $params);
}
/**
* Delete
*/
public function delete()
{
$this->load();
// delete record
$this->getStore()->update('DELETE FROM `player_software` WHERE `versionId` = :versionId', [
'versionId' => $this->versionId
]);
// Library location
$libraryLocation = $this->config->getSetting('LIBRARY_LOCATION');
// delete file
if (file_exists($libraryLocation . 'playersoftware/' . $this->fileName)) {
unlink($libraryLocation . 'playersoftware/' . $this->fileName);
}
// delete unpacked file
if (is_dir($libraryLocation . 'playersoftware/chromeos/' . $this->versionId)) {
(new Filesystem())->remove($libraryLocation . 'playersoftware/chromeos/' . $this->versionId);
}
}
/**
* @throws \Xibo\Support\Exception\InvalidArgumentException
*/
public function unpack(string $libraryFolder, ServerRequest $request): static
{
// ChromeOS
// Unpack the `.chrome` file as a tar/gz, validate its signature, extract it into the library folder
if ($this->type === 'chromeOS') {
$this->getLog()->debug('add: handling chromeOS upload');
$fullFileName = $libraryFolder . 'playersoftware/' . $this->fileName;
// Check the signature of the file to make sure it comes from a verified source.
try {
$this->getLog()->debug('unpack: loading gnupg to verify the signature');
$gpg = new \gnupg();
$gpg->seterrormode(\gnupg::ERROR_EXCEPTION);
$info = $gpg->verify(
file_get_contents($fullFileName),
false,
);
if ($info === false
|| $info[0]['fingerprint'] !== '10415C506BE63E70BAF1D58BC1EF165A0F880F75'
|| $info[0]['status'] !== 0
|| $info[0]['summary'] !== 0
) {
$this->getLog()->error('unpack: unable to verify GPG. file = ' . $this->fileName);
throw new GeneralException();
}
$this->getLog()->debug('unpack: signature verified');
// Signature verified, move the file, so we can decrypt it.
rename($fullFileName, $libraryFolder . 'playersoftware/' . $this->versionId . '.gpg');
$this->getLog()->debug('unpack: using the shell to decrypt the file');
// Go to the shell to decrypt it.
shell_exec('gpg --decrypt --output ' . $libraryFolder . 'playersoftware/' . $this->versionId
. ' ' . $libraryFolder . 'playersoftware/' . $this->versionId . '.gpg');
// Was this successful?
if (!file_exists($libraryFolder . 'playersoftware/' . $this->versionId)) {
throw new NotFoundException('Not found after decryption');
}
// Rename the GPG file back to its original name.
rename($libraryFolder . 'playersoftware/' . $this->versionId . '.gpg', $fullFileName);
} catch (\Exception $e) {
$this->getLog()->error('unpack: ' . $e->getMessage());
throw new InvalidArgumentException(__('Package file unsupported or invalid'));
}
$zip = new \ZipArchive();
if (!$zip->open($libraryFolder . 'playersoftware/' . $this->versionId)) {
throw new InvalidArgumentException(__('Unable to open ZIP'));
}
// Make sure the ZIP file contains a manifest.json file.
if ($zip->locateName('manifest.json') === false) {
throw new InvalidArgumentException(__('Software package does not contain a manifest'));
}
// Make a folder for this
$folder = $libraryFolder . 'playersoftware/chromeos/' . $this->versionId;
if (is_dir($folder)) {
unlink($folder);
}
mkdir($folder);
// Extract to that folder
$zip->extractTo($folder);
$zip->close();
// Update manifest.json
$manifest = json_decode(file_get_contents($folder . '/manifest.json'), true);
$isXiboThemed = $this->config->getThemeConfig('app_name', 'Xibo') === 'Xibo';
if (!$isXiboThemed) {
$manifest['id'] = $this->config->getThemeConfig('theme_url');
$manifest['name'] = $this->config->getThemeConfig('theme_name');
$manifest['description'] = $this->config->getThemeConfig('theme_title');
$manifest['short_name'] = $this->config->getThemeConfig('app_name') . '-chromeos';
}
// Start URL if we're running in a sub-folder.
$manifest['start_url'] = (new HttpsDetect())->getBaseUrl($request) . '/pwa';
// Update asset URLs
for ($i = 0; $i < count($manifest['icons']); $i++) {
if ($manifest['icons'][$i]['sizes'] == '512x512') {
$manifest['icons'][$i]['src'] = $this->config->uri('img/512x512.png');
} else {
$manifest['icons'][$i]['src'] = $this->config->uri('img/192x192.png');
}
}
file_put_contents($folder . '/manifest.json', json_encode($manifest));
// Unlink our decrypted file
unlink($libraryFolder . 'playersoftware/' . $this->versionId);
}
return $this;
}
public function setActive(): static
{
if ($this->type === 'chromeOS') {
$this->getLog()->debug('setActive: set this version to be the latest');
$chromeLocation = $this->config->getSetting('LIBRARY_LOCATION') . 'playersoftware/chromeos';
if (is_link($chromeLocation . '/latest')) {
unlink($chromeLocation . '/latest');
}
symlink($chromeLocation . '/' . $this->versionId, $chromeLocation . '/latest');
}
return $this;
}
/**
* Load
*/
public function load()
{
if ($this->loaded || $this->versionId == null)
return;
$this->loaded = true;
}
/**
* Save this media
* @param array $options
*/
public function save($options = [])
{
$options = array_merge([
'validate' => true
], $options);
if ($options['validate']) {
$this->validate();
}
if ($this->versionId == null || $this->versionId == 0) {
$this->add();
} else {
$this->edit();
}
}
public function validate() {
// do we already have a file with the same exact name?
$params = [];
$checkSQL = 'SELECT `fileName` FROM `player_software` WHERE `fileName` = :fileName';
if ($this->versionId != null) {
$checkSQL .= ' AND `versionId` <> :versionId ';
$params['versionId'] = $this->versionId;
}
$params['fileName'] = $this->fileName;
$result = $this->getStore()->select($checkSQL, $params);
if (count($result) > 0) {
throw new DuplicateEntityException(__('You already own Player Version file with this name.'));
}
}
/**
* @return $this
*/
public function decorateRecord(): static
{
$version = '';
$code = null;
$type = '';
$explode = explode('_', $this->fileName);
$explodeExt = explode('.', $this->fileName);
$playerShowVersion = $explodeExt[0];
// standard releases
if (count($explode) === 5) {
if (str_contains($explode[4], '.')) {
$explodeExtension = explode('.', $explode[4]);
$explode[4] = $explodeExtension[0];
}
if (str_contains($explode[3], 'v')) {
$version = strtolower(substr(strrchr($explode[3], 'v'), 1, 3)) ;
}
if (str_contains($explode[4], 'R')) {
$code = strtolower(substr(strrchr($explode[4], 'R'), 1, 3)) ;
}
$playerShowVersion = $version . ' Revision ' . $code;
// for DSDevices specific apk
} elseif (count($explode) === 6) {
if (str_contains($explode[5], '.')) {
$explodeExtension = explode('.', $explode[5]);
$explode[5] = $explodeExtension[0];
}
if (str_contains($explode[3], 'v')) {
$version = strtolower(substr(strrchr($explode[3], 'v'), 1, 3)) ;
}
if (str_contains($explode[4], 'R')) {
$code = strtolower(substr(strrchr($explode[4], 'R'), 1, 3)) ;
}
$playerShowVersion = $version . ' Revision ' . $code . ' ' . $explode[5];
// for white labels
} elseif (count($explode) === 3) {
if (str_contains($explode[2], '.')) {
$explodeExtension = explode('.', $explode[2]);
$explode[2] = $explodeExtension[0];
}
if (str_contains($explode[1], 'v')) {
$version = strtolower(substr(strrchr($explode[1], 'v'), 1, 3)) ;
}
if (str_contains($explode[2], 'R')) {
$code = strtolower(substr(strrchr($explode[2], 'R'), 1, 3)) ;
}
$playerShowVersion = $version . ' Revision ' . $code . ' ' . $explode[0];
}
$extension = strtolower(substr(strrchr($this->fileName, '.'), 1));
if ($extension == 'apk') {
$type = 'android';
} else if ($extension == 'ipk') {
$type = 'lg';
} else if ($extension == 'wgt') {
$type = 'sssp';
} else if ($extension == 'chrome') {
$type = 'chromeOS';
}
$this->version = $version;
$this->code = $code;
$this->playerShowVersion = $playerShowVersion;
$this->type = $type;
return $this;
}
}

1190
lib/Entity/Playlist.php Normal file

File diff suppressed because it is too large Load Diff

654
lib/Entity/Region.php Normal file
View File

@@ -0,0 +1,654 @@
<?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\Entity;
use Carbon\Carbon;
use Xibo\Factory\ActionFactory;
use Xibo\Factory\CampaignFactory;
use Xibo\Factory\PermissionFactory;
use Xibo\Factory\PlaylistFactory;
use Xibo\Factory\RegionFactory;
use Xibo\Factory\RegionOptionFactory;
use Xibo\Helper\DateFormatHelper;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
/**
* Class Region
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class Region implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The ID of this region")
* @var int
*/
public $regionId;
/**
* @SWG\Property(description="The Layout ID this region belongs to")
* @var int
*/
public $layoutId;
/**
* @SWG\Property(description="The userId of the User that owns this Region")
* @var int
*/
public $ownerId;
/**
* @SWG\Property(description="Region Type, zone, playlist, frame or canvas")
* @var string
*/
public $type;
/**
* @SWG\Property(description="The name of this Region")
* @var string
*/
public $name;
/**
* @SWG\Property(description="Width of the region")
* @var double
*/
public $width;
/**
* @SWG\Property(description="Height of the Region")
* @var double
*/
public $height;
/**
* @SWG\Property(description="The top coordinate of the Region")
* @var double
*/
public $top;
/**
* @SWG\Property(description="The left coordinate of the Region")
* @var double
*/
public $left;
/**
* @SWG\Property(description="The z-index of the Region to control Layering")
* @var int
*/
public $zIndex;
/**
* @SWG\Property(description="The syncKey of this Region")
* @var string
*/
public $syncKey;
/**
* @SWG\Property(description="An array of Region Options")
* @var RegionOption[]
*/
public $regionOptions = [];
/**
* @SWG\Property(description="An array of Permissions")
* @var Permission[]
*/
public $permissions = [];
/**
* @var int
* @SWG\Property(
* description="A read-only estimate of this Regions's total duration in seconds. This is valid when the parent layout status is 1 or 2."
* )
*/
public $duration;
/**
* @SWG\Property(description="Flag, whether this region is used as an interactive drawer attached to a layout.")
* @var int
*/
public $isDrawer = 0;
/** @var Action[] */
public $actions = [];
/**
* Temporary Id used during import/upgrade
* @var string read only string
*/
public $tempId = null;
/**
* @var Playlist|null
* @SWG\Property(
* description="This Regions Playlist - null if getPlaylist() has not been called."
* )
*/
public $regionPlaylist = null;
//<editor-fold desc="Factories and Dependencies">
/**
* @var RegionFactory
*/
private $regionFactory;
/**
* @var RegionOptionFactory
*/
private $regionOptionFactory;
/**
* @var PermissionFactory
*/
private $permissionFactory;
/**
* @var PlaylistFactory
*/
private $playlistFactory;
/** @var ActionFactory */
private $actionFactory;
/** @var CampaignFactory */
private $campaignFactory;
//</editor-fold>
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param RegionFactory $regionFactory
* @param PermissionFactory $permissionFactory
* @param RegionOptionFactory $regionOptionFactory
* @param PlaylistFactory $playlistFactory
* @param ActionFactory $actionFactory
*/
public function __construct($store, $log, $dispatcher, $regionFactory, $permissionFactory, $regionOptionFactory, $playlistFactory, $actionFactory, $campaignFactory)
{
$this->setCommonDependencies($store, $log, $dispatcher);
$this->regionFactory = $regionFactory;
$this->permissionFactory = $permissionFactory;
$this->regionOptionFactory = $regionOptionFactory;
$this->playlistFactory = $playlistFactory;
$this->actionFactory = $actionFactory;
$this->campaignFactory = $campaignFactory;
}
/**
* Clone this object
*/
public function __clone()
{
// Clear the regionId, clone the Playlist
$this->regionId = null;
$this->hash = null;
$this->permissions = [];
$this->regionPlaylist = clone $this->regionPlaylist;
$this->regionOptions = array_map(function ($object) { return clone $object; }, $this->regionOptions);
// Clone actions
$this->actions = array_map(function ($object) { return clone $object; }, $this->actions);
}
/**
* @return string
*/
public function __toString()
{
return sprintf('Region %s - %d x %d (%d, %d). RegionId = %d, LayoutId = %d. OwnerId = %d. Duration = %d', $this->name, $this->width, $this->height, $this->top, $this->left, $this->regionId, $this->layoutId, $this->ownerId, $this->duration);
}
public function getPermissionFolderId()
{
return $this->getPlaylist()->permissionsFolderId;
}
/**
* @return string
*/
private function hash()
{
return md5($this->name
. $this->type
. $this->ownerId
. $this->width
. $this->height
. $this->top
. $this->left
. $this->regionId
. $this->zIndex
. $this->duration
. $this->syncKey
. json_encode($this->actions));
}
/**
* Get the Id
* @return int
*/
public function getId()
{
return $this->regionId;
}
/**
* Get the OwnerId
* @return int
*/
public function getOwnerId()
{
return $this->ownerId;
}
/**
* Sets the Owner
* @param int $ownerId
* @param bool $cascade Cascade ownership change down to Playlist records
* @throws GeneralException
*/
public function setOwner($ownerId, $cascade = false)
{
$this->load();
$this->ownerId = $ownerId;
if ($cascade) {
$playlist = $this->getPlaylist();
$playlist->setOwner($ownerId);
}
}
/**
* Get Option
* @param string $option
* @return RegionOption
* @throws GeneralException
*/
public function getOption($option)
{
$this->load();
foreach ($this->regionOptions as $regionOption) {
/* @var RegionOption $regionOption */
if ($regionOption->option == $option)
return $regionOption;
}
$this->getLog()->debug('RegionOption ' . $option . ' not found');
throw new NotFoundException(__('Region Option not found'));
}
/**
* Get Region Option Value
* @param string $option
* @param mixed $default
* @return mixed
* @throws GeneralException
*/
public function getOptionValue($option, $default = null)
{
$this->load();
try {
$regionOption = $this->getOption($option);
return $regionOption->value;
}
catch (NotFoundException $e) {
return $default;
}
}
/**
* Set Region Option Value
* @param string $option
* @param mixed $value
* @throws GeneralException
*/
public function setOptionValue($option, $value)
{
try {
$this->getOption($option)->value = $value;
}
catch (NotFoundException $e) {
$this->regionOptions[] = $this->regionOptionFactory->create($this->regionId, $option, $value);
}
}
/**
* @param array $options
* @return Playlist
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function getPlaylist($options = [])
{
if ($this->regionPlaylist === null) {
try {
$this->regionPlaylist = $this->playlistFactory->getByRegionId($this->regionId)->load($options);
} catch (NotFoundException $exception) {
$this->regionPlaylist = $this->playlistFactory->create($this->name, $this->ownerId, $this->regionId);
$this->regionPlaylist->save();
}
}
return $this->regionPlaylist;
}
/**
* Load
* @param array $options
* @throws NotFoundException
*/
public function load($options = [])
{
if ($this->loaded || $this->regionId == 0) {
return;
}
$options = array_merge([
'loadPlaylists' => false,
'loadActions' => true
], $options);
$this->getLog()->debug('Load Region with ' . json_encode($options));
// Load permissions
$this->permissions = $this->permissionFactory->getByObjectId(get_class($this), $this->regionId);
// Get region options
$this->regionOptions = $this->regionOptionFactory->getByRegionId($this->regionId);
// Get Region Actions?
if ($options['loadActions']) {
$this->actions = $this->actionFactory->getBySourceAndSourceId('region', $this->regionId);
}
// Load the Playlist?
if ($options['loadPlaylists']) {
$this->getPlaylist($options);
}
$this->hash = $this->hash();
$this->loaded = true;
}
/**
* Validate the region
* @throws InvalidArgumentException
*/
public function validate()
{
if ($this->width <= 0 || $this->height <= 0) {
throw new InvalidArgumentException(__('The Region dimensions cannot be empty or negative'), 'width/height');
}
// Check zindex is positive
if ($this->zIndex < 0) {
throw new InvalidArgumentException(__('Layer must be 0 or a positive number'), 'zIndex');
}
}
/**
* Save
* @param array $options
* @throws GeneralException
*/
public function save($options = [])
{
$options = array_merge([
'saveRegionOptions' => true,
'validate' => true,
'audit' => true,
'notify' => true
], $options);
$this->getLog()->debug('Saving ' . $this . '. Options = ' . json_encode($options, JSON_PRETTY_PRINT));
if ($options['validate']) {
$this->validate();
}
if ($options['audit']) {
// get the layout specific campaignId
$campaignId = $this->campaignFactory->getCampaignIdForLayout($this->layoutId);
}
if ($this->regionId == null || $this->regionId == 0) {
// We are adding
$this->add();
// Add and save a region specific playlist
if ($this->regionPlaylist === null) {
$this->regionPlaylist = $this->playlistFactory->create($this->name, $this->ownerId, $this->regionId);
} else {
// assert the region id
$this->regionPlaylist->regionId = $this->regionId;
$this->regionPlaylist->setOwner($this->ownerId);
}
// TODO: this is strange, campaignId will only be set if we are configured to Audit.
if (isset($campaignId)) {
$campaign = $this->campaignFactory->getById($campaignId);
$this->regionPlaylist->folderId = $campaign->folderId;
$this->regionPlaylist->permissionsFolderId = $campaign->permissionsFolderId;
}
$this->regionPlaylist->save($options);
// Audit
if ($options['audit']) {
$this->audit(
$this->regionId,
'Added',
[
'regionId' => $this->regionId,
'campaignId' => $campaignId,
'details' => (string)$this,
]
);
}
} else if ($this->hash != $this->hash()) {
$this->update();
// There are 3 cases that we need to consider
// 1 - Saving direct edit of region properties, $this->regionPlaylist will be null, as such we load it from database.
// 2 - Saving whole Layout without changing ownership, $this->regionPlaylist will be populated including widgets property, we do not need to save widgets, load from database,
// 3 - Saving whole Layout and changing the ownership (reassignAll or setOwner on Layout), in this case, we need to save widgets to cascade the ownerId change, don't load from database
// case 3 due to - https://github.com/xibosignage/xibo/issues/2061
$regionPlaylist = $this->playlistFactory->getByRegionId($this->regionId);
if ($this->regionPlaylist == null || $this->ownerId == $regionPlaylist->ownerId) {
$this->regionPlaylist = $regionPlaylist;
}
$this->regionPlaylist->name = $this->name;
if (isset($campaignId)) {
$campaign = $this->campaignFactory->getById($campaignId);
$this->regionPlaylist->folderId = $campaign->folderId;
$this->regionPlaylist->permissionsFolderId = $campaign->permissionsFolderId;
}
$this->regionPlaylist->save($options);
if ($options['audit'] && count($this->getChangedProperties()) > 0) {
$change = $this->getChangedProperties();
$change['campaignId'][] = $campaignId;
$this->audit($this->regionId, 'Saved', $change);
}
}
if ($options['saveRegionOptions']) {
// Save all Options
foreach ($this->regionOptions as $regionOption) {
/* @var RegionOption $regionOption */
// Assert the regionId
$regionOption->regionId = $this->regionId;
$regionOption->save();
}
}
}
/**
* Delete Region
* @param array $options
* @throws GeneralException
*/
public function delete($options = [])
{
$options = array_merge([
'notify' => true
], $options);
// We must ensure everything is loaded before we delete
if ($this->hash == null) {
$this->load();
}
$this->getLog()->debug('Deleting ' . $this);
// Delete Permissions
foreach ($this->permissions as $permission) {
/* @var Permission $permission */
$permission->deleteAll();
}
// Delete all region options
foreach ($this->regionOptions as $regionOption) {
/* @var RegionOption $regionOption */
$regionOption->delete();
}
foreach ($this->actions as $action) {
$action->delete();
}
// Delete any actions that had this Region id as targetId, to avoid orphaned records in action table.
$this->getStore()->update('DELETE FROM `action` WHERE targetId = :targetId', ['targetId' => $this->regionId]);
// Delete the region specific playlist
$this->getPlaylist()->delete(['regionDelete' => true]);
// Delete this region
$this->getStore()->update('DELETE FROM `region` WHERE regionId = :regionId', array('regionId' => $this->regionId));
$this->getLog()->audit('Region', $this->regionId, 'Region Deleted', ['regionId' => $this->regionId, 'layoutId' => $this->layoutId]);
// Notify Layout
if ($options['notify'])
$this->notifyLayout();
}
/**
* Add
*/
private function add()
{
$this->getLog()->debug('Adding region to LayoutId ' . $this->layoutId);
$sql = '
INSERT INTO `region` (`layoutId`, `ownerId`, `name`, `width`, `height`, `top`, `left`, `zIndex`, `isDrawer`, `type`, `syncKey`)
VALUES (:layoutId, :ownerId, :name, :width, :height, :top, :left, :zIndex, :isDrawer, :type, :syncKey)
';
$this->regionId = $this->getStore()->insert($sql, array(
'layoutId' => $this->layoutId,
'ownerId' => $this->ownerId,
'name' => $this->name,
'width' => $this->width,
'height' => $this->height,
'top' => $this->top,
'left' => $this->left,
'zIndex' => $this->zIndex,
'isDrawer' => $this->isDrawer,
'type' => $this->type,
'syncKey' => $this->syncKey
));
}
/**
* Update
*/
private function update()
{
$this->getLog()->debug('Editing ' . $this);
$sql = '
UPDATE `region` SET
`ownerId` = :ownerId,
`name` = :name,
`width` = :width,
`height` = :height,
`top` = :top,
`left` = :left,
`zIndex` = :zIndex,
`duration` = :duration,
`isDrawer` = :isDrawer,
`type` = :type,
`syncKey` = :syncKey
WHERE `regionId` = :regionId
';
$this->getStore()->update($sql, array(
'ownerId' => $this->ownerId,
'name' => $this->name,
'width' => $this->width,
'height' => $this->height,
'top' => $this->top,
'left' => $this->left,
'zIndex' => $this->zIndex,
'duration' => $this->duration,
'isDrawer' => $this->isDrawer,
'type' => $this->type,
'syncKey' => $this->syncKey,
'regionId' => $this->regionId
));
}
/**
* Notify the Layout (set to building)
*/
public function notifyLayout()
{
$this->getStore()->update('
UPDATE `layout` SET `status` = 3, `modifiedDT` = :modifiedDt WHERE layoutId = :layoutId
', [
'layoutId' => $this->layoutId,
'modifiedDt' => Carbon::now()->format(DateFormatHelper::getSystemFormat())
]);
}
}

View File

@@ -0,0 +1,91 @@
<?php
/*
* Copyright (c) 2022 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\Entity;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class RegionOption
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class RegionOption implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The regionId that this Option applies to")
* @var int
*/
public $regionId;
/**
* @SWG\Property(description="The option name")
* @var string
*/
public $option;
/**
* @SWG\Property(description="The option value")
* @var string
*/
public $value;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
/**
* Clone
*/
public function __clone()
{
$this->regionId = null;
}
public function save()
{
$sql = 'INSERT INTO `regionoption` (`regionId`, `option`, `value`) VALUES (:regionId, :option, :value) ON DUPLICATE KEY UPDATE `value` = :value2';
$this->getStore()->insert($sql, array(
'regionId' => $this->regionId,
'option' => $this->option,
'value' => $this->value,
'value2' => $this->value,
));
}
public function delete()
{
$sql = 'DELETE FROM `regionoption` WHERE `regionId` = :regionId AND `option` = :option';
$this->getStore()->update($sql, array('regionId' => $this->regionId, 'option' => $this->option));
}
}

85
lib/Entity/ReportForm.php Normal file
View File

@@ -0,0 +1,85 @@
<?php
/**
* Copyright (C) 2021 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\Entity;
/**
* Class ReportForm
* @package Xibo\Entity
*
*/
class ReportForm
{
/**
* On demand report form template
* @var string
*/
public $template;
/**
* Report name is the string that is defined in .report file
* @var string
*/
public $reportName;
/**
* Report category is the string that is defined in .report file
* @var string|null
*/
public $reportCategory;
/**
* The defaults that is used in report form twig file
* @var array
*/
public $defaults;
/**
* The string that is displayed when we popover the Schedule button
* @var string
*/
public $reportAddBtnTitle;
/**
* ReportForm constructor.
* @param string $template
* @param string $reportName
* @param string $reportCategory
* @param array $defaults
* @param string|null $reportAddBtnTitle
*/
public function __construct(
string $template,
string $reportName,
string $reportCategory,
array $defaults = [],
string $reportAddBtnTitle = 'Schedule'
) {
$this->template = $template;
$this->reportName = $reportName;
$this->reportCategory = $reportCategory;
$this->defaults = $defaults;
$this->reportAddBtnTitle = $reportAddBtnTitle;
return $this;
}
}

111
lib/Entity/ReportResult.php Normal file
View File

@@ -0,0 +1,111 @@
<?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\Entity;
/**
* Class ReportResult
* @package Xibo\Entity
*
*/
class ReportResult implements \JsonSerializable
{
/**
* Number of total records
* @var int
*/
public $recordsTotal;
/**
* Chart data points
* @var array|null
*/
public $chart;
/**
* Error message
* @var null|string
*/
public $error;
/**
* Metadata that is used in the report preview or in the email template
* @var array
*/
public $metadata;
/**
* Datatable Records
* @var array
*/
public $table;
/**
* ReportResult constructor.
* @param array $metadata
* @param array $table
* @param int $recordsTotal
* @param array $chart
* @param null|string $error
*/
public function __construct(
array $metadata = [],
array $table = [],
int $recordsTotal = 0,
array $chart = [],
?string $error = null
) {
$this->metadata = $metadata;
$this->table = $table;
$this->recordsTotal = $recordsTotal;
$this->chart = $chart;
$this->error = $error;
return $this;
}
public function getMetaData(): array
{
return $this->metadata;
}
public function getRows(): array
{
return $this->table;
}
public function countLast(): int
{
return $this->recordsTotal;
}
public function jsonSerialize(): array
{
return [
'metadata' => $this->metadata,
'table' => $this->table,
'recordsTotal' => $this->recordsTotal,
'chart' => $this->chart,
'error' => $this->error
];
}
}

View File

@@ -0,0 +1,211 @@
<?php
/*
* Copyright (c) 2022 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\Entity;
use Respect\Validation\Validator as v;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\InvalidArgumentException;
/**
* Class ReportSchedule
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class ReportSchedule implements \JsonSerializable
{
use EntityTrait;
public static $SCHEDULE_DAILY = '0 0 * * *';
public static $SCHEDULE_WEEKLY = '0 0 * * 1';
public static $SCHEDULE_MONTHLY = '0 0 1 * *';
public static $SCHEDULE_YEARLY = '0 0 1 1 *';
public $reportScheduleId;
public $lastSavedReportId;
public $name;
public $reportName;
public $filterCriteria;
public $schedule;
public $lastRunDt = 0;
public $previousRunDt;
public $createdDt;
public $isActive = 1;
public $fromDt = 0;
public $toDt = 0;
public $message;
/**
* @SWG\Property(description="The username of the User that owns this report schedule")
* @var string
*/
public $owner;
/**
* @SWG\Property(description="The ID of the User that owns this report schedule")
* @var int
*/
public $userId;
/**
* Command constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
/**
* Save
* @param array $options
* @throws InvalidArgumentException
*/
public function save($options = [])
{
$options = array_merge([
'validate' => true
], $options);
if ($options['validate'])
$this->validate();
if ($this->reportScheduleId == null) {
$this->add();
$this->getLog()->debug('Adding report schedule');
}
else
{
$this->edit();
$this->getLog()->debug('Editing a report schedule');
}
}
/**
* Validate
* @throws InvalidArgumentException
*/
public function validate()
{
if (!v::stringType()->notEmpty()->validate($this->name))
throw new InvalidArgumentException(__('Missing name'), 'name');
}
/**
* Delete
*/
public function delete()
{
$this->getStore()->update('DELETE FROM `reportschedule` WHERE `reportScheduleId` = :reportScheduleId', ['reportScheduleId' => $this->reportScheduleId]);
}
private function add()
{
$this->reportScheduleId = $this->getStore()->insert('
INSERT INTO `reportschedule` (`name`, `lastSavedReportId`, `reportName`, `schedule`, `lastRunDt`, `previousRunDt`, `filterCriteria`, `userId`, `isActive`, `fromDt`, `toDt`, `message`, `createdDt`) VALUES
(:name, :lastSavedReportId, :reportName, :schedule, :lastRunDt, :previousRunDt, :filterCriteria, :userId, :isActive, :fromDt, :toDt, :message, :createdDt)
', [
'name' => $this->name,
'lastSavedReportId' => $this->lastSavedReportId,
'reportName' => $this->reportName,
'schedule' => $this->schedule,
'lastRunDt' => $this->lastRunDt,
'previousRunDt' => $this->previousRunDt,
'filterCriteria' => $this->filterCriteria,
'userId' => $this->userId,
'isActive' => $this->isActive,
'fromDt' => $this->fromDt,
'toDt' => $this->toDt,
'message' => $this->message,
'createdDt' => $this->createdDt,
]);
}
/**
* Edit
*/
private function edit()
{
$this->getStore()->update('
UPDATE `reportschedule`
SET `name` = :name,
`lastSavedReportId` = :lastSavedReportId,
`reportName` = :reportName,
`schedule` = :schedule,
`lastRunDt` = :lastRunDt,
`previousRunDt` = :previousRunDt,
`filterCriteria` = :filterCriteria,
`userId` = :userId,
`isActive` = :isActive,
`fromDt` = :fromDt,
`toDt` = :toDt,
`message` = :message,
`createdDt` = :createdDt
WHERE reportScheduleId = :reportScheduleId', [
'reportScheduleId' => $this->reportScheduleId,
'lastSavedReportId' => $this->lastSavedReportId,
'name' => $this->name,
'reportName' => $this->reportName,
'schedule' => $this->schedule,
'lastRunDt' => $this->lastRunDt,
'previousRunDt' => $this->previousRunDt,
'filterCriteria' => $this->filterCriteria,
'userId' => $this->userId,
'isActive' => $this->isActive,
'fromDt' => $this->fromDt,
'toDt' => $this->toDt,
'message' => $this->message,
'createdDt' => $this->createdDt
]);
}
/**
* Get Id
* @return int
*/
public function getId()
{
return $this->reportScheduleId;
}
/**
* Get Owner Id
* @return int
*/
public function getOwnerId()
{
return $this->userId;
}
/**
* Returns the last saved report id
* @return integer
*/
public function getLastSavedReportId()
{
return $this->lastSavedReportId;
}
}

127
lib/Entity/RequiredFile.php Normal file
View File

@@ -0,0 +1,127 @@
<?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\Entity;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\DeadlockException;
/**
* Class RequiredFile
* @package Xibo\Entity
*/
class RequiredFile implements \JsonSerializable
{
public static $TYPE_DEPENDENCY = 'P';
public static $TYPE_LAYOUT = 'L';
public static $TYPE_MEDIA = 'M';
public static $TYPE_WIDGET_DATA = 'D';
use EntityTrait;
public $rfId;
public $displayId;
public $type;
public $itemId;
public $size = 0;
public $path;
public $bytesRequested = 0;
public $complete = 0;
public $released = 1;
public $fileType;
/** @var string The realId of a dependency which we will use to resolve it */
public $realId;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
/**
* Save
* @return $this
*/
public function save($options = [])
{
if ($this->rfId == null) {
$this->add();
} else if ($this->hasPropertyChanged('bytesRequested') || $this->hasPropertyChanged('complete')) {
$this->edit($options);
}
return $this;
}
/**
* Add
*/
private function add()
{
$this->rfId = $this->store->insert('
INSERT INTO `requiredfile` (`displayId`, `type`, `itemId`, `bytesRequested`, `complete`, `size`, `path`, `released`, `fileType`, `realId`)
VALUES (:displayId, :type, :itemId, :bytesRequested, :complete, :size, :path, :released, :fileType, :realId)
', [
'displayId' => $this->displayId,
'type' => $this->type,
'itemId' => $this->itemId,
'bytesRequested' => $this->bytesRequested,
'complete' => $this->complete,
'size' => $this->size,
'path' => $this->path,
'released' => $this->released,
'fileType' => $this->fileType,
'realId' => $this->realId,
]);
}
/**
* Edit
*/
private function edit($options)
{
$options = array_merge([
'connection' => 'default',
'useTransaction' => true,
], $options);
try {
$this->store->updateWithDeadlockLoop('
UPDATE `requiredfile` SET complete = :complete, bytesRequested = :bytesRequested
WHERE rfId = :rfId
', [
'rfId' => $this->rfId,
'bytesRequested' => $this->bytesRequested,
'complete' => $this->complete
], $options['connection'], $options['useTransaction']);
} catch (DeadlockException $deadlockException) {
$this->getLog()->error('Failed to update bytes requested on ' . $this->rfId . ' due to deadlock');
}
}
}

206
lib/Entity/Resolution.php Normal file
View File

@@ -0,0 +1,206 @@
<?php
/*
* Copyright (c) 2022 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\Entity;
use Respect\Validation\Validator as v;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\InvalidArgumentException;
/**
* Class Resolution
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class Resolution implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The ID of this Resolution")
* @var int
*/
public $resolutionId;
/**
* @SWG\Property(description="The resolution name")
* @var string
*/
public $resolution;
/**
* @SWG\Property(description="The display width of the resolution")
* @var double
*/
public $width;
/**
* @SWG\Property(description="The display height of the resolution")
* @var double
*/
public $height;
/**
* @SWG\Property(description="The designer width of the resolution")
* @var double
*/
public $designerWidth;
/**
* @SWG\Property(description="The designer height of the resolution")
* @var double
*/
public $designerHeight;
/**
* @SWG\Property(description="The layout schema version")
* @var int
*/
public $version = 2;
/**
* @SWG\Property(description="A flag indicating whether this resolution is enabled or not")
* @var int
*/
public $enabled = 1;
/**
* @SWG\Property(description="The userId who owns this Resolution")
* @var int
*/
public $userId;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
/**
* @return int
*/
public function getId()
{
return $this->resolutionId;
}
/**
* @return int
*/
public function getOwnerId()
{
// No owner
return $this->userId;
}
/**
* @throws InvalidArgumentException
*/
public function validate()
{
if (!v::stringType()->notEmpty()->validate($this->resolution)) {
throw new InvalidArgumentException(__('Please provide a name'), 'name');
}
if (!v::intType()->notEmpty()->min(1)->validate($this->width)) {
throw new InvalidArgumentException(__('Please provide a width'), 'width');
}
if (!v::intType()->notEmpty()->min(1)->validate($this->height)) {
throw new InvalidArgumentException(__('Please provide a height'), 'height');
}
// Set the designer width and height
$factor = min (800 / $this->width, 800 / $this->height);
$this->designerWidth = round($this->width * $factor);
$this->designerHeight = round($this->height * $factor);
}
/**
* Save
* @param bool|true $validate
* @throws InvalidArgumentException
*/
public function save($validate = true)
{
if ($validate)
$this->validate();
if ($this->resolutionId == null || $this->resolutionId == 0)
$this->add();
else
$this->edit();
$this->getLog()->audit('Resolution', $this->resolutionId, 'Saving', $this->getChangedProperties());
}
public function delete()
{
$this->getStore()->update('DELETE FROM resolution WHERE resolutionID = :resolutionId', ['resolutionId' => $this->resolutionId]);
}
private function add()
{
$this->resolutionId = $this->getStore()->insert('
INSERT INTO `resolution` (resolution, width, height, intended_width, intended_height, version, enabled, `userId`)
VALUES (:resolution, :width, :height, :intended_width, :intended_height, :version, :enabled, :userId)
', [
'resolution' => $this->resolution,
'width' => $this->designerWidth,
'height' => $this->designerHeight,
'intended_width' => $this->width,
'intended_height' => $this->height,
'version' => $this->version,
'enabled' => $this->enabled,
'userId' => $this->userId
]);
}
private function edit()
{
$this->getStore()->update('
UPDATE resolution SET resolution = :resolution,
width = :width,
height = :height,
intended_width = :intended_width,
intended_height = :intended_height,
enabled = :enabled
WHERE resolutionID = :resolutionId
', [
'resolutionId' => $this->resolutionId,
'resolution' => $this->resolution,
'width' => $this->designerWidth,
'height' => $this->designerHeight,
'intended_width' => $this->width,
'intended_height' => $this->height,
'enabled' => $this->enabled
]);
}
}

275
lib/Entity/SavedReport.php Normal file
View File

@@ -0,0 +1,275 @@
<?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\Entity;
use Xibo\Factory\MediaFactory;
use Xibo\Factory\SavedReportFactory;
use Xibo\Service\ConfigServiceInterface;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class SavedReport
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class SavedReport implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="Saved report ID")
* @var int
*/
public $savedReportId;
/**
* @SWG\Property(description="Saved report name As")
* @var string
*/
public $saveAs;
/**
* @SWG\Property(description="Report schedule Id of the saved report")
* @var int
*/
public $reportScheduleId;
/**
* @SWG\Property(description="Report schedule name of the saved report")
* @var string
*/
public $reportScheduleName;
/**
* @SWG\Property(description="Report name")
* @var string
*/
public $reportName;
/**
* @SWG\Property(description="Saved report generated on")
* @var string
*/
public $generatedOn;
/**
* @SWG\Property(description="The username of the User that owns this saved report")
* @var string
*/
public $owner;
/**
* @SWG\Property(description="The ID of the User that owns this saved report")
* @var int
*/
public $userId;
/**
* @SWG\Property(description="Original name of the saved report media file")
* @var string
*/
public $originalFileName;
/**
* @SWG\Property(description="Stored As")
* @var string
*/
public $storedAs;
/**
* @SWG\Property(description="Schema Version")
* @var int
*/
public $schemaVersion = 2;
/**
* @SWG\Property(description="The Saved Report file name")
* @var string
*/
public $fileName;
/**
* @SWG\Property(description="The Saved Report file size in bytes")
* @var int
*/
public $size;
/**
* @SWG\Property(description="A MD5 checksum of the stored Saved Report file")
* @var string
*/
public $md5;
/**
* @var ConfigServiceInterface
*/
private $config;
/**
* @var MediaFactory
*/
private $mediaFactory;
/**
* @var SavedReportFactory
*/
private $savedReportFactory;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
* @param ConfigServiceInterface $config
* @param MediaFactory $mediaFactory
* @param SavedReportFactory $savedReportFactory
*/
public function __construct($store, $log, $dispatcher, $config, $mediaFactory, $savedReportFactory)
{
$this->setCommonDependencies($store, $log, $dispatcher);
$this->config = $config;
$this->mediaFactory = $mediaFactory;
$this->savedReportFactory = $savedReportFactory;
}
/**
* Add
*/
private function add()
{
$this->savedReportId = $this->getStore()->insert('
INSERT INTO `saved_report` (`saveAs`, `reportScheduleId`, `generatedOn`, `userId`, `schemaVersion`, `fileName`, `size`, `md5`)
VALUES (:saveAs, :reportScheduleId, :generatedOn, :userId, :schemaVersion, :fileName, :size, :md5)
', [
'saveAs' => $this->saveAs,
'reportScheduleId' => $this->reportScheduleId,
'generatedOn' => $this->generatedOn,
'userId' => $this->userId,
'schemaVersion' => $this->schemaVersion,
'fileName' => $this->fileName,
'size' => $this->size,
'md5' => $this->md5
]);
}
/**
* Edit
*/
private function edit()
{
$sql = '
UPDATE `saved_report`
SET `saveAs` = :saveAs,
`reportScheduleId` = :reportScheduleId,
`generatedOn` = :generatedOn,
`userId` = :userId,
`schemaVersion` = :schemaVersion
WHERE savedReportId = :savedReportId
';
$params = [
'saveAs' => $this->saveAs,
'reportScheduleId' => $this->reportScheduleId,
'generatedOn' => $this->generatedOn,
'userId' => $this->userId,
'schemaVersion' => $this->schemaVersion,
'savedReportId' => $this->savedReportId,
];
$this->getStore()->update($sql, $params);
}
/**
* Delete
*/
public function delete()
{
$this->load();
$this->getLog()->debug('Delete saved report: '.$this->saveAs.'. Generated on: '.$this->generatedOn);
$this->getStore()->update('DELETE FROM `saved_report` WHERE `savedReportId` = :savedReportId', [
'savedReportId' => $this->savedReportId
]);
// Update last saved report in report schedule
$this->getLog()->debug('Update last saved report in report schedule');
$this->getStore()->update('
UPDATE `reportschedule` SET lastSavedReportId = ( SELECT IFNULL(MAX(`savedReportId`), 0) FROM `saved_report` WHERE `reportScheduleId`= :reportScheduleId)
WHERE `reportScheduleId` = :reportScheduleId',
[
'reportScheduleId' => $this->reportScheduleId
]);
// Library location
$libraryLocation = $this->config->getSetting('LIBRARY_LOCATION');
// delete file
if (file_exists($libraryLocation . 'savedreport/'. $this->fileName)) {
unlink($libraryLocation . 'savedreport/'. $this->fileName);
}
}
/**
* Load
*/
public function load()
{
if ($this->loaded || $this->savedReportId == null)
return;
$this->loaded = true;
}
/**
* Get Id
* @return int
*/
public function getId()
{
return $this->savedReportId;
}
/**
* Get Owner Id
* @return int
*/
public function getOwnerId()
{
return $this->userId;
}
/**
* Save
*/
public function save()
{
if ($this->savedReportId == null || $this->savedReportId == 0)
$this->add();
else
$this->edit();
}
}

2190
lib/Entity/Schedule.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,144 @@
<?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\Entity;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
/**
* Schedule Criteria entity
* @SWG\Definition()
*/
class ScheduleCriteria implements \JsonSerializable
{
use EntityTrait;
public int $id;
public int $eventId;
public string $type;
public string $metric;
public string $condition;
public string $value;
public function __construct(
StorageServiceInterface $store,
LogServiceInterface $logService,
EventDispatcherInterface $dispatcher
) {
$this->setCommonDependencies($store, $logService, $dispatcher);
}
/**
* Basic checks to make sure we have all the fields, etc that we need/
* @return void
* @throws InvalidArgumentException
*/
private function validate(): void
{
if (empty($this->eventId)) {
throw new InvalidArgumentException(__('Criteria must be attached to an event'), 'eventId');
}
if (empty($this->metric)) {
throw new InvalidArgumentException(__('Please select a metric'), 'metric');
}
if (!in_array($this->condition, ['set', 'lt', 'lte', 'eq', 'neq', 'gt', 'gte', 'contains', 'ncontains'])) {
throw new InvalidArgumentException(__('Please enter a valid condition'), 'condition');
}
}
/**
* @throws NotFoundException|InvalidArgumentException
*/
public function save(array $options = []): ScheduleCriteria
{
$options = array_merge([
'validate' => true,
'audit' => true,
], $options);
// Validate?
if ($options['validate']) {
$this->validate();
}
if (empty($this->id)) {
$this->add();
} else {
$this->edit();
}
if ($options['audit']) {
$this->audit($this->id, 'Saved schedule criteria to event', null, true);
}
return $this;
}
/**
* Delete this criteria
* @return void
*/
public function delete(): void
{
$this->getStore()->update('DELETE FROM `schedule_criteria` WHERE `id` = :id', ['id' => $this->id]);
}
private function add(): void
{
$this->id = $this->getStore()->insert('
INSERT INTO `schedule_criteria` (`eventId`, `type`, `metric`, `condition`, `value`)
VALUES (:eventId, :type, :metric, :condition, :value)
', [
'eventId' => $this->eventId,
'type' => $this->type,
'metric' => $this->metric,
'condition' => $this->condition,
'value' => $this->value,
]);
}
private function edit(): void
{
$this->getStore()->update('
UPDATE `schedule_criteria` SET
`eventId` = :eventId,
`type` = :type,
`metric` = :metric,
`condition` = :condition,
`value` = :value
WHERE `id` = :id
', [
'eventId' => $this->eventId,
'type' => $this->type,
'metric' => $this->metric,
'condition' => $this->condition,
'value' => $this->value,
'id' => $this->id,
]);
}
}

View File

@@ -0,0 +1,38 @@
<?php
/*
* Spring Signage Ltd - http://www.springsignage.com
* Copyright (C) 2016 Spring Signage Ltd
* (ScheduleEvent.php)
*/
namespace Xibo\Entity;
/**
* Class ScheduleEvent
* @package Xibo\Entity
*/
class ScheduleEvent
{
public $fromDt;
public $toDt;
/**
* ScheduleEvent constructor.
* @param $fromDt
* @param $toDt
*/
public function __construct($fromDt, $toDt)
{
$this->fromDt = $fromDt;
$this->toDt = $toDt;
}
/**
* @return string
*/
public function __toString()
{
return $this->fromDt . $this->toDt;
}
}

View File

@@ -0,0 +1,91 @@
<?php
/*
* Copyright (c) 2022 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\Entity;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class ScheduleExclusion
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class ScheduleExclusion implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="Excluded Schedule ID")
* @var int
*/
public $scheduleExclusionId;
/**
* @SWG\Property(description="The eventId that this Excluded Schedule applies to")
* @var int
*/
public $eventId;
/**
* @SWG\Property(
* description="A Unix timestamp representing the from date of an excluded recurring event in CMS time."
* )
* @var int
*/
public $fromDt;
/**
* @SWG\Property(
* description="A Unix timestamp representing the to date of an excluded recurring event in CMS time."
* )
* @var int
*/
public $toDt;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
public function save()
{
$this->getStore()->insert('INSERT INTO `scheduleexclusions` (`eventId`, `fromDt`, `toDt`) VALUES (:eventId, :fromDt, :toDt)', [
'eventId' => $this->eventId,
'fromDt' => $this->fromDt,
'toDt' => $this->toDt,
]);
}
public function delete()
{
$this->getStore()->update('DELETE FROM `scheduleexclusions` WHERE `scheduleExclusionId` = :scheduleExclusionId', [
'scheduleExclusionId' => $this->scheduleExclusionId
]);
}
}

View File

@@ -0,0 +1,237 @@
<?php
/*
* Copyright (c) 2022 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\Entity;
use Xibo\Factory\ScheduleReminderFactory;
use Xibo\Service\ConfigServiceInterface;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class ScheduleReminder
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class ScheduleReminder implements \JsonSerializable
{
use EntityTrait;
public static $TYPE_MINUTE = 1;
public static $TYPE_HOUR = 2;
public static $TYPE_DAY = 3;
public static $TYPE_WEEK = 4;
public static $TYPE_MONTH = 5;
public static $OPTION_BEFORE_START = 1;
public static $OPTION_AFTER_START = 2;
public static $OPTION_BEFORE_END = 3;
public static $OPTION_AFTER_END = 4;
public static $MINUTE = 60;
public static $HOUR = 3600;
public static $DAY = 86400;
public static $WEEK = 604800;
public static $MONTH = 30 * 86400;
/**
* @SWG\Property(description="Schedule Reminder ID")
* @var int
*/
public $scheduleReminderId;
/**
* @SWG\Property(description="The event ID of the schedule reminder")
* @var int
*/
public $eventId;
/**
* @SWG\Property(description="An integer number to define minutes, hours etc.")
* @var int
*/
public $value;
/**
* @SWG\Property(description="The type of the reminder (i.e. Minute, Hour, Day, Week, Month)")
* @var int
*/
public $type;
/**
* @SWG\Property(description="The options regarding sending a reminder for an event. (i.e., Before start, After start, Before end, After end)")
* @var int
*/
public $option;
/**
* @SWG\Property(description="Email flag for schedule reminder")
* @var int
*/
public $isEmail;
/**
* @SWG\Property(description="A date that indicates the reminder date")
* @var int
*/
public $reminderDt;
/**
* @SWG\Property(description="Last reminder date a reminder was sent")
* @var int
*/
public $lastReminderDt = 0;
/**
* @var ConfigServiceInterface
*/
private $config;
/**
* @var ScheduleReminderFactory
*/
private $scheduleReminderFactory;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
* @param ConfigServiceInterface $config
* @param ScheduleReminderFactory $scheduleReminderFactory
*/
public function __construct($store, $log, $dispatcher, $config, $scheduleReminderFactory)
{
$this->setCommonDependencies($store, $log, $dispatcher);
$this->config = $config;
$this->scheduleReminderFactory = $scheduleReminderFactory;
}
/**
* Add
*/
private function add()
{
$this->scheduleReminderId = $this->getStore()->insert('
INSERT INTO `schedulereminder` (`eventId`, `value`, `type`, `option`, `reminderDt`, `isEmail`, `lastReminderDt`)
VALUES (:eventId, :value, :type, :option, :reminderDt, :isEmail, :lastReminderDt)
', [
'eventId' => $this->eventId,
'value' => $this->value,
'type' => $this->type,
'option' => $this->option,
'reminderDt' => $this->reminderDt,
'isEmail' => $this->isEmail,
'lastReminderDt' => $this->lastReminderDt,
]);
}
/**
* Edit
*/
private function edit()
{
$sql = '
UPDATE `schedulereminder`
SET `eventId` = :eventId,
`type` = :type,
`value` = :value,
`option` = :option,
`reminderDt` = :reminderDt,
`isEmail` = :isEmail,
`lastReminderDt` = :lastReminderDt
WHERE scheduleReminderId = :scheduleReminderId
';
$params = [
'eventId' => $this->eventId,
'type' => $this->type,
'value' => $this->value,
'option' => $this->option,
'reminderDt' => $this->reminderDt,
'isEmail' => $this->isEmail,
'lastReminderDt' => $this->lastReminderDt,
'scheduleReminderId' => $this->scheduleReminderId,
];
$this->getStore()->update($sql, $params);
}
/**
* Delete
*/
public function delete()
{
$this->load();
$this->getLog()->debug('Delete schedule reminder: '.$this->scheduleReminderId);
$this->getStore()->update('DELETE FROM `schedulereminder` WHERE `scheduleReminderId` = :scheduleReminderId', [
'scheduleReminderId' => $this->scheduleReminderId
]);
}
/**
* Load
*/
public function load()
{
if ($this->loaded || $this->scheduleReminderId == null) {
return;
}
$this->loaded = true;
}
/**
* Get Id
* @return int
*/
public function getId()
{
return $this->scheduleReminderId;
}
/**
* Get Reminder Date
* @return int
*/
public function getReminderDt()
{
return $this->reminderDt;
}
/**
* Save
*/
public function save()
{
if ($this->scheduleReminderId == null || $this->scheduleReminderId == 0) {
$this->add();
} else {
$this->edit();
}
}
}

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\Entity;
use Xibo\Connector\ProviderDetails;
/**
* @SWG\Definition()
*/
class SearchResult implements \JsonSerializable
{
public $title;
public $description;
public $thumbnail;
public $source;
public $type;
public $id;
public $download;
public $fileSize;
public $width;
public $height;
public $orientation;
public $duration;
public $videoThumbnailUrl;
public $tags = [];
public $isFeatured = 0;
/** @var ProviderDetails */
public $provider;
public function jsonSerialize(): array
{
return [
'id' => $this->id,
'source' => $this->source,
'type' => $this->type,
'title' => $this->title,
'description' => $this->description,
'thumbnail' => $this->thumbnail,
'duration' => $this->duration,
'download' => $this->download,
'provider' => $this->provider,
'width' => $this->width,
'height' => $this->height,
'orientation' => $this->orientation,
'fileSize' => $this->fileSize,
'videoThumbnailUrl' => $this->videoThumbnailUrl,
'tags' => $this->tags,
'isFeatured' => $this->isFeatured
];
}
}

View File

@@ -0,0 +1,37 @@
<?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\Entity;
/**
* @SWG\Definition()
*/
class SearchResults implements \JsonSerializable
{
public $data = [];
public function jsonSerialize(): array
{
return [
'data' => $this->data
];
}
}

71
lib/Entity/Session.php Normal file
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\Entity;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class Session
* @package Xibo\Entity
*/
class Session implements \JsonSerializable
{
use EntityTrait;
public $sessionId;
public $userId;
public $userName;
public $isExpired;
public $lastAccessed;
public $remoteAddress;
public $userAgent;
public $expiresAt;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
/**
* @return int the userId
*/
public function getId(): int
{
return $this->userId;
}
/**
* @return int the owner UserId (always 1)
*/
public function getOwnerId(): int
{
return 1;
}
}

21
lib/Entity/Setting.php Normal file
View File

@@ -0,0 +1,21 @@
<?php
/*
* Spring Signage Ltd - http://www.springsignage.com
* Copyright (C) 2015 Spring Signage Ltd
* (Setting.php)
*/
namespace Xibo\Entity;
/**
* Class Setting
* @package Xibo\Entity
*/
class Setting
{
use EntityTrait;
public $setting;
public $value;
}

454
lib/Entity/SyncGroup.php Executable file
View File

@@ -0,0 +1,454 @@
<?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\Entity;
use Carbon\Carbon;
use Respect\Validation\Validator as v;
use Xibo\Factory\DisplayFactory;
use Xibo\Factory\PermissionFactory;
use Xibo\Factory\ScheduleFactory;
use Xibo\Factory\SyncGroupFactory;
use Xibo\Helper\DateFormatHelper;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
use Xibo\Support\Sanitizer\SanitizerInterface;
/**
* @SWG\Definition()
*/
class SyncGroup implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The ID of this Entity")
* @var int
*/
public $syncGroupId;
/**
* @SWG\Property(description="The name of this Entity")
* @var string
*/
public $name;
/**
* @SWG\Property(description="The datetime this entity was created")
* @var string
*/
public $createdDt;
/**
* @SWG\Property(description="The datetime this entity was last modified")
* @var ?string
*/
public $modifiedDt;
/**
* @SWG\Property(description="The ID of the user that last modified this sync group")
* @var int
*/
public $modifiedBy;
/**
* @SWG\Property(description="The name of the user that last modified this sync group")
* @var string
*/
public $modifiedByName;
/**
* @SWG\Property(description="The ID of the owner of this sync group")
* @var int
*/
public $ownerId;
/**
* @SWG\Property(description="The name of the owner of this sync group")
* @var string
*/
public $owner;
/**
* @SWG\Property(description="The publisher port number")
* @var int
*/
public $syncPublisherPort = 9590;
/**
* @SWG\Property(description="The delay (in ms) when displaying the changes in content")
* @var int
*/
public $syncSwitchDelay = 750;
/**
* @SWG\Property(description="The delay (in ms) before unpausing the video on start.")
* @var int
*/
public $syncVideoPauseDelay = 100;
/**
* @SWG\Property(description="The ID of the lead Display for this sync group")
* @var int
*/
public $leadDisplayId;
/**
* @SWG\Property(description="The name of the lead Display for this sync group")
* @var string
*/
public $leadDisplay;
/**
* @SWG\Property(description="The id of the Folder this Sync Group belongs to")
* @var int
*/
public $folderId;
/**
* @SWG\Property(description="The id of the Folder responsible for providing permissions for this Sync Group")
* @var int
*/
public $permissionsFolderId;
private SyncGroupFactory $syncGroupFactory;
private DisplayFactory $displayFactory;
private PermissionFactory $permissionFactory;
private $permissions = [];
private ScheduleFactory $scheduleFactory;
/**
* @param $store
* @param $log
* @param $dispatcher
* @param SyncGroupFactory $syncGroupFactory
* @param DisplayFactory $displayFactory
*/
public function __construct(
$store,
$log,
$dispatcher,
SyncGroupFactory $syncGroupFactory,
DisplayFactory $displayFactory,
PermissionFactory $permissionFactory,
ScheduleFactory $scheduleFactory
) {
$this->setCommonDependencies($store, $log, $dispatcher);
$this->setPermissionsClass('Xibo\Entity\SyncGroup');
$this->syncGroupFactory = $syncGroupFactory;
$this->displayFactory = $displayFactory;
$this->permissionFactory = $permissionFactory;
$this->scheduleFactory = $scheduleFactory;
}
/**
* @return Display[]
* @throws NotFoundException
*/
public function getSyncGroupMembers(): array
{
return $this->displayFactory->getBySyncGroupId($this->syncGroupId);
}
/**
* @return array
*/
public function getGroupMembersForForm(): array
{
return $this->getStore()->select('SELECT `display`.displayId, `display`.display, `display`.syncGroupId, `syncgroup`.leadDisplayId, `displaygroup`.displayGroupId
FROM `display`
INNER JOIN `syncgroup` ON `syncgroup`.syncGroupId = `display`.syncGroupId
INNER JOIN `lkdisplaydg` ON lkdisplaydg.displayid = display.displayId
INNER JOIN `displaygroup` ON displaygroup.displaygroupid = lkdisplaydg.displaygroupid AND `displaygroup`.isDisplaySpecific = 1
WHERE `display`.syncGroupId = :syncGroupId
ORDER BY IF(`syncgroup`.leadDisplayId = `display`.displayId, 0, 1), displayId', [
'syncGroupId' => $this->syncGroupId
]);
}
public function getGroupMembersForEditForm($eventId): array
{
return $this->getStore()->select('SELECT `display`.displayId, `display`.display, `display`.syncGroupId, `syncgroup`.leadDisplayId, `schedule_sync`.layoutId, `displaygroup`.displayGroupId
FROM `display`
INNER JOIN `syncgroup` ON `syncgroup`.syncGroupId = `display`.syncGroupId
INNER JOIN `schedule_sync` ON `schedule_sync`.displayId = `display`.displayId
INNER JOIN `lkdisplaydg` ON lkdisplaydg.displayid = display.displayId
INNER JOIN `displaygroup` ON displaygroup.displaygroupid = lkdisplaydg.displaygroupid AND `displaygroup`.isDisplaySpecific = 1
WHERE `display`.syncGroupId = :syncGroupId AND `schedule_sync`.eventId = :eventId
ORDER BY IF(`syncgroup`.leadDisplayId = `display`.displayId, 0, 1), displayId', [
'syncGroupId' => $this->syncGroupId,
'eventId' => $eventId
]);
}
public function getLayoutIdForDisplay(int $eventId, int $displayId)
{
$layout = $this->getStore()->select('SELECT `schedule_sync`.layoutId
FROM `display`
INNER JOIN `schedule_sync` ON `schedule_sync`.displayId = `display`.displayId
WHERE `display`.syncGroupId = :syncGroupId AND `schedule_sync`.eventId = :eventId AND `schedule_sync`.displayId = :displayId', [
'eventId' => $eventId,
'displayId' => $displayId,
'syncGroupId' => $this->syncGroupId
]);
if (count($layout) <= 0) {
return null;
}
return $layout[0]['layoutId'];
}
/**
* @return int
*/
public function getId(): int
{
return $this->syncGroupId;
}
/**
* @return int
*/
public function getPermissionFolderId(): int
{
return $this->permissionsFolderId;
}
/**
* @return int
*/
public function getOwnerId(): int
{
return $this->ownerId;
}
/**
* Set the owner of this group
* @param $userId
*/
public function setOwner($userId): void
{
$this->ownerId = $userId;
}
/**
* Load the contents for this display group
* @param array $options
* @throws NotFoundException
*/
public function load($options = [])
{
$options = array_merge([], $options);
if ($this->loaded || $this->syncGroupId == null || $this->syncGroupId == 0) {
return;
}
$this->permissions = $this->permissionFactory->getByObjectId(get_class($this), $this->syncGroupId);
// We are loaded
$this->loaded = true;
}
/**
* @param $options
* @return void
* @throws InvalidArgumentException
*/
public function save($options = []): void
{
$options = array_merge([
'validate' => true,
], $options);
if ($options['validate']) {
$this->validate();
}
if (!isset($this->syncGroupId)) {
$this->add();
} else {
$this->edit();
}
}
/**
* @return void
* @throws InvalidArgumentException
*/
public function validate(): void
{
if (!v::stringType()->notEmpty()->validate($this->name)) {
throw new InvalidArgumentException(__('Name cannot be empty'), 'name');
}
if ($this->syncPublisherPort <= 0 || $this->syncPublisherPort === null) {
throw new InvalidArgumentException(__('Sync Publisher Port cannot be empty'), 'syncPublisherPort');
}
if (!isset($this->leadDisplayId) && isset($this->syncGroupId)) {
throw new InvalidArgumentException(__('Please select lead Display for this sync group'), 'leadDisplayId');
}
if ($this->syncSwitchDelay < 0) {
throw new InvalidArgumentException(__('Switch Delay value cannot be negative'), 'syncSwitchDelay');
}
if ($this->syncVideoPauseDelay < 0) {
throw new InvalidArgumentException(__('Video Pause Delay value cannot be negative'), 'syncVideoPauseDelay');
}
}
public function validateForSchedule(SanitizerInterface $sanitizer)
{
foreach ($this->getSyncGroupMembers() as $display) {
if (empty($sanitizer->getInt('layoutId_' . $display->displayId))) {
$this->getLog()->error('Sync Event : Missing Layout for DisplayID ' . $display->displayId);
throw new InvalidArgumentException(
__('Please make sure to select a Layout for all Displays in this Sync Group.')
);
}
}
}
private function add(): void
{
$time = Carbon::now()->format(DateFormatHelper::getSystemFormat());
$this->syncGroupId = $this->getStore()->insert('
INSERT INTO syncgroup (`name`, `createdDt`, `modifiedDt`, `ownerId`, `modifiedBy`, `syncPublisherPort`, `syncSwitchDelay`, `syncVideoPauseDelay`, `folderId`, `permissionsFolderId`)
VALUES (:name, :createdDt, :modifiedDt, :ownerId, :modifiedBy, :syncPublisherPort, :syncSwitchDelay, :syncVideoPauseDelay, :folderId, :permissionsFolderId)
', [
'name' => $this->name,
'createdDt' => $time,
'modifiedDt' => null,
'modifiedBy' => $this->modifiedBy,
'ownerId' => $this->ownerId,
'syncPublisherPort' => $this->syncPublisherPort,
'syncSwitchDelay' => $this->syncSwitchDelay,
'syncVideoPauseDelay' => $this->syncVideoPauseDelay,
'folderId' => $this->folderId,
'permissionsFolderId' => $this->permissionsFolderId
]);
}
private function edit(): void
{
$this->getLog()->debug(sprintf('Updating Sync Group. %s, %d', $this->name, $this->syncGroupId));
$time = Carbon::now()->format(DateFormatHelper::getSystemFormat());
$this->getStore()->update('
UPDATE syncgroup
SET `name` = :name,
`modifiedDt` = :modifiedDt,
`ownerId` = :ownerId,
`modifiedBy` = :modifiedBy,
`syncPublisherPort` = :syncPublisherPort,
`syncSwitchDelay` = :syncSwitchDelay,
`syncVideoPauseDelay` = :syncVideoPauseDelay,
`leadDisplayId` = :leadDisplayId,
`folderId` = :folderId,
`permissionsFolderId` = :permissionsFolderId
WHERE syncGroupId = :syncGroupId
', [
'name' => $this->name,
'modifiedDt' => $time,
'ownerId' => $this->ownerId,
'modifiedBy' => $this->modifiedBy,
'syncPublisherPort' => $this->syncPublisherPort,
'syncSwitchDelay' => $this->syncSwitchDelay,
'syncVideoPauseDelay' => $this->syncVideoPauseDelay,
'leadDisplayId' => $this->leadDisplayId == 0 ? null : $this->leadDisplayId,
'folderId' => $this->folderId,
'permissionsFolderId' => $this->permissionsFolderId,
'syncGroupId' => $this->syncGroupId,
]);
}
/**
* @return void
* @throws NotFoundException
*/
public function delete(): void
{
// unlink Displays from this syncGroup
foreach ($this->getSyncGroupMembers() as $display) {
$this->getStore()->update('UPDATE `display` SET `display`.syncGroupId = NULL WHERE `display`.displayId = :displayId', [
'displayId' => $display->displayId
]);
}
// go through events using this syncGroupId and remove them
// this will also remove links in schedule_sync table
foreach ($this->scheduleFactory->getBySyncGroupId($this->syncGroupId) as $event) {
$event->delete();
}
$this->getStore()->update('DELETE FROM `syncgroup` WHERE `syncgroup`.syncGroupId = :syncGroupId', [
'syncGroupId' => $this->syncGroupId
]);
}
/**
* @param array $displayIds
* @return void
* @throws InvalidArgumentException
* @throws NotFoundException
*/
public function setMembers(array $displayIds): void
{
foreach ($displayIds as $displayId) {
$display = $this->displayFactory->getById($displayId);
if (empty($display->syncGroupId)) {
$this->getStore()->update('UPDATE `display` SET `display`.syncGroupId = :syncGroupId WHERE `display`.displayId = :displayId', [
'syncGroupId' => $this->syncGroupId,
'displayId' => $display->displayId
]);
$display->notify();
} else if (!empty($display->syncGroupId) && $display->syncGroupId !== $this->syncGroupId) {
throw new InvalidArgumentException(
sprintf(
__('Display %s already belongs to a different sync group ID %d'),
$display->display,
$display->syncGroupId
)
);
}
}
}
/**
* @param array $displayIds
* @return void
* @throws NotFoundException
*/
public function unSetMembers(array $displayIds): void
{
foreach ($displayIds as $displayId) {
$display = $this->displayFactory->getById($displayId);
if ($display->syncGroupId === $this->syncGroupId) {
$this->getStore()->update('UPDATE `display` SET `display`.syncGroupId = NULL WHERE `display`.displayId = :displayId', [
'displayId' => $display->displayId
]);
$this->getStore()->update(' DELETE FROM `schedule_sync` WHERE `schedule_sync`.displayId = :displayId
AND `schedule_sync`.eventId IN (SELECT eventId FROM schedule WHERE schedule.syncGroupId = :syncGroupId)', [
'displayId' => $display->displayId,
'syncGroupId' => $this->syncGroupId
]);
}
$display->notify();
}
}
}

236
lib/Entity/Tag.php Normal file
View File

@@ -0,0 +1,236 @@
<?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\Entity;
use Xibo\Factory\TagFactory;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\DuplicateEntityException;
use Xibo\Support\Exception\InvalidArgumentException;
/**
* Class Tag
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class Tag implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The Tag ID")
* @var int
*/
public $tagId;
/**
* @SWG\Property(description="The Tag Name")
* @var string
*/
public $tag;
/**
* @SWG\Property(description="Flag, whether the tag is a system tag")
* @var int
*/
public $isSystem = 0;
/**
* @SWG\Property(description="Flag, whether the tag requires additional values")
* @var int
*/
public $isRequired = 0;
/**
* @SWG\Property(description="An array of options assigned to this Tag")
* @var ?string
*/
public $options;
/** @var TagFactory */
private $tagFactory;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
* @param TagFactory $tagFactory
*/
public function __construct($store, $log, $dispatcher, $tagFactory)
{
$this->setCommonDependencies($store, $log, $dispatcher);
$this->tagFactory = $tagFactory;
}
public function __clone()
{
$this->tagId = null;
}
/**
* @throws InvalidArgumentException
* @throws DuplicateEntityException
*/
public function validate()
{
// Name Validation
if (strlen($this->tag) > 50 || strlen($this->tag) < 1) {
throw new InvalidArgumentException(__("Tag must be between 1 and 50 characters"), 'tag');
}
// Check for duplicates
$duplicates = $this->tagFactory->query(null, [
'tagExact' => $this->tag,
'notTagId' => $this->tagId,
'disableUserCheck' => 1
]);
if (count($duplicates) > 0) {
throw new DuplicateEntityException(sprintf(__("You already own a Tag called '%s'. Please choose another name."), $this->tag));
}
}
/**
* Save
* @param array $options
* @throws DuplicateEntityException
* @throws InvalidArgumentException
*/
public function save($options = [])
{
// Default options
$options = array_merge([
'validate' => true
], $options);
if ($options['validate']) {
$this->validate();
}
// If the tag doesn't exist already - save it
if ($this->tagId == null || $this->tagId == 0) {
$this->add();
} else {
$this->update();
}
$this->getLog()->debug('Saving Tag: %s, %d', $this->tag, $this->tagId);
}
/**
* Add a tag
* @throws \PDOException
*/
private function add()
{
$this->tagId = $this->getStore()->insert('INSERT INTO `tag` (tag, isRequired, options) VALUES (:tag, :isRequired, :options) ON DUPLICATE KEY UPDATE tag = tag', [
'tag' => $this->tag,
'isRequired' => $this->isRequired,
'options' => ($this->options == null) ? null : $this->options
]);
}
/**
* Update a Tag
* @throws \PDOException
*/
private function update()
{
$this->getStore()->update('UPDATE `tag` SET tag = :tag, isRequired = :isRequired, options = :options WHERE tagId = :tagId', [
'tagId' => $this->tagId,
'tag' => $this->tag,
'isRequired' => $this->isRequired,
'options' => ($this->options == null) ? null : $this->options
]);
}
/**
* Delete Tag
*/
public function delete()
{
// Delete the Tag record
$this->getStore()->update('DELETE FROM `tag` WHERE tagId = :tagId', ['tagId' => $this->tagId]);
}
/**
* Is this tag a system tag?
* @return bool
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function isSystemTag()
{
$tag = $this->tagFactory->getById($this->tagId);
return $tag->isSystem === 1;
}
/**
* Removes Tag value from lktagtables
*
* @param array $values An Array of values that should be removed from assignment
*/
public function updateTagValues($values)
{
$this->getLog()->debug('Tag options were changed, the following values need to be removed ' . json_encode($values));
foreach ($values as $value) {
$this->getLog()->debug('removing following value from lktag tables ' . $value);
$this->getStore()->update('UPDATE `lktagcampaign` SET `value` = null WHERE tagId = :tagId AND value = :value',
[
'value' => $value,
'tagId' => $this->tagId
]);
$this->getStore()->update('UPDATE `lktagdisplaygroup` SET `value` = null WHERE tagId = :tagId AND value = :value',
[
'value' => $value,
'tagId' => $this->tagId
]);
$this->getStore()->update('UPDATE `lktaglayout` SET `value` = null WHERE tagId = :tagId AND value = :value',
[
'value' => $value,
'tagId' => $this->tagId
]);
$this->getStore()->update('UPDATE `lktagmedia` SET `value` = null WHERE tagId = :tagId AND value = :value',
[
'value' => $value,
'tagId' => $this->tagId
]);
$this->getStore()->update('UPDATE `lktagplaylist` SET `value` = null WHERE tagId = :tagId AND value = :value',
[
'value' => $value,
'tagId' => $this->tagId
]);
}
}
}

92
lib/Entity/TagLink.php Normal file
View File

@@ -0,0 +1,92 @@
<?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\Entity;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\InvalidArgumentException;
/**
* @SWG\Definition()
*/
class TagLink implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The Tag")
* @var string
*/
public $tag;
/**
* @SWG\Property(description="The Tag ID")
* @var int
*/
public $tagId;
/**
* @SWG\Property(description="The Tag Value")
* @var string
*/
public $value = null;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
public function validateOptions(Tag $tag)
{
if ($tag->options) {
if (!is_array($tag->options)) {
$tag->options = json_decode($tag->options);
}
if (!empty($this->value) && !in_array($this->value, $tag->options)) {
throw new InvalidArgumentException(
sprintf(
__('Provided tag value %s, not found in tag %s options, please select the correct value'),
$this->value,
$this->tag
),
'tagValue'
);
}
}
if (empty($this->value) && $tag->isRequired === 1) {
throw new InvalidArgumentException(
sprintf(
__('Selected Tag %s requires a value, please enter the Tag in %s|Value format or provide Tag value in the dedicated field.'),
$this->tag,
$this->tag
),
'tagValue'
);
}
}
}

180
lib/Entity/TagLinkTrait.php Normal file
View File

@@ -0,0 +1,180 @@
<?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\Entity;
use Xibo\Support\Exception\NotFoundException;
/**
* Class TagLinkTrait
* @package Xibo\Entity
*/
trait TagLinkTrait
{
/**
* Does the Entity have the provided tag?
* @param $searchTag
* @return bool
* @throws NotFoundException
*/
public function hasTag($searchTag)
{
foreach ($this->tags as $tag) {
if ($tag->tag == $searchTag) {
return true;
}
}
return false;
}
/**
* Assign Tag
* @param TagLink $tag
* @return $this
*/
public function assignTag(TagLink $tag)
{
$this->linkTags[] = $tag;
$this->tags[] = $tag;
return $this;
}
/**
* Unassign tag
* @param TagLink $tag
* @return $this
*/
public function unassignTag(TagLink $tag)
{
$this->unlinkTags[] = $tag;
foreach ($this->tags as $key => $currentTag) {
if ($currentTag->tagId === $tag->tagId) {
array_splice($this->tags, $key, 1);
}
}
return $this;
}
/**
* Link
*/
public function linkTagToEntity($table, $column, $entityId, $tagId, $value)
{
$this->getLog()->debug(sprintf('Linking %s %d, to tagId %d', $column, $entityId, $tagId));
$this->getStore()->update('INSERT INTO `' . $table .'` (`tagId`, `'.$column.'`, `value`) VALUES (:tagId, :entityId, :value) ON DUPLICATE KEY UPDATE '.$column.' = :entityId, `value` = :value', [
'tagId' => $tagId,
'entityId' => $entityId,
'value' => $value
]);
}
/**
* Unlink
*/
public function unlinkTagFromEntity($table, $column, $entityId, $tagId)
{
$this->getLog()->debug(sprintf('Unlinking %s %d, from tagId %d', $column, $entityId, $tagId));
$this->getStore()->update('DELETE FROM `'.$table.'` WHERE tagId = :tagId AND `'.$column.'` = :entityId', [
'tagId' => $tagId,
'entityId' => $entityId
]);
}
/**
* Unlink all Tags from Entity
*/
public function unlinkAllTagsFromEntity($table, $column, $entityId)
{
$this->getLog()->debug(sprintf('Unlinking all Tags from %s %d', $column, $entityId));
$this->getStore()->update('DELETE FROM `'.$table.'` WHERE `'.$column.'` = :entityId', [
'entityId' => $entityId
]);
}
/**
* @param TagLink[] $tags
*/
public function updateTagLinks($tags = [])
{
if ($this->tags != $tags) {
$this->unlinkTags = array_udiff($this->tags, $tags, function ($a, $b) {
/* @var TagLink $a */
/* @var TagLink $b */
return $a->tagId - $b->tagId;
});
$this->getLog()->debug(sprintf('Tags to be removed: %s', json_encode($this->unlinkTags)));
// see what we need to add
$this->linkTags = array_udiff($tags, $this->tags, function ($a, $b) {
/* @var TagLink $a */
/* @var TagLink $b */
if ($a->value !== $b->value && $a->tagId === $b->tagId) {
return -1;
} else {
return $a->tagId - $b->tagId;
}
});
// Replace the arrays
$this->tags = $tags;
$this->getLog()->debug(sprintf('Tags to be added: %s', json_encode($this->linkTags)));
$this->getLog()->debug(sprintf('Tags remaining: %s', json_encode($this->tags)));
} else {
$this->getLog()->debug('Tags were not changed');
}
}
/**
* Convert TagLink array into a string for use on forms.
* @return string
*/
public function getTagString()
{
$tagsString = '';
if (empty($this->tags)) {
return $tagsString;
}
$i = 1;
foreach ($this->tags as $tagLink) {
/** @var TagLink $tagLink */
if ($i > 1) {
$tagsString .= ',';
}
$tagsString .= $tagLink->tag . (($tagLink->value) ? '|' . $tagLink->value : '');
$i++;
}
return $tagsString;
}
}

309
lib/Entity/Task.php Normal file
View File

@@ -0,0 +1,309 @@
<?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\Entity;
use Carbon\Carbon;
use Cron\CronExpression;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
/**
* Class Task
* @package Xibo\XTR
*/
class Task implements \JsonSerializable
{
use EntityTrait;
public static $STATUS_RUNNING = 1;
public static $STATUS_IDLE = 2;
public static $STATUS_ERROR = 3;
public static $STATUS_SUCCESS = 4;
public static $STATUS_TIMEOUT = 5;
public $taskId;
public $name;
public $configFile;
public $class;
public $status;
public $pid = 0;
public $options = [];
public $schedule;
public $lastRunDt = 0;
public $lastRunStartDt;
public $lastRunMessage;
public $lastRunStatus;
public $lastRunDuration = 0;
public $lastRunExitCode = 0;
public $isActive;
public $runNow;
/**
* Command constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
/**
* @return \DateTime|string
* @throws \Exception
*/
public function nextRunDate(): \DateTime|string
{
try {
try {
$cron = new CronExpression($this->schedule);
} catch (\Exception $e) {
// Try and take the first X characters instead.
try {
$cron = new CronExpression(substr($this->schedule, 0, strlen($this->schedule) - 2));
} catch (\Exception) {
$this->getLog()->error('nextRunDate: cannot fix CRON syntax error ' . $this->taskId);
throw $e;
}
}
if ($this->lastRunDt == 0) {
return (new \DateTime())->format('U');
}
return $cron->getNextRunDate(\DateTime::createFromFormat('U', $this->lastRunDt))->format('U');
} catch (\Exception) {
$this->getLog()->error('Invalid CRON expression for TaskId ' . $this->taskId);
$this->status = self::$STATUS_ERROR;
return (new \DateTime())->add(new \DateInterval('P1Y'))->format('U');
}
}
/**
* Set class and options
* @throws NotFoundException
*/
public function setClassAndOptions()
{
if ($this->configFile == null)
throw new NotFoundException(__('No config file recorded for task. Please recreate.'));
// Get the class and default set of options from the config file.
if (!file_exists(PROJECT_ROOT . $this->configFile))
throw new NotFoundException(__('Config file not found for Task'));
$config = json_decode(file_get_contents(PROJECT_ROOT . $this->configFile), true);
$this->class = $config['class'];
$this->options = array_merge($config['options'], $this->options);
}
/**
* Validate
* @throws InvalidArgumentException
*/
private function validate(): void
{
// Test the CRON expression
if (empty($this->schedule)) {
throw new InvalidArgumentException(__('Please enter a CRON expression in the Schedule'), 'schedule');
}
try {
$cron = new CronExpression($this->schedule);
$cron->getNextRunDate();
} catch (\Exception $e) {
$this->getLog()->info('run: CRON syntax error for taskId ' . $this->taskId
. ', e: ' . $e->getMessage());
try {
$trimmed = substr($this->schedule, 0, strlen($this->schedule) - 2);
$cron = new CronExpression($trimmed);
$cron->getNextRunDate();
} catch (\Exception) {
throw new InvalidArgumentException(__('Invalid CRON expression in the Schedule'), 'schedule');
}
// Swap to the trimmed (and correct) schedule
$this->schedule = $trimmed;
}
}
/**
* Save
* @throws InvalidArgumentException
*/
public function save(array $options = []): void
{
$options = array_merge([
'validate' => true,
], $options);
if ($options['validate']) {
$this->validate();
}
if ($this->taskId == null) {
$this->add();
} else {
// If we've transitioned from active to inactive, then reset the task status
if ($this->getOriginalValue('isActive') != $this->isActive) {
$this->status = Task::$STATUS_IDLE;
}
$this->edit();
}
}
/**
* Delete
*/
public function delete()
{
$this->getStore()->update('DELETE FROM `task` WHERE `taskId` = :taskId', ['taskId' => $this->taskId]);
}
private function add()
{
$this->taskId = $this->getStore()->insert('
INSERT INTO `task` (`name`, `status`, `configFile`, `class`, `pid`, `options`, `schedule`,
`lastRunDt`, `lastRunMessage`, `lastRunStatus`, `lastRunDuration`, `lastRunExitCode`,
`isActive`, `runNow`) VALUES
(:name, :status, :configFile, :class, :pid, :options, :schedule,
:lastRunDt, :lastRunMessage, :lastRunStatus, :lastRunDuration, :lastRunExitCode,
:isActive, :runNow)
', [
'name' => $this->name,
'status' => $this->status,
'pid' => $this->pid,
'configFile' => $this->configFile,
'class' => $this->class,
'options' => json_encode($this->options),
'schedule' => $this->schedule,
'lastRunDt' => $this->lastRunDt,
'lastRunMessage' => $this->lastRunMessage,
'lastRunStatus' => $this->lastRunStatus,
'lastRunDuration' => $this->lastRunDuration,
'lastRunExitCode' => $this->lastRunExitCode,
'isActive' => $this->isActive,
'runNow' => $this->runNow
]);
}
private function edit()
{
$this->getStore()->update('
UPDATE `task` SET
`name` = :name,
`status` = :status,
`pid` = :pid,
`configFile` = :configFile,
`class` = :class,
`options` = :options,
`schedule` = :schedule,
`lastRunDt` = :lastRunDt,
`lastRunMessage` = :lastRunMessage,
`lastRunStatus` = :lastRunStatus,
`lastRunDuration` = :lastRunDuration,
`lastRunExitCode` = :lastRunExitCode,
`isActive` = :isActive,
`runNow` = :runNow
WHERE `taskId` = :taskId
', [
'taskId' => $this->taskId,
'name' => $this->name,
'status' => $this->status,
'pid' => $this->pid,
'configFile' => $this->configFile,
'class' => $this->class,
'options' => json_encode($this->options),
'schedule' => $this->schedule,
'lastRunDt' => $this->lastRunDt,
'lastRunMessage' => $this->lastRunMessage,
'lastRunStatus' => $this->lastRunStatus,
'lastRunDuration' => $this->lastRunDuration,
'lastRunExitCode' => $this->lastRunExitCode,
'isActive' => $this->isActive,
'runNow' => $this->runNow
]);
}
/**
* Set this task to be started, updating the DB as necessary
* @return $this
*/
public function setStarted(): Task
{
// Set to running
$this->status = \Xibo\Entity\Task::$STATUS_RUNNING;
$this->lastRunStartDt = Carbon::now()->format('U');
$this->pid = getmypid();
$this->store->update('
UPDATE `task` SET `status` = :status, lastRunStartDt = :lastRunStartDt, pid = :pid
WHERE taskId = :taskId
', [
'taskId' => $this->taskId,
'status' => $this->status,
'lastRunStartDt' => $this->lastRunStartDt,
'pid' => $this->pid,
], 'xtr', true, false);
return $this;
}
/**
* Set this task to be finished, updating only the fields we might have changed
* @return $this
*/
public function setFinished(): Task
{
$this->getStore()->update('
UPDATE `task` SET
`status` = :status,
`pid` = :pid,
`lastRunDt` = :lastRunDt,
`lastRunMessage` = :lastRunMessage,
`lastRunStatus` = :lastRunStatus,
`lastRunDuration` = :lastRunDuration,
`lastRunExitCode` = :lastRunExitCode,
`runNow` = :runNow
WHERE `taskId` = :taskId
', [
'taskId' => $this->taskId,
'status' => $this->status,
'pid' => $this->pid,
'lastRunDt' => $this->lastRunDt,
'lastRunMessage' => $this->lastRunMessage,
'lastRunStatus' => $this->lastRunStatus,
'lastRunDuration' => $this->lastRunDuration,
'lastRunExitCode' => $this->lastRunExitCode,
'runNow' => $this->runNow
], 'xtr', true, false);
return $this;
}
}

120
lib/Entity/Transition.php Normal file
View File

@@ -0,0 +1,120 @@
<?php
/*
* Copyright (c) 2022 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\Entity;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\InvalidArgumentException;
/**
* Class Transition
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class Transition
{
use EntityTrait;
/**
* @SWG\Property(description="The transition ID")
* @var int
*/
public $transitionId;
/**
* @SWG\Property(description="The transition name")
* @var string
*/
public $transition;
/**
* @SWG\Property(description="Code for transition")
* @var string
*/
public $code;
/**
* @SWG\Property(description="Flag indicating whether this is a directional transition")
* @var int
*/
public $hasDirection;
/**
* @SWG\Property(description="Flag indicating whether this transition has a duration option")
* @var int
*/
public $hasDuration;
/**
* @SWG\Property(description="Flag indicating whether this transition should be available for IN assignments")
* @var int
*/
public $availableAsIn;
/**
* @SWG\Property(description="Flag indicating whether this transition should be available for OUT assignments")
* @var int
*/
public $availableAsOut;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
public function getId()
{
return $this->transitionId;
}
public function getOwnerId()
{
return 1;
}
/**
* @throws InvalidArgumentException
*/
public function save()
{
if ($this->transitionId == null || $this->transitionId == 0) {
throw new InvalidArgumentException();
}
$this->getStore()->update('
UPDATE `transition` SET AvailableAsIn = :availableAsIn, AvailableAsOut = :availableAsOut WHERE transitionID = :transitionId
', [
'availableAsIn' => $this->availableAsIn,
'availableAsOut' => $this->availableAsOut,
'transitionId' => $this->transitionId
]);
}
}

1577
lib/Entity/User.php Normal file

File diff suppressed because it is too large Load Diff

547
lib/Entity/UserGroup.php Normal file
View File

@@ -0,0 +1,547 @@
<?php
/*
* Copyright (c) 2022 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\Entity;
use Respect\Validation\Validator as v;
use Xibo\Factory\UserFactory;
use Xibo\Factory\UserGroupFactory;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\DuplicateEntityException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
/**
* Class UserGroup
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class UserGroup
{
use EntityTrait;
/**
* @SWG\Property(description="The Group ID")
* @var int
*/
public $groupId;
/**
* @SWG\Property(description="The group name")
* @var string
*/
public $group;
/**
* @SWG\Property(description="A flag indicating whether this is a user specific group or not")
* @var int
*/
public $isUserSpecific = 0;
/**
* @SWG\Property(description="A flag indicating the special everyone group")
* @var int
*/
public $isEveryone = 0;
/**
* @SWG\Property(description="Description of this User Group")
* @var string
*/
public $description;
/**
* @SWG\Property(description="This users library quota in bytes. 0 = unlimited")
* @var int
*/
public $libraryQuota;
/**
* @SWG\Property(description="Does this Group receive system notifications.")
* @var int
*/
public $isSystemNotification = 0;
/**
* @SWG\Property(description="Does this Group receive display notifications.")
* @var int
*/
public $isDisplayNotification = 0;
/**
* @SWG\Property(description="Does this Group receive DataSet notifications.")
* @var int
*/
public $isDataSetNotification = 0;
/**
* @SWG\Property(description="Does this Group receive Layout notifications.")
* @var int
*/
public $isLayoutNotification = 0;
/**
* @SWG\Property(description="Does this Group receive Library notifications.")
* @var int
*/
public $isLibraryNotification = 0;
/**
* @SWG\Property(description="Does this Group receive Report notifications.")
* @var int
*/
public $isReportNotification = 0;
/**
* @SWG\Property(description="Does this Group receive Schedule notifications.")
* @var int
*/
public $isScheduleNotification = 0;
/**
* @SWG\Property(description="Does this Group receive Custom notifications.")
* @var int
*/
public $isCustomNotification = 0;
/**
* @SWG\Property(description="Is this Group shown in the list of choices when onboarding a new user")
* @var int
*/
public $isShownForAddUser = 0;
/**
* @SWG\Property(description="Default Home page for new users")
* @var string
*/
public $defaultHomepageId;
/**
* @SWG\Property(description="Features this User Group has direct access to", @SWG\Items(type="string"))
* @var array
*/
public $features = [];
// Users
private $users = [];
/**
* @var UserGroupFactory
*/
private $userGroupFactory;
/**
* @var UserFactory
*/
private $userFactory;
private $assignedUserIds = [];
private $unassignedUserIds = [];
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
* @param UserGroupFactory $userGroupFactory
* @param UserFactory $userFactory
*/
public function __construct($store, $log, $dispatcher, $userGroupFactory, $userFactory)
{
$this->setCommonDependencies($store, $log, $dispatcher);
$this->userGroupFactory = $userGroupFactory;
$this->userFactory = $userFactory;
}
/**
*
*/
public function __clone()
{
// Clear the groupId
$this->groupId = null;
}
/**
* @return string
*/
public function __toString()
{
return sprintf('ID = %d, Group = %s, IsUserSpecific = %d', $this->groupId, $this->group, $this->isUserSpecific);
}
/**
* Generate a unique hash for this User Group
*/
private function hash()
{
return md5(json_encode($this));
}
/**
* @return int
*/
public function getId()
{
return $this->groupId;
}
/**
* @return int
*/
public function getOwnerId()
{
return 0;
}
/**
* Set the Owner of this Group
* @param User $user
*/
public function setOwner($user)
{
$this->load();
$this->isUserSpecific = 1;
$this->isEveryone = 0;
$this->assignUser($user);
}
/**
* Assign User
* @param User $user
*/
public function assignUser($user)
{
$this->load();
if (!in_array($user, $this->users)) {
$this->users[] = $user;
$this->assignedUserIds[] = $user->userId;
}
}
/**
* Unassign User
* @param User $user
*/
public function unassignUser($user)
{
$this->load();
$this->unassignedUserIds[] = $user->userId;
$this->users = array_udiff($this->users, [$user], function ($a, $b) {
/**
* @var User $a
* @var User $b
*/
return $a->getId() - $b->getId();
});
}
/**
* Validate
*/
public function validate()
{
if (!v::stringType()->length(1, 50)->validate($this->group)) {
throw new InvalidArgumentException(__('User Group Name cannot be empty.') . $this, 'name');
}
if ($this->libraryQuota !== null && !v::intType()->validate($this->libraryQuota)) {
throw new InvalidArgumentException(__('Library Quota must be a whole number.'), 'libraryQuota');
}
try {
$group = $this->userGroupFactory->getByName($this->group, $this->isUserSpecific);
if ($this->groupId == null || $this->groupId != $group->groupId) {
throw new DuplicateEntityException(
__('There is already a group with this name. Please choose another.')
);
}
} catch (NotFoundException $e) {
}
}
/**
* Load this User Group
* @param array $options
*/
public function load($options = [])
{
$options = array_merge([
'loadUsers' => true
], $options);
if ($this->loaded || $this->groupId == 0) {
return;
}
if ($options['loadUsers']) {
// Load all assigned users
$this->users = $this->userFactory->getByGroupId($this->groupId);
}
// Set the hash
$this->hash = $this->hash();
$this->loaded = true;
}
/**
* Save the group
* @param array $options
* @throws DuplicateEntityException
* @throws InvalidArgumentException
*/
public function save($options = [])
{
$options = array_merge([
'validate' => true,
'linkUsers' => true
], $options);
if ($options['validate']) {
$this->validate();
}
if ($this->groupId == null || $this->groupId == 0) {
$this->add();
$this->audit($this->groupId, 'User Group added', ['group' => $this->group]);
} else if ($this->hash() != $this->hash) {
$this->edit();
$this->audit($this->groupId, 'User Group edited');
}
if ($options['linkUsers']) {
$this->linkUsers();
$this->unlinkUsers();
if (count($this->assignedUserIds) > 0) {
$this->audit($this->groupId, 'Users assigned', ['userIds' => implode(',', $this->assignedUserIds)]);
}
if (count($this->unassignedUserIds) > 0) {
$this->audit($this->groupId, 'Users unassigned', ['userIds' => implode(',', $this->unassignedUserIds)]);
}
}
}
/**
* Save features
* @return $this
*/
public function saveFeatures()
{
$this->getStore()->update('
UPDATE `group` SET features = :features WHERE groupId = :groupId
', [
'groupId' => $this->groupId,
'features' => json_encode($this->features)
]);
$this->audit($this->groupId, 'User Group feature access modified', [
'features' => json_encode($this->features)
]);
return $this;
}
/**
* Delete this Group
*/
public function delete()
{
// We must ensure everything is loaded before we delete
if ($this->hash == null) {
$this->load();
}
// Unlink users
$this->removeAssignments();
$this->getStore()->update('DELETE FROM `permission` WHERE groupId = :groupId', ['groupId' => $this->groupId]);
$this->getStore()->update('DELETE FROM `group` WHERE groupId = :groupId', ['groupId' => $this->groupId]);
$this->audit($this->groupId, 'User group deleted.', false);
}
/**
* Remove all assignments
*/
private function removeAssignments()
{
// Delete Notifications
// NB: notifications aren't modelled as child objects because there could be many thousands of notifications on each
// usergroup. We consider the notification to be the parent here and it manages the assignments.
// This does mean that we might end up with an empty notification (not assigned to anything)
$this->getStore()->update('DELETE FROM `lknotificationuser` WHERE `userId` IN (SELECT `userId` FROM `lkusergroup` WHERE `groupId` = :groupId) ', ['groupId' => $this->groupId]);
$this->getStore()->update('DELETE FROM `lknotificationgroup` WHERE `groupId` = :groupId', ['groupId' => $this->groupId]);
// Remove user assignments
$this->users = [];
$this->unlinkUsers();
}
/**
* Add
*/
private function add()
{
$this->groupId = $this->getStore()->insert('
INSERT INTO `group` (
`group`,
`IsUserSpecific`,
`description`,
`libraryQuota`,
`isSystemNotification`,
`isDisplayNotification`,
`isDataSetNotification`,
`isLayoutNotification`,
`isLibraryNotification`,
`isReportNotification`,
`isScheduleNotification`,
`isCustomNotification`,
`isShownForAddUser`,
`defaultHomepageId`
)
VALUES (
:group,
:isUserSpecific,
:description,
:libraryQuota,
:isSystemNotification,
:isDisplayNotification,
:isDataSetNotification,
:isLayoutNotification,
:isLibraryNotification,
:isReportNotification,
:isScheduleNotification,
:isCustomNotification,
:isShownForAddUser,
:defaultHomepageId
)
', [
'group' => $this->group,
'isUserSpecific' => $this->isUserSpecific,
'description' => $this->description,
'libraryQuota' => $this->libraryQuota,
'isSystemNotification' => $this->isSystemNotification,
'isDisplayNotification' => $this->isDisplayNotification,
'isDataSetNotification' => $this->isDataSetNotification,
'isLayoutNotification' => $this->isLayoutNotification,
'isLibraryNotification' => $this->isLibraryNotification,
'isReportNotification' => $this->isReportNotification,
'isScheduleNotification' => $this->isScheduleNotification,
'isCustomNotification' => $this->isCustomNotification,
'isShownForAddUser' => $this->isShownForAddUser,
'defaultHomepageId' => $this->defaultHomepageId
]);
}
/**
* Edit
*/
private function edit()
{
$this->getStore()->update('
UPDATE `group` SET
`group` = :group,
`description` = :description,
libraryQuota = :libraryQuota,
`isSystemNotification` = :isSystemNotification,
`isDisplayNotification` = :isDisplayNotification,
`isDataSetNotification` = :isDataSetNotification,
`isLayoutNotification` = :isLayoutNotification,
`isLibraryNotification` = :isLibraryNotification,
`isReportNotification` = :isReportNotification,
`isScheduleNotification` = :isScheduleNotification,
`isCustomNotification` = :isCustomNotification,
`isShownForAddUser` = :isShownForAddUser,
`defaultHomepageId` = :defaultHomepageId
WHERE groupId = :groupId
', [
'groupId' => $this->groupId,
'group' => $this->group,
'description' => $this->description,
'libraryQuota' => $this->libraryQuota,
'isSystemNotification' => $this->isSystemNotification,
'isDisplayNotification' => $this->isDisplayNotification,
'isDataSetNotification' => $this->isDataSetNotification,
'isLayoutNotification' => $this->isLayoutNotification,
'isLibraryNotification' => $this->isLibraryNotification,
'isReportNotification' => $this->isReportNotification,
'isScheduleNotification' => $this->isScheduleNotification,
'isCustomNotification' => $this->isCustomNotification,
'isShownForAddUser' => $this->isShownForAddUser,
'defaultHomepageId' => $this->defaultHomepageId
]);
}
/**
* Link Users
*/
private function linkUsers()
{
$insert = $this->getStore()->getConnection()->prepare(
'INSERT INTO `lkusergroup` (groupId, userId)
VALUES (:groupId, :userId) ON DUPLICATE KEY UPDATE groupId = groupId'
);
foreach ($this->users as $user) {
/* @var User $user */
$this->getLog()->debug('Linking %s to %s', $user->userName, $this->group);
$insert->execute([
'groupId' => $this->groupId,
'userId' => $user->userId
]);
}
}
/**
* Unlink Users
*/
private function unlinkUsers()
{
$params = ['groupId' => $this->groupId];
$sql = 'DELETE FROM `lkusergroup` WHERE groupId = :groupId AND userId NOT IN (0';
$i = 0;
foreach ($this->users as $user) {
/* @var User $user */
$i++;
$sql .= ',:userId' . $i;
$params['userId' . $i] = $user->userId;
}
$sql .= ')';
$this->getStore()->update($sql, $params);
}
}

View File

@@ -0,0 +1,222 @@
<?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\Entity;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class UserGroupNotification
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class UserNotification implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(
* description="The User Id"
* )
* @var int
*/
public $userId;
/**
* @SWG\Property(
* description="The Notification Id"
* )
* @var int
*/
public $notificationId;
/**
* @SWG\Property(
* description="Release Date expressed as Unix Timestamp"
* )
* @var int
*/
public $releaseDt;
/**
* @SWG\Property(
* description="Read Date expressed as Unix Timestamp"
* )
* @var int
*/
public $readDt;
/**
* @SWG\Property(
* description="Email Date expressed as Unix Timestamp"
* )
* @var int
*/
public $emailDt;
/**
* @SWG\Property(
* description="A flag indicating whether to show as read or not"
* )
* @var int
*/
public $read;
/**
* @SWG\Property(
* description="The subject"
* )
* @var string
*/
public $subject;
/**
* @SWG\Property(
* description="The body"
* )
* @var string
*/
public $body;
/**
* @SWG\Property(
* description="Should the notification interrupt the CMS UI on navigate/login"
* )
* @var int
*/
public $isInterrupt;
/**
* @SWG\Property(
* description="Flag for system notification"
* )
* @var int
*/
public $isSystem;
/**
* @var string
*/
public $email;
/**
* @var string
*/
public $filename;
/**
* @var string
*/
public $originalFileName;
/**
* @var string
*/
public $nonusers;
/**
* @var string
*/
public $type;
/**
* Command constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
/**
* Set Read
* @param int $readDt
*/
public function setRead($readDt)
{
$this->read = 1;
if ($this->readDt == 0) {
$this->readDt = $readDt;
}
}
/**
* Set unread
*/
public function setUnread()
{
$this->read = 0;
}
/**
* Set Emailed
* @param int $emailDt
*/
public function setEmailed($emailDt)
{
if ($this->emailDt == 0) {
$this->emailDt = $emailDt;
}
}
/**
* Save
*/
public function save()
{
$this->getStore()->update('
UPDATE `lknotificationuser`
SET `read` = :read,
`readDt` = :readDt,
`emailDt` = :emailDt
WHERE notificationId = :notificationId
AND userId = :userId
', [
'read' => $this->read,
'readDt' => $this->readDt,
'emailDt' => $this->emailDt,
'notificationId' => $this->notificationId,
'userId' => $this->userId
]);
}
/**
* @return string
*/
public function getTypeForGroup(): string
{
return match ($this->type) {
'dataset' => 'isDataSetNotification',
'display' => 'isDisplayNotification',
'layout' => 'isLayoutNotification',
'library' => 'isLibraryNotification',
'report' => 'isReportNotification',
'schedule' => 'isScheduleNotification',
default => 'isCustomNotification',
};
}
}

99
lib/Entity/UserOption.php Normal file
View File

@@ -0,0 +1,99 @@
<?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\Entity;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class UserOption
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class UserOption implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The userId that this Option applies to")
* @var int
*/
public $userId;
/**
* @SWG\Property(description="The option name")
* @var string
*/
public $option;
/**
* @SWG\Property(description="The option value")
* @var string
*/
public $value;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
$this->excludeProperty('userId');
}
/**
* @inheritdoc
*/
public function __toString()
{
return $this->userId . '-' . $this->option . '-' . md5($this->value);
}
public function save()
{
// when the option is not in the database and default is on, switching option value to off
// would not insert the record, hence the additional condition here xibosignage/xibo#2975
if ($this->hasPropertyChanged('value')
|| ($this->getOriginalValue('value') === null && $this->value !== null)
) {
$this->getStore()->insert('INSERT INTO `useroption` (`userId`, `option`, `value`) VALUES (:userId, :option, :value) ON DUPLICATE KEY UPDATE `value` = :value2', [
'userId' => $this->userId,
'option' => $this->option,
'value' => $this->value,
'value2' => $this->value,
]);
}
}
public function delete()
{
$this->getStore()->update('DELETE FROM `useroption` WHERE `userId` = :userId AND `option` = :option', [
'userId' => $this->userId,
'option' => $this->option
]);
}
}

60
lib/Entity/UserType.php Normal file
View File

@@ -0,0 +1,60 @@
<?php
/*
* Copyright (c) 2022 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\Entity;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class UserType
* @package Xibo\Entity
*
*/
class UserType
{
use EntityTrait;
public $userTypeId;
public $userType;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
public function getId()
{
return $this->userTypeId;
}
public function getOwnerId()
{
return 1;
}
}

1353
lib/Entity/Widget.php Normal file

File diff suppressed because it is too large Load Diff

115
lib/Entity/WidgetAudio.php Normal file
View File

@@ -0,0 +1,115 @@
<?php
/*
* Copyright (c) 2022 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\Entity;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class WidgetAudio
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class WidgetAudio implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The Widget Id")
* @var int
*/
public $widgetId;
/**
* @SWG\Property(description="The Media Id")
* @var int
*/
public $mediaId;
/**
* @SWG\Property(description="The percentage volume")
* @var int
*/
public $volume;
/**
* @SWG\Property(description="Flag indicating whether to loop")
* @var int
*/
public $loop;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
/**
* Get Id
* @return int
*/
public function getId()
{
return $this->mediaId;
}
/**
* Save this widget audio
*/
public function save()
{
$sql = '
INSERT INTO `lkwidgetaudio` (widgetId, mediaId, `volume`, `loop`)
VALUES (:widgetId, :mediaId, :volume, :loop)
ON DUPLICATE KEY UPDATE volume = :volume, `loop` = :loop
';
$this->getStore()->insert($sql, array(
'widgetId' => $this->widgetId,
'mediaId' => $this->mediaId,
'volume' => $this->volume,
'loop' => $this->loop
));
}
/**
* Delete this widget audio
*/
public function delete()
{
$this->getStore()->update('
DELETE FROM `lkwidgetaudio`
WHERE widgetId = :widgetId AND mediaId = :mediaId
', [
'widgetId' => $this->widgetId,
'mediaId' => $this->mediaId
]);
}
}

169
lib/Entity/WidgetData.php Normal file
View File

@@ -0,0 +1,169 @@
<?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\Entity;
use Carbon\Carbon;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Xibo\Helper\DateFormatHelper;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class representing widget data
* @SWG\Definition()
*/
class WidgetData implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(
* property="id",
* description="The ID"
* )
* @var int|null
*/
public ?int $id = null;
/**
* @SWG\Property(
* property="widgetId",
* description="The Widget ID"
* )
* @var int
*/
public int $widgetId;
/**
* @SWG\Property(
* property="data",
* description="Array of data properties depending on the widget data type this data is for",
* @SWG\Items(type="string")
* )
* @var array
*/
public array $data;
/**
* @SWG\Property(
* property="displayOrder",
* description="The Display Order"
* )
* @var int
*/
public int $displayOrder;
/**
* @SWG\Property(
* property="createdDt",
* description="The datetime this entity was created"
* )
* @var ?string
*/
public ?string $createdDt;
/**
* @SWG\Property(
* property="modifiedDt",
* description="The datetime this entity was last modified"
* )
* @var ?string
*/
public ?string $modifiedDt;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct(
StorageServiceInterface $store,
LogServiceInterface $log,
EventDispatcherInterface $dispatcher
) {
$this->setCommonDependencies($store, $log, $dispatcher);
}
/**
* Save this widget data
* @return $this
*/
public function save(): WidgetData
{
if ($this->id === null) {
$this->add();
} else {
$this->modifiedDt = Carbon::now()->format(DateFormatHelper::getSystemFormat());
$this->edit();
}
return $this;
}
/**
* Delete this widget data
* @return void
*/
public function delete(): void
{
$this->getStore()->update('DELETE FROM `widgetdata` WHERE `id` = :id', ['id' => $this->id]);
}
/**
* Add and capture the ID
* @return void
*/
private function add(): void
{
$this->id = $this->getStore()->insert('
INSERT INTO `widgetdata` (`widgetId`, `data`, `displayOrder`, `createdDt`, `modifiedDt`)
VALUES (:widgetId, :data, :displayOrder, :createdDt, :modifiedDt)
', [
'widgetId' => $this->widgetId,
'data' => $this->data == null ? null : json_encode($this->data),
'displayOrder' => $this->displayOrder,
'createdDt' => Carbon::now()->format(DateFormatHelper::getSystemFormat()),
'modifiedDt' => null,
]);
}
/**
* Edit
* @return void
*/
private function edit(): void
{
$this->getStore()->update('
UPDATE `widgetdata` SET
`data` = :data,
`displayOrder` = :displayOrder,
`modifiedDt` = :modifiedDt
WHERE `id` = :id
', [
'id' => $this->id,
'data' => $this->data == null ? null : json_encode($this->data),
'displayOrder' => $this->displayOrder,
'modifiedDt' => $this->modifiedDt,
]);
}
}

111
lib/Entity/WidgetOption.php Normal file
View File

@@ -0,0 +1,111 @@
<?php
/*
* Copyright (c) 2022 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\Entity;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class WidgetOption
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class WidgetOption implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The Widget ID that this Option belongs to")
* @var int
*/
public $widgetId;
/**
* @SWG\Property(description="The option type, either attrib or raw")
* @var string
*/
public $type;
/**
* @SWG\Property(description="The option name")
* @var string
*/
public $option;
/**
* @SWG\Property(description="The option value")
* @var string
*/
public $value;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
public function __clone()
{
$this->widgetId = null;
}
public function __toString()
{
if ($this->type == 'cdata') {
return sprintf('%s WidgetOption %s', $this->type, $this->option);
}
else {
return sprintf('%s WidgetOption %s with value %s', $this->type, $this->option, $this->value);
}
}
public function save()
{
$this->getLog()->debug('Saving ' . $this);
$this->getStore()->insert('
INSERT INTO `widgetoption` (`widgetId`, `type`, `option`, `value`)
VALUES (:widgetId, :type, :option, :value) ON DUPLICATE KEY UPDATE `value` = :value2
', array(
'widgetId' => $this->widgetId,
'type' => $this->type,
'option' => $this->option,
'value' => $this->value,
'value2' => $this->value
));
}
public function delete()
{
$this->getStore()->update('DELETE FROM `widgetoption` WHERE `widgetId` = :widgetId AND `option` = :option', array(
'widgetId' => $this->widgetId, 'option' => $this->option)
);
}
}