init commit

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

View File

@@ -0,0 +1,174 @@
<?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\Service;
use Psr\Log\NullLogger;
use Slim\Views\Twig;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Xibo\Entity\User;
use Xibo\Helper\ApplicationState;
use Xibo\Helper\NullSanitizer;
use Xibo\Helper\NullView;
use Xibo\Helper\SanitizerService;
use Xibo\Storage\PdoStorageService;
class BaseDependenciesService
{
/**
* @var LogServiceInterface
*/
private $log;
/**
* @var SanitizerService
*/
private $sanitizerService;
/**
* @var ApplicationState
*/
private $state;
/**
* @var ConfigServiceInterface
*/
private $configService;
/**
* @var User
*/
private $user;
/**
* @var Twig
*/
private $view;
/**
* @var PdoStorageService
*/
private $storageService;
/** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface */
private $dispatcher;
public function setLogger(LogServiceInterface $logService)
{
$this->log = $logService;
}
/**
* @return LogServiceInterface
*/
public function getLogger()
{
if ($this->log === null) {
$this->log = new NullLogService(new NullLogger());
}
return $this->log;
}
public function setSanitizer(SanitizerService $sanitizerService)
{
$this->sanitizerService = $sanitizerService;
}
public function getSanitizer(): SanitizerService
{
if ($this->sanitizerService === null) {
$this->sanitizerService = new NullSanitizer();
}
return $this->sanitizerService;
}
public function setState(ApplicationState $applicationState)
{
$this->state = $applicationState;
}
public function getState(): ApplicationState
{
return $this->state;
}
public function setUser(User $user)
{
$this->user = $user;
}
public function getUser(): User
{
return $this->user;
}
public function setConfig(ConfigServiceInterface $configService)
{
$this->configService = $configService;
}
public function getConfig() : ConfigServiceInterface
{
return $this->configService;
}
public function setView(Twig $view)
{
$this->view = $view;
}
public function getView() : Twig
{
if ($this->view === null) {
$this->view = new NullView();
}
return $this->view;
}
public function setStore(PdoStorageService $storageService)
{
$this->storageService = $storageService;
}
public function getStore()
{
return $this->storageService;
}
public function setDispatcher(EventDispatcherInterface $dispatcher): BaseDependenciesService
{
$this->dispatcher = $dispatcher;
return $this;
}
public function getDispatcher(): EventDispatcherInterface
{
if ($this->dispatcher === null) {
$this->getLogger()->error('getDispatcher: [base] No dispatcher found, returning an empty one');
$this->dispatcher = new EventDispatcher();
}
return $this->dispatcher;
}
}

View File

@@ -0,0 +1,817 @@
<?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\Service;
use Carbon\Carbon;
use Stash\Interfaces\PoolInterface;
use Xibo\Helper\Environment;
use Xibo\Helper\NatoAlphabet;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\ConfigurationException;
/**
* Class ConfigService
* @package Xibo\Service
*/
class ConfigService implements ConfigServiceInterface
{
/**
* @var StorageServiceInterface
*/
public $store;
/**
* @var PoolInterface
*/
public $pool;
/** @var string Setting Cache Key */
private $settingCacheKey = 'settings';
/** @var bool Has the settings cache been dropped this request? */
private $settingsCacheDropped = false;
/** @var array */
private $settings = null;
/**
* @var string
*/
public $rootUri;
public $envTested = false;
public $envFault = false;
public $envWarning = false;
/**
* Database Config
* @var array
*/
public static $dbConfig = [];
//
// Extra Settings
//
public $middleware = null;
public $logHandlers = null;
public $logProcessors = null;
public $authentication = null;
public $samlSettings = null;
public $casSettings = null;
public $cacheDrivers = null;
public $timeSeriesStore = null;
public $cacheNamespace = 'Xibo';
private $apiKeyPaths = null;
private $connectorSettings = null;
/**
* Theme Specific Config
* @var array
*/
public $themeConfig = [];
/** @var bool Has a theme been loaded? */
private $themeLoaded = false;
/**
* @inheritdoc
*/
public function setDependencies($store, $rootUri)
{
if ($store == null)
throw new \RuntimeException('ConfigService setDependencies called with null store');
if ($rootUri == null)
throw new \RuntimeException('ConfigService setDependencies called with null rootUri');
$this->store = $store;
$this->rootUri = $rootUri;
}
/**
* @inheritdoc
*/
public function setPool($pool)
{
$this->pool = $pool;
}
/**
* Get Cache Pool
* @return \Stash\Interfaces\PoolInterface
*/
private function getPool()
{
return $this->pool;
}
/**
* Get Store
* @return StorageServiceInterface
*/
protected function getStore()
{
if ($this->store == null)
throw new \RuntimeException('Config Service called before setDependencies');
return $this->store;
}
/**
* @inheritdoc
*/
public function getDatabaseConfig()
{
return self::$dbConfig;
}
/**
* Get App Root URI
* @return string
*/
public function rootUri()
{
if ($this->rootUri == null)
throw new \RuntimeException('Config Service called before setDependencies');
return $this->rootUri;
}
/**
* @inheritdoc
*/
public function getCacheDrivers()
{
return $this->cacheDrivers;
}
/**
* @inheritdoc
*/
public function getTimeSeriesStore()
{
return $this->timeSeriesStore;
}
/**
* @inheritdoc
*/
public function getCacheNamespace()
{
return $this->cacheNamespace;
}
/**
* @inheritDoc
*/
public function getConnectorSettings(string $connector): array
{
if ($this->connectorSettings !== null && array_key_exists($connector, $this->connectorSettings)) {
return $this->connectorSettings[$connector];
} else {
return [];
}
}
/**
* Loads the settings from file.
* DO NOT CALL ANY STORE() METHODS IN HERE
* @param \Psr\Container\ContainerInterface $container DI container which may be used in settings.php
* @param string $settings Settings Path
* @return ConfigServiceInterface
*/
public static function Load($container, string $settings)
{
$config = new ConfigService();
// Include the provided settings file.
require ($settings);
// Create a DB config
self::$dbConfig = [
'host' => $dbhost,
'user' => $dbuser,
'password' => $dbpass,
'name' => $dbname,
'ssl' => $dbssl ?? null,
'sslVerify' => $dbsslverify ?? null
];
// Pull in other settings
// Log handlers
if (isset($logHandlers))
$config->logHandlers = $logHandlers;
// Log Processors
if (isset($logProcessors))
$config->logProcessors = $logProcessors;
// Middleware
if (isset($middleware))
$config->middleware = $middleware;
// Authentication
if (isset($authentication))
$config->authentication = $authentication;
// Saml settings
if (isset($samlSettings))
$config->samlSettings = $samlSettings;
// CAS settings
if (isset($casSettings))
$config->casSettings = $casSettings;
// Cache drivers
if (isset($cacheDrivers))
$config->cacheDrivers = $cacheDrivers;
// Time series store settings
if (isset($timeSeriesStore))
$config->timeSeriesStore = $timeSeriesStore;
if (isset($cacheNamespace))
$config->cacheNamespace = $cacheNamespace;
if (isset($apiKeyPaths))
$config->apiKeyPaths = $apiKeyPaths;
// Connector settings
if (isset($connectorSettings)) {
$config->connectorSettings = $connectorSettings;
}
// Set this as the global config
return $config;
}
/**
* Loads the theme
* @param string|null $themeName
* @throws ConfigurationException
*/
public function loadTheme($themeName = null): void
{
global $config;
// What is the currently selected theme?
$globalTheme = ($themeName == null)
? basename($this->getSetting('GLOBAL_THEME_NAME', 'default'))
: $themeName;
// Is this theme valid?
$systemTheme = (is_dir(PROJECT_ROOT . '/web/theme/' . $globalTheme)
&& file_exists(PROJECT_ROOT . '/web/theme/' . $globalTheme . '/config.php'));
$customTheme = (is_dir(PROJECT_ROOT . '/web/theme/custom/' . $globalTheme)
&& file_exists(PROJECT_ROOT . '/web/theme/custom/' . $globalTheme . '/config.php'));
if ($systemTheme) {
require(PROJECT_ROOT . '/web/theme/' . $globalTheme . '/config.php');
$themeFolder = 'theme/' . $globalTheme . '/';
} elseif ($customTheme) {
require(PROJECT_ROOT . '/web/theme/custom/' . $globalTheme . '/config.php');
$themeFolder = 'theme/custom/' . $globalTheme . '/';
} else {
throw new ConfigurationException(__('The theme "%s" does not exist', $globalTheme));
}
$this->themeLoaded = true;
$this->themeConfig = $config;
$this->themeConfig['themeCode'] = $globalTheme;
$this->themeConfig['themeFolder'] = $themeFolder;
}
/**
* Get Theme Specific Settings
* @param null $settingName
* @param null $default
* @return mixed|array|string
*/
public function getThemeConfig($settingName = null, $default = null)
{
if ($settingName == null)
return $this->themeConfig;
if (isset($this->themeConfig[$settingName]))
return $this->themeConfig[$settingName];
else
return $default;
}
/**
* Get theme URI
* @param string $uri
* @param bool $local
* @return string
*/
public function uri($uri, $local = false)
{
$rootUri = ($local) ? PROJECT_ROOT . '/web/' : $this->rootUri();
if (!$this->themeLoaded)
return $rootUri . 'theme/default/' . $uri;
// Serve the appropriate theme file
if (is_dir(PROJECT_ROOT . '/web/' . $this->themeConfig['themeFolder'] . $uri)) {
return $rootUri . $this->themeConfig['themeFolder'] . $uri;
}
else if (file_exists(PROJECT_ROOT . '/web/' . $this->themeConfig['themeFolder'] . $uri)) {
return $rootUri . $this->themeConfig['themeFolder'] . $uri;
}
else {
return $rootUri . 'theme/default/' . $uri;
}
}
/**
* Check a theme file exists
* @param string $uri
* @return string
*/
public function fileExists($uri)
{
// Serve the appropriate file
return file_exists(PROJECT_ROOT . '/web/' . $uri);
}
/**
* Check a theme file exists
* @param string $uri
* @return string
*/
public function themeFileExists($uri)
{
if (!$this->themeLoaded)
return file_exists(PROJECT_ROOT . '/web/theme/default/' . $uri);
// Serve the appropriate theme file
if (file_exists(PROJECT_ROOT . '/web/' . $this->themeConfig['themeFolder'] . $uri)) {
return true;
} else {
return file_exists(PROJECT_ROOT . '/web/theme/default/' . $uri);
}
}
/**
* @return array|mixed|null
*/
private function loadSettings()
{
$item = null;
if ($this->settings === null) {
// We need to load in our settings
if ($this->getPool() !== null) {
// Try the cache
$item = $this->getPool()->getItem($this->settingCacheKey);
$data = $item->get();
if ($item->isHit()) {
$this->settings = $data;
}
}
// Are we still null?
if ($this->settings === null) {
// Load from the database
$this->settings = $this->getStore()->select('SELECT `setting`, `value`, `userSee`, `userChange` FROM `setting`', []);
}
}
// We should have our settings by now, so cache them if we can/need to
if ($item !== null && $item->isMiss()) {
// See about caching these settings - dependent on whether we're logging or not
$cacheExpiry = 60 * 5;
foreach ($this->settings as $setting) {
if ($setting['setting'] == 'ELEVATE_LOG_UNTIL' && intval($setting['value']) > Carbon::now()->format('U')) {
$cacheExpiry = intval($setting['value']);
break;
}
}
$item->set($this->settings);
$item->expiresAfter($cacheExpiry);
$this->getPool()->saveDeferred($item);
}
return $this->settings;
}
/** @inheritdoc */
public function getSettings()
{
$settings = $this->loadSettings();
$parsed = [];
// Go through each setting and create a key/value pair
foreach ($settings as $setting) {
$parsed[$setting['setting']] = $setting['value'];
}
return $parsed;
}
/** @inheritdoc */
public function getSetting($setting, $default = NULL, $full = false)
{
$settings = $this->loadSettings();
if ($full) {
foreach ($settings as $item) {
if ($item['setting'] == $setting) {
return $item;
}
}
return [
'setting' => $setting,
'value' => $default,
'userSee' => 1,
'userChange' => 1
];
} else {
$settings = $this->getSettings();
return (isset($settings[$setting])) ? $settings[$setting] : $default;
}
}
/** @inheritdoc */
public function changeSetting($setting, $value, $userChange = 0)
{
$settings = $this->getSettings();
// Update in memory cache
foreach ($this->settings as $item) {
if ($item['setting'] == $setting) {
$item['value'] = $value;
break;
}
}
if (isset($settings[$setting])) {
// We've already got this setting recorded, update it for
// Update in database
$this->getStore()->update('UPDATE `setting` SET `value` = :value WHERE `setting` = :setting', [
'setting' => $setting,
'value' => ($value === null) ? '' : $value
]);
} else {
// A new setting we've not seen before.
// record it in the settings table.
$this->getStore()->insert('
INSERT INTO `setting` (`value`, setting, `userChange`) VALUES (:value, :setting, :userChange);', [
'setting' => $setting,
'value' => ($value === null) ? '' : $value,
'userChange' => $userChange
]);
}
// Drop the cache if we've not already done so this time around
if (!$this->settingsCacheDropped && $this->getPool() !== null) {
$this->getPool()->deleteItem($this->settingCacheKey);
$this->settingsCacheDropped = true;
$this->settings = null;
}
}
/**
* Is the provided setting visible
* @param string $setting
* @return bool
*/
public function isSettingVisible($setting)
{
return $this->getSetting($setting, null, true)['userSee'] == 1;
}
/**
* Is the provided setting editable
* @param string $setting
* @return bool
*/
public function isSettingEditable($setting)
{
$item = $this->getSetting($setting, null, true);
return $item['userSee'] == 1 && $item['userChange'] == 1;
}
/**
* Should the host be considered a proxy exception
* @param $host
* @return bool
*/
public function isProxyException($host)
{
$proxyExceptions = $this->getSetting('PROXY_EXCEPTIONS');
// If empty, cannot be an exception
if (empty($proxyExceptions))
return false;
// Simple test
if (stripos($host, $proxyExceptions) !== false)
return true;
// Host test
$parsedHost = parse_url($host, PHP_URL_HOST);
// Kick out extremely malformed hosts
if ($parsedHost === false)
return false;
// Go through each exception and test against the host
foreach (explode(',', $proxyExceptions) as $proxyException) {
if (stripos($parsedHost, $proxyException) !== false)
return true;
}
// If we've got here without returning, then we aren't an exception
return false;
}
/**
* Get Proxy Configuration
* @param array $httpOptions
* @return array
*/
public function getGuzzleProxy($httpOptions = [])
{
// Proxy support
if ($this->getSetting('PROXY_HOST') != '') {
$proxy = $this->getSetting('PROXY_HOST') . ':' . $this->getSetting('PROXY_PORT');
if ($this->getSetting('PROXY_AUTH') != '') {
$scheme = explode('://', $proxy);
$proxy = $scheme[0] . '://' . $this->getSetting('PROXY_AUTH') . '@' . $scheme[1];
}
$httpOptions['proxy'] = [
'http' => $proxy,
'https' => $proxy
];
if ($this->getSetting('PROXY_EXCEPTIONS') != '') {
$httpOptions['proxy']['no'] = explode(',', $this->getSetting('PROXY_EXCEPTIONS'));
}
}
// Global timeout
// All outbound HTTP should have a timeout as they tie up a PHP process while the request completes (if
// triggered from an incoming request)
// https://github.com/xibosignage/xibo/issues/2631
if (!array_key_exists('timeout', $httpOptions)) {
$httpOptions['timeout'] = 20;
}
if (!array_key_exists('connect_timeout', $httpOptions)) {
$httpOptions['connect_timeout'] = 5;
}
return $httpOptions;
}
/**
* @inheritDoc
*/
public function getApiKeyDetails()
{
if ($this->apiKeyPaths == null) {
// We load the defaults
$libraryLocation = $this->getSetting('LIBRARY_LOCATION');
// We use the defaults
$this->apiKeyPaths = [
'publicKeyPath' => $libraryLocation . 'certs/public.key',
'privateKeyPath' => $libraryLocation . 'certs/private.key',
'encryptionKey' => file_get_contents($libraryLocation . 'certs/encryption.key')
];
}
return $this->apiKeyPaths;
}
private function testItem(&$results, $item, $result, $advice, $fault = true)
{
// 1=OK, 0=Failure, 2=Warning
$status = ($result) ? 1 : (($fault) ? 0 : 2);
// Set fault flag
if (!$result && $fault)
$this->envFault = true;
// Set warning flag
if (!$result && !$fault)
$this->envWarning = true;
$results[] = [
'item' => $item,
'status' => $status,
'advice' => $advice
];
}
/**
* Checks the Environment and Determines if it is suitable
* @return array
*/
public function checkEnvironment()
{
$rows = array();
$this->testItem($rows, __('PHP Version'),
Environment::checkPHP(),
sprintf(__("PHP version %s or later required."), Environment::$VERSION_REQUIRED) . ' Detected ' . phpversion()
);
$this->testItem($rows, __('Cache File System Permissions'),
Environment::checkCacheFileSystemPermissions(),
__('Write permissions are required for cache/')
);
$this->testItem($rows, __('MySQL database (PDO MySql)'),
Environment::checkPDO(),
__('PDO support with MySQL drivers must be enabled in PHP.')
);
$this->testItem($rows, __('JSON Extension'),
Environment::checkJson(),
__('PHP JSON extension required to function.')
);
$this->testItem($rows, __('SOAP Extension'),
Environment::checkSoap(),
__('PHP SOAP extension required to function.')
);
$this->testItem($rows, __('GD Extension'),
Environment::checkGd(),
__('PHP GD extension required to function.')
);
$this->testItem($rows, __('Session'),
Environment::checkGd(),
__('PHP session support required to function.')
);
$this->testItem($rows, __('FileInfo'),
Environment::checkFileInfo(),
__('Requires PHP FileInfo support to function. If you are on Windows you need to enable the php_fileinfo.dll in your php.ini file.')
);
$this->testItem($rows, __('PCRE'),
Environment::checkPCRE(),
__('PHP PCRE support to function.')
);
$this->testItem($rows, __('Gettext'),
Environment::checkPCRE(),
__('PHP Gettext support to function.')
);
$this->testItem($rows, __('DOM Extension'),
Environment::checkDom(),
__('PHP DOM core functionality enabled.')
);
$this->testItem($rows, __('DOM XML Extension'),
Environment::checkDomXml(),
__('PHP DOM XML extension to function.')
);
$this->testItem($rows, __('Allow PHP to open external URLs'),
(Environment::checkCurl() || Environment::checkAllowUrlFopen()),
__('You must have the curl extension enabled or PHP configured with "allow_url_fopen = On" for the CMS to access external resources. We strongly recommend curl.'),
false
);
$this->testItem($rows, __('DateTimeZone'),
Environment::checkTimezoneIdentifiers(),
__('This enables us to get a list of time zones supported by the hosting server.'),
false
);
$this->testItem($rows, __('ZIP'),
Environment::checkZip(),
__('This enables import / export of layouts.')
);
$advice = __('Support for uploading large files is recommended.');
$advice .= __('We suggest setting your PHP post_max_size and upload_max_filesize to at least 128M, and also increasing your max_execution_time to at least 120 seconds.');
$this->testItem($rows, __('Large File Uploads'),
Environment::checkPHPUploads(),
$advice,
false
);
$this->testItem($rows, __('cURL'),
Environment::checkCurlInstalled(),
__('cURL is used to fetch data from the Internet or Local Network')
);
$this->testItem($rows, __('OpenSSL'),
Environment::checkOpenSsl(),
__('OpenSSL is used to seal and verify messages sent to XMR'),
false
);
$this->testItem($rows, __('SimpleXML'),
Environment::checkSimpleXml(),
__('SimpleXML is used to parse RSS feeds and other XML data sources')
);
$this->testItem($rows, __('GNUPG'),
Environment::checkGnu(),
__('checkGnu is used to verify the integrity of Player Software versions uploaded to the CMS'),
false
);
$this->envTested = true;
return $rows;
}
/**
* Is there an environment fault
* @return bool
*/
public function environmentFault()
{
if (!$this->envTested) {
$this->checkEnvironment();
}
return $this->envFault || !Environment::checkSettingsFileSystemPermissions();
}
/**
* Is there an environment warning
* @return bool
*/
public function environmentWarning()
{
if (!$this->envTested) {
$this->checkEnvironment();
}
return $this->envWarning;
}
/**
* Check binlog format
* @return bool
*/
public function checkBinLogEnabled()
{
//TODO: move this into storage interface
$results = $this->getStore()->select('show variables like \'log_bin\'', []);
if (count($results) <= 0)
return false;
return ($results[0]['Value'] != 'OFF');
}
/**
* Check binlog format
* @return bool
*/
public function checkBinLogFormat()
{
//TODO: move this into storage interface
$results = $this->getStore()->select('show variables like \'binlog_format\'', []);
if (count($results) <= 0)
return false;
return ($results[0]['Value'] != 'STATEMENT');
}
public function getPhoneticKey()
{
return NatoAlphabet::convertToNato($this->getSetting('SERVER_KEY'));
}
}

View File

@@ -0,0 +1,185 @@
<?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\Service;
use Stash\Interfaces\PoolInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\ConfigurationException;
/**
* Interface ConfigServiceInterface
* @package Xibo\Service
*/
interface ConfigServiceInterface
{
/**
* Set Service Dependencies
* @param StorageServiceInterface $store
* @param string $rootUri
*/
public function setDependencies($store, $rootUri);
/**
* Get Cache Pool
* @param PoolInterface $pool
* @return mixed
*/
public function setPool($pool);
/**
* Get Database Config
* @return array
*/
public function getDatabaseConfig();
/**
* Get settings
* @return array|mixed|null
*/
public function getSettings();
/**
* Gets the requested setting from the DB object given
* @param $setting string
* @param string[optional] $default
* @param bool[optional] $full
* @return string
*/
public function getSetting($setting, $default = NULL, $full = false);
/**
* Change Setting
* @param string $setting
* @param mixed $value
* @param int $userChange
*/
public function changeSetting($setting, $value, $userChange = 0);
/**
* Is the provided setting visible
* @param string $setting
* @return bool
*/
public function isSettingVisible($setting);
/**
* Is the provided setting editable
* @param string $setting
* @return bool
*/
public function isSettingEditable($setting);
/**
* Should the host be considered a proxy exception
* @param $host
* @return bool
*/
public function isProxyException($host);
/**
* Get Proxy Configuration
* @param array $httpOptions
* @return array
*/
public function getGuzzleProxy($httpOptions = []);
/**
* Get API key details from Configuration
* @return array
*/
public function getApiKeyDetails();
/**
* Checks the Environment and Determines if it is suitable
* @return string
*/
public function checkEnvironment();
/**
* Loads the theme
* @param string[Optional] $themeName
* @throws ConfigurationException
*/
public function loadTheme($themeName = null);
/**
* Get Theme Specific Settings
* @param null $settingName
* @param null $default
* @return null
*/
public function getThemeConfig($settingName = null, $default = null);
/**
* Get theme URI
* @param string $uri
* @param bool $local
* @return string
*/
public function uri($uri, $local = false);
/**
* Check a theme file exists
* @param string $uri
* @return bool
*/
public function themeFileExists($uri);
/**
* Check a web file exists
* @param string $uri
* @return bool
*/
public function fileExists($uri);
/**
* Get App Root URI
* @return mixed
*/
public function rootUri();
/**
* Get cache drivers
* @return array
*/
public function getCacheDrivers();
/**
* Get time series store settings
* @return array
*/
public function getTimeSeriesStore();
/**
* Get the cache namespace
* @return string
*/
public function getCacheNamespace();
/**
* Get Connector settings from the file based settings
* this acts as an override for settings stored in the database
* @param string $connector The connector to return settings for.
* @return array
*/
public function getConnectorSettings(string $connector): array;
}

View File

@@ -0,0 +1,978 @@
<?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\Service;
use Carbon\Carbon;
use Stash\Interfaces\PoolInterface;
use Xibo\Entity\Display;
use Xibo\Factory\ScheduleFactory;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\DeadlockException;
use Xibo\XMR\CollectNowAction;
use Xibo\XMR\DataUpdateAction;
/**
* Class DisplayNotifyService
* @package Xibo\Service
*/
class DisplayNotifyService implements DisplayNotifyServiceInterface
{
/** @var ConfigServiceInterface */
private $config;
/** @var LogServiceInterface */
private $log;
/** @var StorageServiceInterface */
private $store;
/** @var PoolInterface */
private $pool;
/** @var PlayerActionServiceInterface */
private $playerActionService;
/** @var ScheduleFactory */
private $scheduleFactory;
/** @var bool */
private $collectRequired = false;
/** @var int[] */
private $displayIds = [];
/** @var int[] */
private $displayIdsRequiringActions = [];
/** @var string[] */
private $keysProcessed = [];
/** @inheritdoc */
public function __construct($config, $log, $store, $pool, $playerActionService, $scheduleFactory)
{
$this->config = $config;
$this->log = $log;
$this->store = $store;
$this->pool = $pool;
$this->playerActionService = $playerActionService;
$this->scheduleFactory = $scheduleFactory;
}
/** @inheritdoc */
public function init()
{
$this->collectRequired = false;
return $this;
}
/** @inheritdoc */
public function collectNow()
{
$this->collectRequired = true;
return $this;
}
/** @inheritdoc */
public function collectLater()
{
$this->collectRequired = false;
return $this;
}
/** @inheritdoc */
public function processQueue()
{
if (count($this->displayIds) <= 0) {
return;
}
$this->log->debug('Process queue of ' . count($this->displayIds) . ' display notifications');
// We want to do 3 things.
// 1. Drop the Cache for each displayId
// 2. Update the mediaInventoryStatus on each DisplayId to 3 (pending)
// 3. Fire a PlayerAction if appropriate - what is appropriate?!
// Unique our displayIds
$displayIds = array_values(array_unique($this->displayIds, SORT_NUMERIC));
// Make a list of them that we can use in the update statement
$qmarks = str_repeat('?,', count($displayIds) - 1) . '?';
try {
// This runs on the default connection which will already be committed and closed by the time we get
// here. This doesn't run in a transaction.
$this->store->updateWithDeadlockLoop(
'UPDATE `display` SET mediaInventoryStatus = 3 WHERE displayId IN (' . $qmarks . ')',
$displayIds,
'default',
false
);
} catch (DeadlockException $deadlockException) {
$this->log->error('Failed to update media inventory status: ' . $deadlockException->getMessage());
}
// Dump the cache
foreach ($displayIds as $displayId) {
$this->pool->deleteItem(Display::getCachePrefix() . $displayId);
}
// Player actions
$this->processPlayerActions();
}
/**
* Process Actions
*/
private function processPlayerActions()
{
if (count($this->displayIdsRequiringActions) <= 0) {
return;
}
$this->log->debug('Process queue of ' . count($this->displayIdsRequiringActions) . ' display actions');
$displayIdsRequiringActions = array_values(array_unique($this->displayIdsRequiringActions, SORT_NUMERIC));
$qmarks = str_repeat('?,', count($displayIdsRequiringActions) - 1) . '?';
$displays = $this->store->select(
'SELECT displayId, xmrChannel, xmrPubKey, display, client_type AS clientType, `client_code`
FROM `display`
WHERE displayId IN (' . $qmarks . ')',
$displayIdsRequiringActions
);
foreach ($displays as $row) {
// TOOD: this should be improved
$display = new Display(
$this->store,
$this->log,
null,
$this->config,
null,
null,
null,
null,
);
$display->displayId = intval($row['displayId']);
$display->xmrChannel = $row['xmrChannel'];
$display->xmrPubKey = $row['xmrPubKey'];
$display->display = $row['display'];
$display->clientType = $row['clientType'];
$display->clientCode = intval($row['client_code']);
try {
$this->playerActionService->sendAction($display, new CollectNowAction());
} catch (\Exception $e) {
$this->log->notice(
'DisplayId ' .
$row['displayId'] .
' Save would have triggered Player Action, but the action failed with message: ' . $e->getMessage()
);
}
}
}
/** @inheritdoc */
public function notifyByDisplayId($displayId)
{
$this->log->debug('Notify by DisplayId ' . $displayId);
// Don't process if the displayId is already in the collection (there is little point in running the
// extra query)
if (in_array($displayId, $this->displayIds)) {
return;
}
$this->displayIds[] = $displayId;
if ($this->collectRequired) {
$this->displayIdsRequiringActions[] = $displayId;
}
}
/** @inheritdoc */
public function notifyByDisplayGroupId($displayGroupId)
{
$this->log->debug('Notify by DisplayGroupId ' . $displayGroupId);
if (in_array('displayGroup_' . $displayGroupId, $this->keysProcessed)) {
$this->log->debug('Already processed ' . $displayGroupId . ' skipping this time.');
return;
}
$sql = '
SELECT DISTINCT `lkdisplaydg`.displayId
FROM `lkdgdg`
INNER JOIN `lkdisplaydg`
ON `lkdisplaydg`.displayGroupID = `lkdgdg`.childId
WHERE `lkdgdg`.parentId = :displayGroupId
';
foreach ($this->store->select($sql, ['displayGroupId' => $displayGroupId]) as $row) {
// Don't process if the displayId is already in the collection
if (in_array($row['displayId'], $this->displayIds)) {
continue;
}
$this->displayIds[] = $row['displayId'];
$this->log->debug(
'DisplayGroup[' . $displayGroupId .'] change caused notify on displayId[' .
$row['displayId'] . ']'
);
if ($this->collectRequired) {
$this->displayIdsRequiringActions[] = $row['displayId'];
}
}
$this->keysProcessed[] = 'displayGroup_' . $displayGroupId;
}
/** @inheritdoc */
public function notifyByCampaignId($campaignId)
{
$this->log->debug('Notify by CampaignId ' . $campaignId);
if (in_array('campaign_' . $campaignId, $this->keysProcessed)) {
$this->log->debug('Already processed ' . $campaignId . ' skipping this time.');
return;
}
$sql = '
SELECT DISTINCT display.displayId,
schedule.eventId,
schedule.fromDt,
schedule.toDt,
schedule.recurrence_type AS recurrenceType,
schedule.recurrence_detail AS recurrenceDetail,
schedule.recurrence_range AS recurrenceRange,
schedule.recurrenceRepeatsOn,
schedule.lastRecurrenceWatermark,
schedule.dayPartId
FROM `schedule`
INNER JOIN `lkscheduledisplaygroup`
ON `lkscheduledisplaygroup`.eventId = `schedule`.eventId
INNER JOIN `lkdgdg`
ON `lkdgdg`.parentId = `lkscheduledisplaygroup`.displayGroupId
INNER JOIN `lkdisplaydg`
ON lkdisplaydg.DisplayGroupID = `lkdgdg`.childId
INNER JOIN `display`
ON lkdisplaydg.DisplayID = display.displayID
INNER JOIN (
SELECT campaignId
FROM campaign
WHERE campaign.campaignId = :activeCampaignId
UNION
SELECT DISTINCT parent.campaignId
FROM `lkcampaignlayout` child
INNER JOIN `lkcampaignlayout` parent
ON parent.layoutId = child.layoutId
WHERE child.campaignId = :activeCampaignId
) campaigns
ON campaigns.campaignId = `schedule`.campaignId
WHERE (
(`schedule`.FromDT < :toDt AND IFNULL(`schedule`.toDt, UNIX_TIMESTAMP()) > :fromDt)
OR `schedule`.recurrence_range >= :fromDt
OR (
IFNULL(`schedule`.recurrence_range, 0) = 0 AND IFNULL(`schedule`.recurrence_type, \'\') <> \'\'
)
)
UNION
SELECT DISTINCT display.DisplayID,
0 AS eventId,
0 AS fromDt,
0 AS toDt,
NULL AS recurrenceType,
NULL AS recurrenceDetail,
NULL AS recurrenceRange,
NULL AS recurrenceRepeatsOn,
NULL AS lastRecurrenceWatermark,
NULL AS dayPartId
FROM `display`
INNER JOIN `lkcampaignlayout`
ON `lkcampaignlayout`.LayoutID = `display`.DefaultLayoutID
WHERE `lkcampaignlayout`.CampaignID = :activeCampaignId2
UNION
SELECT `lkdisplaydg`.displayId,
0 AS eventId,
0 AS fromDt,
0 AS toDt,
NULL AS recurrenceType,
NULL AS recurrenceDetail,
NULL AS recurrenceRange,
NULL AS recurrenceRepeatsOn,
NULL AS lastRecurrenceWatermark,
NULL AS dayPartId
FROM `lkdisplaydg`
INNER JOIN `lklayoutdisplaygroup`
ON `lklayoutdisplaygroup`.displayGroupId = `lkdisplaydg`.displayGroupId
INNER JOIN `lkcampaignlayout`
ON `lkcampaignlayout`.layoutId = `lklayoutdisplaygroup`.layoutId
WHERE `lkcampaignlayout`.campaignId = :assignedCampaignId
UNION
SELECT `schedule_sync`.displayId,
schedule.eventId,
schedule.fromDt,
schedule.toDt,
schedule.recurrence_type AS recurrenceType,
schedule.recurrence_detail AS recurrenceDetail,
schedule.recurrence_range AS recurrenceRange,
schedule.recurrenceRepeatsOn,
schedule.lastRecurrenceWatermark,
schedule.dayPartId
FROM `schedule`
INNER JOIN `schedule_sync`
ON `schedule_sync`.eventId = `schedule`.eventId
INNER JOIN `lkcampaignlayout`
ON `lkcampaignlayout`.layoutId = `schedule_sync`.layoutId
WHERE `lkcampaignlayout`.campaignId = :assignedCampaignId
AND (
(`schedule`.FromDT < :toDt AND IFNULL(`schedule`.toDt, `schedule`.fromDt) > :fromDt)
OR `schedule`.recurrence_range >= :fromDt
OR (
IFNULL(`schedule`.recurrence_range, 0) = 0 AND IFNULL(`schedule`.recurrence_type, \'\') <> \'\'
)
)
';
$currentDate = Carbon::now();
$rfLookAhead = $currentDate->copy()->addSeconds($this->config->getSetting('REQUIRED_FILES_LOOKAHEAD'));
$params = [
'fromDt' => $currentDate->subHour()->format('U'),
'toDt' => $rfLookAhead->format('U'),
'activeCampaignId' => $campaignId,
'activeCampaignId2' => $campaignId,
'assignedCampaignId' => $campaignId
];
foreach ($this->store->select($sql, $params) as $row) {
// Don't process if the displayId is already in the collection (there is little point in running the
// extra query)
if (in_array($row['displayId'], $this->displayIds)) {
continue;
}
// Is this schedule active?
if ($row['eventId'] != 0) {
$scheduleEvents = $this->scheduleFactory
->createEmpty()
->hydrate($row)
->getEvents($currentDate, $rfLookAhead);
if (count($scheduleEvents) <= 0) {
$this->log->debug(
'Skipping eventId ' . $row['eventId'] .
' because it doesnt have any active events in the window'
);
continue;
}
}
$this->log->debug(
'Campaign[' . $campaignId .']
change caused notify on displayId[' . $row['displayId'] . ']'
);
$this->displayIds[] = $row['displayId'];
if ($this->collectRequired) {
$this->displayIdsRequiringActions[] = $row['displayId'];
}
}
$this->keysProcessed[] = 'campaign_' . $campaignId;
}
/** @inheritdoc */
public function notifyByDataSetId($dataSetId)
{
$this->log->debug('notifyByDataSetId: dataSetId: ' . $dataSetId);
if (in_array('dataSet_' . $dataSetId, $this->keysProcessed)) {
$this->log->debug('notifyByDataSetId: already processed.');
return;
}
// Set the Sync task to runNow
$this->store->update('UPDATE `task` SET `runNow` = 1 WHERE `class` LIKE :taskClassLike', [
'taskClassLike' => '%WidgetSyncTask%',
]);
// Query the schedule for any data connectors.
// This is a simple test to see if there are ever any schedules for this dataSetId
// TODO: this could be improved.
$sql = '
SELECT DISTINCT display.displayId
FROM `schedule`
INNER JOIN `lkscheduledisplaygroup`
ON `lkscheduledisplaygroup`.eventId = `schedule`.eventId
INNER JOIN `lkdgdg`
ON `lkdgdg`.parentId = `lkscheduledisplaygroup`.displayGroupId
INNER JOIN `lkdisplaydg`
ON `lkdisplaydg`.DisplayGroupID = `lkdgdg`.childId
INNER JOIN `display`
ON `lkdisplaydg`.DisplayID = `display`.displayID
WHERE `schedule`.dataSetId = :dataSetId
';
foreach ($this->store->select($sql, ['dataSetId' => $dataSetId]) as $row) {
$this->displayIds[] = $row['displayId'];
if ($this->collectRequired) {
$this->displayIdsRequiringActions[] = $row['displayId'];
}
}
$this->keysProcessed[] = 'dataSet_' . $dataSetId;
}
/** @inheritdoc */
public function notifyByPlaylistId($playlistId)
{
$this->log->debug('Notify by PlaylistId ' . $playlistId);
if (in_array('playlist_' . $playlistId, $this->keysProcessed)) {
$this->log->debug('Already processed ' . $playlistId . ' skipping this time.');
return;
}
$sql = '
SELECT DISTINCT display.displayId,
schedule.eventId,
schedule.fromDt,
schedule.toDt,
schedule.recurrence_type AS recurrenceType,
schedule.recurrence_detail AS recurrenceDetail,
schedule.recurrence_range AS recurrenceRange,
schedule.recurrenceRepeatsOn,
schedule.lastRecurrenceWatermark,
schedule.dayPartId
FROM `schedule`
INNER JOIN `lkscheduledisplaygroup`
ON `lkscheduledisplaygroup`.eventId = `schedule`.eventId
INNER JOIN `lkdgdg`
ON `lkdgdg`.parentId = `lkscheduledisplaygroup`.displayGroupId
INNER JOIN `lkdisplaydg`
ON lkdisplaydg.DisplayGroupID = `lkdgdg`.childId
INNER JOIN `display`
ON lkdisplaydg.DisplayID = display.displayID
INNER JOIN `lkcampaignlayout`
ON `lkcampaignlayout`.campaignId = `schedule`.campaignId
INNER JOIN `region`
ON `lkcampaignlayout`.layoutId = region.layoutId
INNER JOIN `playlist`
ON `playlist`.regionId = `region`.regionId
WHERE `playlist`.playlistId = :playlistId
AND (
(schedule.FromDT < :toDt AND IFNULL(`schedule`.toDt, UNIX_TIMESTAMP()) > :fromDt)
OR `schedule`.recurrence_range >= :fromDt
OR (
IFNULL(`schedule`.recurrence_range, 0) = 0 AND IFNULL(`schedule`.recurrence_type, \'\') <> \'\'
)
)
UNION
SELECT DISTINCT display.DisplayID,
0 AS eventId,
0 AS fromDt,
0 AS toDt,
NULL AS recurrenceType,
NULL AS recurrenceDetail,
NULL AS recurrenceRange,
NULL AS recurrenceRepeatsOn,
NULL AS lastRecurrenceWatermark,
NULL AS dayPartId
FROM `display`
INNER JOIN `lkcampaignlayout`
ON `lkcampaignlayout`.LayoutID = `display`.DefaultLayoutID
INNER JOIN `region`
ON `lkcampaignlayout`.layoutId = region.layoutId
INNER JOIN `playlist`
ON `playlist`.regionId = `region`.regionId
WHERE `playlist`.playlistId = :playlistId
UNION
SELECT `lkdisplaydg`.displayId,
0 AS eventId,
0 AS fromDt,
0 AS toDt,
NULL AS recurrenceType,
NULL AS recurrenceDetail,
NULL AS recurrenceRange,
NULL AS recurrenceRepeatsOn,
NULL AS lastRecurrenceWatermark,
NULL AS dayPartId
FROM `lkdisplaydg`
INNER JOIN `lklayoutdisplaygroup`
ON `lklayoutdisplaygroup`.displayGroupId = `lkdisplaydg`.displayGroupId
INNER JOIN `lkcampaignlayout`
ON `lkcampaignlayout`.layoutId = `lklayoutdisplaygroup`.layoutId
INNER JOIN `region`
ON `lkcampaignlayout`.layoutId = region.layoutId
INNER JOIN `playlist`
ON `playlist`.regionId = `region`.regionId
WHERE `playlist`.playlistId = :playlistId
';
$currentDate = Carbon::now();
$rfLookAhead = $currentDate->copy()->addSeconds($this->config->getSetting('REQUIRED_FILES_LOOKAHEAD'));
$params = [
'fromDt' => $currentDate->subHour()->format('U'),
'toDt' => $rfLookAhead->format('U'),
'playlistId' => $playlistId
];
foreach ($this->store->select($sql, $params) as $row) {
// Don't process if the displayId is already in the collection (there is little point in running the
// extra query)
if (in_array($row['displayId'], $this->displayIds)) {
continue;
}
// Is this schedule active?
if ($row['eventId'] != 0) {
$scheduleEvents = $this->scheduleFactory
->createEmpty()
->hydrate($row)
->getEvents($currentDate, $rfLookAhead);
if (count($scheduleEvents) <= 0) {
$this->log->debug(
'Skipping eventId ' . $row['eventId'] .
' because it doesnt have any active events in the window'
);
continue;
}
}
$this->log->debug(
'Playlist[' . $playlistId .'] change caused notify on displayId[' .
$row['displayId'] . ']'
);
$this->displayIds[] = $row['displayId'];
if ($this->collectRequired) {
$this->displayIdsRequiringActions[] = $row['displayId'];
}
}
$this->keysProcessed[] = 'playlist_' . $playlistId;
}
/** @inheritdoc */
public function notifyByLayoutCode($code)
{
if (in_array('layoutCode_' . $code, $this->keysProcessed)) {
$this->log->debug('Already processed ' . $code . ' skipping this time.');
return;
}
$this->log->debug('Notify by Layout Code: ' . $code);
// Get the Display Ids we need to notify
$sql = '
SELECT DISTINCT display.displayId,
schedule.eventId,
schedule.fromDt,
schedule.toDt,
schedule.recurrence_type AS recurrenceType,
schedule.recurrence_detail AS recurrenceDetail,
schedule.recurrence_range AS recurrenceRange,
schedule.recurrenceRepeatsOn,
schedule.lastRecurrenceWatermark,
schedule.dayPartId
FROM `schedule`
INNER JOIN `lkscheduledisplaygroup`
ON `lkscheduledisplaygroup`.eventId = `schedule`.eventId
INNER JOIN `lkdgdg`
ON `lkdgdg`.parentId = `lkscheduledisplaygroup`.displayGroupId
INNER JOIN `lkdisplaydg`
ON lkdisplaydg.DisplayGroupID = `lkdgdg`.childId
INNER JOIN `display`
ON lkdisplaydg.DisplayID = display.displayID
INNER JOIN (
SELECT DISTINCT campaignId
FROM layout
INNER JOIN lkcampaignlayout ON lkcampaignlayout.layoutId = layout.layoutId
INNER JOIN action on layout.layoutId = action.sourceId
WHERE action.layoutCode = :code AND layout.publishedStatusId = 1
UNION
SELECT DISTINCT campaignId
FROM layout
INNER JOIN lkcampaignlayout ON lkcampaignlayout.layoutId = layout.layoutId
INNER JOIN region ON region.layoutId = layout.layoutId
INNER JOIN action on region.regionId = action.sourceId
WHERE action.layoutCode = :code AND layout.publishedStatusId = 1
UNION
SELECT DISTINCT campaignId
FROM layout
INNER JOIN lkcampaignlayout ON lkcampaignlayout.layoutId = layout.layoutId
INNER JOIN region ON region.layoutId = layout.layoutId
INNER JOIN playlist ON playlist.regionId = region.regionId
INNER JOIN widget on playlist.playlistId = widget.playlistId
INNER JOIN action on widget.widgetId = action.sourceId
WHERE
action.layoutCode = :code AND
layout.publishedStatusId = 1
) campaigns
ON campaigns.campaignId = `schedule`.campaignId
WHERE (
(`schedule`.FromDT < :toDt AND IFNULL(`schedule`.toDt, UNIX_TIMESTAMP()) > :fromDt)
OR `schedule`.recurrence_range >= :fromDt
OR (
IFNULL(`schedule`.recurrence_range, 0) = 0 AND IFNULL(`schedule`.recurrence_type, \'\') <> \'\'
)
)
UNION
SELECT DISTINCT display.displayId,
schedule.eventId,
schedule.fromDt,
schedule.toDt,
schedule.recurrence_type AS recurrenceType,
schedule.recurrence_detail AS recurrenceDetail,
schedule.recurrence_range AS recurrenceRange,
schedule.recurrenceRepeatsOn,
schedule.lastRecurrenceWatermark,
schedule.dayPartId
FROM `schedule`
INNER JOIN `lkscheduledisplaygroup`
ON `lkscheduledisplaygroup`.eventId = `schedule`.eventId
INNER JOIN `lkdgdg`
ON `lkdgdg`.parentId = `lkscheduledisplaygroup`.displayGroupId
INNER JOIN `lkdisplaydg`
ON lkdisplaydg.DisplayGroupID = `lkdgdg`.childId
INNER JOIN `display`
ON lkdisplaydg.DisplayID = display.displayID
WHERE schedule.actionLayoutCode = :code
AND (
(`schedule`.FromDT < :toDt AND IFNULL(`schedule`.toDt, UNIX_TIMESTAMP()) > :fromDt)
OR `schedule`.recurrence_range >= :fromDt
OR (
IFNULL(`schedule`.recurrence_range, 0) = 0 AND IFNULL(`schedule`.recurrence_type, \'\') <> \'\'
)
)
UNION
SELECT DISTINCT display.DisplayID,
0 AS eventId,
0 AS fromDt,
0 AS toDt,
NULL AS recurrenceType,
NULL AS recurrenceDetail,
NULL AS recurrenceRange,
NULL AS recurrenceRepeatsOn,
NULL AS lastRecurrenceWatermark,
NULL AS dayPartId
FROM `display`
INNER JOIN `lkcampaignlayout`
ON `lkcampaignlayout`.LayoutID = `display`.DefaultLayoutID
WHERE `lkcampaignlayout`.CampaignID IN (
SELECT DISTINCT campaignId
FROM layout
INNER JOIN lkcampaignlayout ON lkcampaignlayout.layoutId = layout.layoutId
INNER JOIN action on layout.layoutId = action.sourceId
WHERE action.layoutCode = :code AND layout.publishedStatusId = 1
UNION
SELECT DISTINCT campaignId
FROM layout
INNER JOIN lkcampaignlayout ON lkcampaignlayout.layoutId = layout.layoutId
INNER JOIN region ON region.layoutId = layout.layoutId
INNER JOIN action on region.regionId = action.sourceId
WHERE action.layoutCode = :code AND layout.publishedStatusId = 1
UNION
SELECT DISTINCT campaignId
FROM layout
INNER JOIN lkcampaignlayout ON lkcampaignlayout.layoutId = layout.layoutId
INNER JOIN region ON region.layoutId = layout.layoutId
INNER JOIN playlist ON playlist.regionId = region.regionId
INNER JOIN widget on playlist.playlistId = widget.playlistId
INNER JOIN action on widget.widgetId = action.sourceId
WHERE
action.layoutCode = :code AND layout.publishedStatusId = 1
)
UNION
SELECT `lkdisplaydg`.displayId,
0 AS eventId,
0 AS fromDt,
0 AS toDt,
NULL AS recurrenceType,
NULL AS recurrenceDetail,
NULL AS recurrenceRange,
NULL AS recurrenceRepeatsOn,
NULL AS lastRecurrenceWatermark,
NULL AS dayPartId
FROM `lkdisplaydg`
INNER JOIN `lklayoutdisplaygroup`
ON `lklayoutdisplaygroup`.displayGroupId = `lkdisplaydg`.displayGroupId
INNER JOIN `lkcampaignlayout`
ON `lkcampaignlayout`.layoutId = `lklayoutdisplaygroup`.layoutId
WHERE `lkcampaignlayout`.campaignId IN (
SELECT DISTINCT campaignId
FROM layout
INNER JOIN lkcampaignlayout ON lkcampaignlayout.layoutId = layout.layoutId
INNER JOIN action on layout.layoutId = action.sourceId
WHERE action.layoutCode = :code AND layout.publishedStatusId = 1
UNION
SELECT DISTINCT campaignId
FROM layout
INNER JOIN lkcampaignlayout ON lkcampaignlayout.layoutId = layout.layoutId
INNER JOIN region ON region.layoutId = layout.layoutId
INNER JOIN action on region.regionId = action.sourceId
WHERE action.layoutCode = :code AND layout.publishedStatusId = 1
UNION
SELECT DISTINCT campaignId
FROM layout
INNER JOIN lkcampaignlayout ON lkcampaignlayout.layoutId = layout.layoutId
INNER JOIN region ON region.layoutId = layout.layoutId
INNER JOIN playlist ON playlist.regionId = region.regionId
INNER JOIN widget on playlist.playlistId = widget.playlistId
INNER JOIN action on widget.widgetId = action.sourceId
WHERE
action.layoutCode = :code AND layout.publishedStatusId = 1
)
';
$currentDate = Carbon::now();
$rfLookAhead = $currentDate->copy()->addSeconds($this->config->getSetting('REQUIRED_FILES_LOOKAHEAD'));
$params = [
'fromDt' => $currentDate->subHour()->format('U'),
'toDt' => $rfLookAhead->format('U'),
'code' => $code
];
foreach ($this->store->select($sql, $params) as $row) {
// Don't process if the displayId is already in the collection (there is little point in running the
// extra query)
if (in_array($row['displayId'], $this->displayIds)) {
continue;
}
// Is this schedule active?
if ($row['eventId'] != 0) {
$scheduleEvents = $this->scheduleFactory
->createEmpty()
->hydrate($row)
->getEvents($currentDate, $rfLookAhead);
if (count($scheduleEvents) <= 0) {
$this->log->debug(
'Skipping eventId ' . $row['eventId'] .
' because it doesnt have any active events in the window'
);
continue;
}
}
$this->log->debug(sprintf(
'Saving Layout with code %s, caused notify on
displayId[' . $row['displayId'] . ']',
$code
));
$this->displayIds[] = $row['displayId'];
if ($this->collectRequired) {
$this->displayIdsRequiringActions[] = $row['displayId'];
}
}
$this->keysProcessed[] = 'layoutCode_' . $code;
}
/** @inheritdoc */
public function notifyByMenuBoardId($menuId)
{
$this->log->debug('Notify by MenuBoard ID ' . $menuId);
if (in_array('menuBoard_' . $menuId, $this->keysProcessed)) {
$this->log->debug('Already processed ' . $menuId . ' skipping this time.');
return;
}
$sql = '
SELECT DISTINCT display.displayId,
schedule.eventId,
schedule.fromDt,
schedule.toDt,
schedule.recurrence_type AS recurrenceType,
schedule.recurrence_detail AS recurrenceDetail,
schedule.recurrence_range AS recurrenceRange,
schedule.recurrenceRepeatsOn,
schedule.lastRecurrenceWatermark,
schedule.dayPartId
FROM `schedule`
INNER JOIN `lkscheduledisplaygroup`
ON `lkscheduledisplaygroup`.eventId = `schedule`.eventId
INNER JOIN `lkdgdg`
ON `lkdgdg`.parentId = `lkscheduledisplaygroup`.displayGroupId
INNER JOIN `lkdisplaydg`
ON lkdisplaydg.DisplayGroupID = `lkdgdg`.childId
INNER JOIN `display`
ON lkdisplaydg.DisplayID = display.displayID
INNER JOIN `lkcampaignlayout`
ON `lkcampaignlayout`.campaignId = `schedule`.campaignId
INNER JOIN `region`
ON `region`.layoutId = `lkcampaignlayout`.layoutId
INNER JOIN `playlist`
ON `playlist`.regionId = `region`.regionId
INNER JOIN `widget`
ON `widget`.playlistId = `playlist`.playlistId
INNER JOIN `widgetoption`
ON `widgetoption`.widgetId = `widget`.widgetId
AND `widgetoption`.type = \'attrib\'
AND `widgetoption`.option = \'menuId\'
AND `widgetoption`.value = :activeMenuId
WHERE (
(schedule.FromDT < :toDt AND IFNULL(`schedule`.toDt, `schedule`.fromDt) > :fromDt)
OR `schedule`.recurrence_range >= :fromDt
OR (
IFNULL(`schedule`.recurrence_range, 0) = 0 AND IFNULL(`schedule`.recurrence_type, \'\') <> \'\'
)
)
UNION
SELECT DISTINCT display.displayId,
0 AS eventId,
0 AS fromDt,
0 AS toDt,
NULL AS recurrenceType,
NULL AS recurrenceDetail,
NULL AS recurrenceRange,
NULL AS recurrenceRepeatsOn,
NULL AS lastRecurrenceWatermark,
NULL AS dayPartId
FROM `display`
INNER JOIN `lkcampaignlayout`
ON `lkcampaignlayout`.LayoutID = `display`.DefaultLayoutID
INNER JOIN `region`
ON `region`.layoutId = `lkcampaignlayout`.layoutId
INNER JOIN `playlist`
ON `playlist`.regionId = `region`.regionId
INNER JOIN `widget`
ON `widget`.playlistId = `playlist`.playlistId
INNER JOIN `widgetoption`
ON `widgetoption`.widgetId = `widget`.widgetId
AND `widgetoption`.type = \'attrib\'
AND `widgetoption`.option = \'menuId\'
AND `widgetoption`.value = :activeMenuId2
UNION
SELECT DISTINCT `lkdisplaydg`.displayId,
0 AS eventId,
0 AS fromDt,
0 AS toDt,
NULL AS recurrenceType,
NULL AS recurrenceDetail,
NULL AS recurrenceRange,
NULL AS recurrenceRepeatsOn,
NULL AS lastRecurrenceWatermark,
NULL AS dayPartId
FROM `lklayoutdisplaygroup`
INNER JOIN `lkdgdg`
ON `lkdgdg`.parentId = `lklayoutdisplaygroup`.displayGroupId
INNER JOIN `lkdisplaydg`
ON lkdisplaydg.DisplayGroupID = `lkdgdg`.childId
INNER JOIN `lkcampaignlayout`
ON `lkcampaignlayout`.layoutId = `lklayoutdisplaygroup`.layoutId
INNER JOIN `region`
ON `region`.layoutId = `lkcampaignlayout`.layoutId
INNER JOIN `playlist`
ON `playlist`.regionId = `region`.regionId
INNER JOIN `widget`
ON `widget`.playlistId = `playlist`.playlistId
INNER JOIN `widgetoption`
ON `widgetoption`.widgetId = `widget`.widgetId
AND `widgetoption`.type = \'attrib\'
AND `widgetoption`.option = \'menuId\'
AND `widgetoption`.value = :activeMenuId3
';
$currentDate = Carbon::now();
$rfLookAhead = $currentDate->copy()->addSeconds($this->config->getSetting('REQUIRED_FILES_LOOKAHEAD'));
$params = [
'fromDt' => $currentDate->subHour()->format('U'),
'toDt' => $rfLookAhead->format('U'),
'activeMenuId' => $menuId,
'activeMenuId2' => $menuId,
'activeMenuId3' => $menuId
];
foreach ($this->store->select($sql, $params) as $row) {
// Don't process if the displayId is already in the collection (there is little point in running the
// extra query)
if (in_array($row['displayId'], $this->displayIds)) {
$this->log->debug('displayId ' . $row['displayId'] . ' already in collection, skipping.');
continue;
}
// Is this schedule active?
if ($row['eventId'] != 0) {
$scheduleEvents = $this->scheduleFactory
->createEmpty()
->hydrate($row)
->getEvents($currentDate, $rfLookAhead);
if (count($scheduleEvents) <= 0) {
$this->log->debug(
'Skipping eventId ' . $row['eventId'] .
' because it doesnt have any active events in the window'
);
continue;
}
}
$this->log->debug('MenuBoard[' . $menuId .'] change caused notify on displayId[' . $row['displayId'] . ']');
$this->displayIds[] = $row['displayId'];
if ($this->collectRequired) {
$this->displayIdsRequiringActions[] = $row['displayId'];
}
}
$this->keysProcessed[] = 'menuBoard_' . $menuId;
$this->log->debug('Finished notify for Menu Board ID ' . $menuId);
}
/** @inheritdoc */
public function notifyDataUpdate(Display $display, int $widgetId): void
{
if (in_array('dataUpdate_' . $display->displayId . '_' . $widgetId, $this->keysProcessed)) {
$this->log->debug('notifyDataUpdate: Already processed displayId: ' . $display->displayId
. ', widgetId: ' . $widgetId . ', skipping this time.');
return;
}
$this->log->debug('notifyDataUpdate: Process displayId: ' . $display->displayId . ', widgetId: ' . $widgetId);
try {
$this->playerActionService->sendAction($display, new DataUpdateAction($widgetId));
} catch (\Exception $e) {
$this->log->notice('notifyDataUpdate: displayId: ' . $display->displayId
. ', save would have triggered Player Action, but the action failed with message: ' . $e->getMessage());
}
}
}

View File

@@ -0,0 +1,119 @@
<?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\Service;
use Stash\Interfaces\PoolInterface;
use Xibo\Entity\Display;
use Xibo\Factory\ScheduleFactory;
use Xibo\Storage\StorageServiceInterface;
/**
* Interface DisplayNotifyServiceInterface
* @package Xibo\Service
*/
interface DisplayNotifyServiceInterface
{
/**
* DisplayNotifyServiceInterface constructor.
* @param ConfigServiceInterface $config
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param PoolInterface $pool
* @param PlayerActionServiceInterface $playerActionService
* @param ScheduleFactory $scheduleFactory
*/
public function __construct($config, $store, $log, $pool, $playerActionService, $scheduleFactory);
/**
* Initialise
* @return $this
*/
public function init();
/**
* @return $this
*/
public function collectNow();
/**
* @return $this
*/
public function collectLater();
/**
* Process Queue of Display Notifications
* @return $this
*/
public function processQueue();
/**
* Notify by Display Id
* @param $displayId
*/
public function notifyByDisplayId($displayId);
/**
* Notify by Display Group Id
* @param $displayGroupId
*/
public function notifyByDisplayGroupId($displayGroupId);
/**
* Notify by CampaignId
* @param $campaignId
*/
public function notifyByCampaignId($campaignId);
/**
* Notify by DataSetId
* @param $dataSetId
*/
public function notifyByDataSetId($dataSetId);
/**
* Notify by PlaylistId
* @param $playlistId
*/
public function notifyByPlaylistId($playlistId);
/**
* Notify By Layout Code
* @param $code
*/
public function notifyByLayoutCode($code);
/**
* Notify by Menu Board ID
* @param $menuId
*/
public function notifyByMenuBoardId($menuId);
/**
* Notify that data has been updated for this display
* @param \Xibo\Entity\Display $display
* @param int $widgetId
* @return void
*/
public function notifyDataUpdate(Display $display, int $widgetId): void;
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Xibo\Service;
use GuzzleHttp\Psr7\Stream;
use Psr\Log\LoggerInterface;
use Xibo\Helper\HttpCacheProvider;
class DownloadService
{
/** @var string File path inside the library folder */
private $filePath;
/** @var string Send file mode */
private $sendFileMode;
/** @var LoggerInterface */
private $logger;
/**
* @param string $filePath
* @param string $sendFileMode
*/
public function __construct(
string $filePath,
string $sendFileMode
) {
$this->filePath = $filePath;
$this->sendFileMode = $sendFileMode;
}
/**
* @param \Psr\Log\LoggerInterface $logger
* @return $this
*/
public function useLogger(LoggerInterface $logger): DownloadService
{
$this->logger = $logger;
return $this;
}
public function returnFile($response, $attachmentName, $nginxRedirect)
{
// Issue some headers
$response = HttpCacheProvider::withEtag($response, $this->filePath);
$response = HttpCacheProvider::withExpires($response, '+1 week');
// Set some headers
$headers = [];
$headers['Content-Length'] = filesize($this->filePath);
$headers['Content-Type'] = 'application/octet-stream';
$headers['Content-Transfer-Encoding'] = 'Binary';
$headers['Content-disposition'] = 'attachment; filename="' . $attachmentName . '"';
// Output the file
if ($this->sendFileMode === 'Apache') {
// Send via Apache X-Sendfile header?
$headers['X-Sendfile'] = $this->filePath;
} else if ($this->sendFileMode === 'Nginx') {
// Send via Nginx X-Accel-Redirect?
$headers['X-Accel-Redirect'] = $nginxRedirect;
}
// Add the headers we've collected to our response
foreach ($headers as $header => $value) {
$response = $response->withHeader($header, $value);
}
// Should we output the file via the application stack, or directly by reading the file.
if ($this->sendFileMode == 'Off') {
// Return the file with PHP
$response = $response->withBody(new Stream(fopen($this->filePath, 'r')));
$this->logger->debug('Returning Stream with response body, sendfile off.');
} else {
$this->logger->debug('Using sendfile to return the file, only output headers.');
}
return $response;
}
}

View File

@@ -0,0 +1,97 @@
<?php
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Service;
use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
use Xibo\Entity\HelpLink;
/**
* Class HelpService
* @package Xibo\Service
*/
class HelpService implements HelpServiceInterface
{
/** @var string */
private string $helpBase;
private ?array $links = null;
/**
* @inheritdoc
*/
public function __construct($helpBase)
{
$this->helpBase = $helpBase;
}
public function getLandingPage(): string
{
return $this->helpBase;
}
public function getLinksForPage(string $pageName): array
{
if ($this->links === null) {
$this->loadLinks();
}
return $this->links[$pageName] ?? [];
}
private function loadLinks(): void
{
// Load links from file.
try {
if (file_exists(PROJECT_ROOT . '/custom/help-links.yaml')) {
$links = (array)Yaml::parseFile(PROJECT_ROOT . '/custom/help-links.yaml');
} else if (file_exists(PROJECT_ROOT . '/help-links.yaml')) {
$links = (array)Yaml::parseFile(PROJECT_ROOT . '/help-links.yaml');
} else {
$this->links = [];
return;
}
} catch (\Exception) {
return;
}
// Parse links.
$this->links = [];
foreach ($links as $pageName => $page) {
// New page
$this->links[$pageName] = [];
foreach ($page as $link) {
$helpLink = new HelpLink($link);
if (!Str::startsWith($helpLink->url, ['http://', 'https://'])) {
$helpLink->url = $this->helpBase . $helpLink->url;
}
if (!empty($helpLink->summary)) {
$helpLink->summary = \Parsedown::instance()->setSafeMode(true)->line($helpLink->summary);
}
$this->links[$pageName][] = $helpLink;
}
}
}
}

View File

@@ -0,0 +1,45 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Service;
use Xibo\Entity\HelpLink;
/**
* Return help links for a page.
* @package Xibo\Service
*/
interface HelpServiceInterface
{
/**
* Get the landing page
* @return string
*/
public function getLandingPage(): string;
/**
* Get links for page
* @param string $pageName The page name to return links for
* @return HelpLink[]
*/
public function getLinksForPage(string $pageName): array;
}

View File

@@ -0,0 +1,82 @@
<?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\Service;
use Xibo\Service\ImageProcessingServiceInterface;
use Intervention\Image\Exception\NotReadableException;
use Intervention\Image\ImageManagerStatic as Img;
/**
* Class ImageProcessingService
* @package Xibo\Service
*/
class ImageProcessingService implements ImageProcessingServiceInterface
{
/** @var LogServiceInterface */
private $log;
/**
* @inheritdoc
*/
public function __construct()
{
}
/**
* @inheritdoc
*/
public function setDependencies($log)
{
$this->log = $log;
return $this;
}
/** @inheritdoc */
public function resizeImage($filePath, $width, $height)
{
try {
Img::configure(array('driver' => 'gd'));
$img = Img::make($filePath);
$img->resize($width, $height, function ($constraint) {
$constraint->aspectRatio();
});
// Get the updated height and width
$updatedHeight = $img->height();
$updatedWidth = $img->width();
$img->save($filePath);
$img->destroy();
} catch (NotReadableException $notReadableException) {
$this->log->error('Image not readable: ' . $notReadableException->getMessage());
}
return [
'filePath' => $filePath,
'height' => $updatedHeight ?? $height,
'width' => $updatedWidth ?? $width
];
}
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* Copyright (C) 2019 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\Service;
use Xibo\Service\LogServiceInterface;
/**
* Interface ImageProcessingServiceInterface
* @package Xibo\Service
*/
interface ImageProcessingServiceInterface
{
/**
* Image Processing constructor.
*/
public function __construct();
/**
* Set Image Processing Dependencies
* @param LogServiceInterface $logger
*/
public function setDependencies($logger);
/**
* Resize Image
* @param $filePath string
* @param $width int
* @param $height int
*/
public function resizeImage($filePath, $width, $height);
}

143
lib/Service/JwtService.php Normal file
View File

@@ -0,0 +1,143 @@
<?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\Service;
use Carbon\Carbon;
use Lcobucci\Clock\SystemClock;
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Encoding\ChainedFormatter;
use Lcobucci\JWT\Encoding\JoseEncoder;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Signer\Rsa\Sha256;
use Lcobucci\JWT\Token;
use Lcobucci\JWT\Token\Builder;
use Lcobucci\JWT\Validation\Constraint\LooseValidAt;
use Lcobucci\JWT\Validation\Constraint\SignedWith;
use Lcobucci\JWT\Validation\Constraint\ValidAt;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
/**
* A service to create and validate JWTs
*/
class JwtService implements JwtServiceInterface
{
/** @var \Psr\Log\LoggerInterface */
private $logger;
/** @var array */
private $keys;
/**
* @param \Psr\Log\LoggerInterface $logger
* @return \Xibo\Service\JwtServiceInterface
*/
public function useLogger(LoggerInterface $logger): JwtServiceInterface
{
$this->logger = $logger;
return $this;
}
/**
* @return \Psr\Log\LoggerInterface|\Psr\Log\NullLogger
*/
private function getLogger(): LoggerInterface
{
if ($this->logger === null) {
return new NullLogger();
}
return $this->logger;
}
/**
* @param $keys
* @return \Xibo\Service\JwtServiceInterface
*/
public function useKeys($keys): JwtServiceInterface
{
$this->keys = $keys;
return $this;
}
/** @inheritDoc */
public function generateJwt($issuedBy, $permittedFor, $identifiedBy, $relatedTo, $ttl): Token
{
$this->getLogger()->debug('generateJwt: Private key path is: ' . $this->getPrivateKeyPath()
. ', identifiedBy: ' . $identifiedBy . ', relatedTo: ' . $relatedTo);
$tokenBuilder = (new Builder(new JoseEncoder(), ChainedFormatter::default()));
$signingKey = Key\InMemory::file($this->getPrivateKeyPath());
return $tokenBuilder
->issuedBy($issuedBy)
->permittedFor($permittedFor)
->identifiedBy($identifiedBy)
->issuedAt(Carbon::now()->toDateTimeImmutable())
->canOnlyBeUsedAfter(Carbon::now()->toDateTimeImmutable())
->expiresAt(Carbon::now()->addSeconds($ttl)->toDateTimeImmutable())
->relatedTo($relatedTo)
->getToken(new Sha256(), $signingKey);
}
/** @inheritDoc */
public function validateJwt($jwt): ?Token
{
$this->getLogger()->debug('validateJwt: ' . $jwt);
$signingKey = Key\InMemory::file($this->getPrivateKeyPath());
$configuration = Configuration::forSymmetricSigner(new Sha256(), $signingKey);
$configuration->setValidationConstraints(
new LooseValidAt(new SystemClock(new \DateTimeZone(\date_default_timezone_get()))),
new SignedWith(new Sha256(), InMemory::plainText(file_get_contents($this->getPublicKeyPath())))
);
// Parse the token
$token = $configuration->parser()->parse($jwt);
$this->getLogger()->debug('validateJwt: token parsed');
// Test against constraints.
$constraints = $configuration->validationConstraints();
$configuration->validator()->assert($token, ...$constraints);
$this->getLogger()->debug('validateJwt: constraints valid');
return $token;
}
/**
* @return string|null
*/
private function getPublicKeyPath(): ?string
{
return $this->keys['publicKeyPath'] ?? null;
}
/**
* @return string|null
*/
private function getPrivateKeyPath(): ?string
{
return $this->keys['privateKeyPath'] ?? null;
}
}

View File

@@ -0,0 +1,36 @@
<?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\Service;
use Lcobucci\JWT\Token;
use Psr\Log\LoggerInterface;
/**
* A service to create and validate JWTs
*/
interface JwtServiceInterface
{
public function useLogger(LoggerInterface $logger): JwtServiceInterface;
public function generateJwt($issuedBy, $permittedFor, $identifiedBy, $relatedTo, $ttl): Token;
public function validateJwt($jwt): ?Token;
}

373
lib/Service/LogService.php Normal file
View File

@@ -0,0 +1,373 @@
<?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\Service;
use Carbon\Carbon;
use Monolog\Logger;
use Psr\Log\LoggerInterface;
use Xibo\Helper\DatabaseLogHandler;
use Xibo\Storage\PdoStorageService;
/**
* Class LogService
* @package Xibo\Service
*/
class LogService implements LogServiceInterface
{
/**
* @var \Psr\Log\LoggerInterface
*/
private $log;
/**
* The Log Mode
* @var string
*/
private $mode;
/**
* The user Id
* @var int
*/
private $userId = 0;
/**
* The User IP Address
*/
private $ipAddress;
/**
* Audit Log Statement
* @var \PDOStatement
*/
private $_auditLogStatement;
/**
* The History session id.
*/
private $sessionHistoryId = 0;
/**
* The API requestId.
*/
private $requestId = 0;
/**
* @inheritdoc
*/
public function __construct($logger, $mode = 'production')
{
$this->log = $logger;
$this->mode = $mode;
}
/** @inheritDoc */
public function getLoggerInterface(): LoggerInterface
{
return $this->log;
}
/**
* @inheritdoc
*/
public function setIpAddress($ip)
{
$this->ipAddress = $ip;
}
/**
* @inheritdoc
*/
public function setUserId($userId)
{
$this->userId = $userId;
}
/**
* @inheritdoc
*/
public function setSessionHistoryId($sessionHistoryId)
{
$this->sessionHistoryId = $sessionHistoryId;
}
public function setRequestId($requestId)
{
$this->requestId = $requestId;
}
public function getUserId(): ?int
{
return $this->userId;
}
public function getSessionHistoryId(): ?int
{
return $this->sessionHistoryId;
}
public function getRequestId(): ?int
{
return $this->requestId;
}
/**
* @inheritdoc
*/
public function setMode($mode)
{
$this->mode = $mode;
}
/**
* @inheritdoc
*/
public function audit($entity, $entityId, $message, $object)
{
$this->debug(sprintf(
'Audit Trail message recorded for %s with id %d. Message: %s from IP %s, session %d',
$entity,
$entityId,
$message,
$this->ipAddress,
$this->sessionHistoryId
));
if ($this->_auditLogStatement == null) {
$this->prepareAuditLogStatement();
}
// If we aren't a string then encode
if (!is_string($object)) {
$object = json_encode($object);
}
$params = [
'logDate' => Carbon::now()->format('U'),
'userId' => $this->userId,
'entity' => $entity,
'message' => $message,
'entityId' => $entityId,
'ipAddress' => $this->ipAddress,
'objectAfter' => $object,
'sessionHistoryId' => $this->sessionHistoryId,
'requestId' => $this->requestId
];
try {
$this->_auditLogStatement->execute($params);
} catch (\PDOException $PDOException) {
$errorCode = $PDOException->errorInfo[1] ?? $PDOException->getCode();
// Catch 2006 errors (mysql gone away)
if ($errorCode != 2006) {
throw $PDOException;
} else {
$this->prepareAuditLogStatement();
$this->_auditLogStatement->execute($params);
}
}
// Although we use the default connection, track audit status separately.
PdoStorageService::incrementStat('audit', 'insert');
}
/**
* Helper function to prepare a PDO statement for inserting into the Audit Log.
* sets $_auditLogStatement
* @return void
*/
private function prepareAuditLogStatement(): void
{
// Use the default connection
// audit log should rollback on failure.
$dbh = PdoStorageService::newConnection('default');
$this->_auditLogStatement = $dbh->prepare('
INSERT INTO `auditlog` (
`logDate`,
`userId`,
`entity`,
`message`,
`entityId`,
`objectAfter`,
`ipAddress`,
`sessionHistoryId`,
`requestId`
)
VALUES (
:logDate,
:userId,
:entity,
:message,
:entityId,
:objectAfter,
:ipAddress,
:sessionHistoryId,
:requestId
)
');
}
/**
* @inheritdoc
*/
public function sql($sql, $params, $logAsError = false)
{
if (strtolower($this->mode) == 'test' || $logAsError) {
$paramSql = '';
foreach ($params as $key => $param) {
$paramSql .= 'SET @' . $key . '=\'' . $param . '\';' . PHP_EOL;
}
($logAsError)
? $this->log->error($paramSql . str_replace(':', '@', $sql))
: $this->log->debug($paramSql . str_replace(':', '@', $sql));
}
}
/**
* @inheritdoc
*/
public function debug($object)
{
// Get the calling class / function
$this->log->debug($this->prepare($object, func_get_args()));
}
/**
* @inheritdoc
*/
public function notice($object)
{
$this->log->notice($this->prepare($object, func_get_args()));
}
/**
* @inheritdoc
*/
public function info($object)
{
$this->log->info($this->prepare($object, func_get_args()));
}
/**
* @inheritdoc
*/
public function warning($object)
{
$this->log->warning($this->prepare($object, func_get_args()));
}
/**
* @inheritdoc
*/
public function error($object)
{
$this->log->error($this->prepare($object, func_get_args()));
}
/**
* @inheritdoc
*/
public function critical($object)
{
$this->log->critical($this->prepare($object, func_get_args()));
}
/**
* @inheritdoc
*/
public function alert($object)
{
$this->log->alert($this->prepare($object, func_get_args()));
}
/**
* @inheritdoc
*/
public function emergency($object)
{
$this->log->emergency($this->prepare($object, func_get_args()));
}
/**
* @inheritdoc
*/
private function prepare($object, $args)
{
if (is_string($object)) {
array_shift($args);
if (count($args) > 0)
$object = vsprintf($object, $args);
}
return $object;
}
/**
* @inheritdoc
*/
public static function resolveLogLevel($level)
{
switch (strtolower($level)) {
case 'emergency':
return Logger::EMERGENCY;
case 'alert':
return Logger::ALERT;
case 'critical':
return Logger::CRITICAL;
case 'warning':
return Logger::WARNING;
case 'notice':
return Logger::NOTICE;
case 'info':
return Logger::INFO;
case 'debug':
case 'audit' :
return Logger::DEBUG;
case 'error':
default:
return Logger::ERROR;
}
}
/** @inheritDoc */
public function setLevel($level)
{
foreach ($this->log->getHandlers() as $handler) {
if ($handler instanceof DatabaseLogHandler) {
$handler->setLevel($level);
}
}
}
}

View File

@@ -0,0 +1,162 @@
<?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\Service;
use Psr\Log\LoggerInterface;
/**
* Interface LogServiceInterface
* @package Xibo\Service
*/
interface LogServiceInterface
{
/**
* Log constructor.
* @param LoggerInterface $logger
* @param string $mode
*/
public function __construct($logger, $mode = 'production');
/**
* Get the underlying logger interface
* useful for custom code and modules which may not want to know about the full Xibo LogServiceInterface
* @return \Psr\Log\LoggerInterface
*/
public function getLoggerInterface(): LoggerInterface;
public function getUserId(): ?int;
public function getSessionHistoryId(): ?int;
public function getRequestId(): ?int;
/**
* Set the user Id
* @param int $userId
*/
public function setUserId($userId);
/**
* Set the User IP Address
* @param $ip
* @return mixed
*/
public function setIpAddress($ip);
/**
* Set history session id
* @param $sessionHistoryId
* @return mixed
*/
public function setSessionHistoryId($sessionHistoryId);
/**
* Set API requestId
* @param $requestId
* @return mixed
*/
public function setRequestId($requestId);
/**
* @param $mode
* @return mixed
*/
public function setMode($mode);
/**
* Audit Log
* @param string $entity
* @param int $entityId
* @param string $message
* @param string|object|array $object
*/
public function audit($entity, $entityId, $message, $object);
/**
* @param $sql
* @param $params
* @param bool $logAsError
* @return mixed
*/
public function sql($sql, $params, $logAsError = false);
/**
* @param string
* @return mixed
*/
public function debug($object);
/**
* @param ...$object
* @return mixed
*/
public function notice($object);
/**
* @param ...$object
* @return mixed
*/
public function info($object);
/**
* @param ...$object
* @return mixed
*/
public function warning($object);
/**
* @param ...$object
* @return mixed
*/
public function error($object);
/**
* @param ...$object
* @return mixed
*/
public function critical($object);
/**
* @param ...$object
* @return mixed
*/
public function alert($object);
/**
* @param ...$object
* @return mixed
*/
public function emergency($object);
/**
* Resolve the log level
* @param string $level
* @return int
*/
public static function resolveLogLevel($level);
/**
* Set the log level on all handlers
* @param $level
*/
public function setLevel($level);
}

View File

@@ -0,0 +1,386 @@
<?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\Service;
use Carbon\Carbon;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Mimey\MimeTypes;
use Stash\Interfaces\PoolInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Xibo\Entity\User;
use Xibo\Event\MediaDeleteEvent;
use Xibo\Factory\FontFactory;
use Xibo\Factory\MediaFactory;
use Xibo\Helper\ByteFormatter;
use Xibo\Helper\DateFormatHelper;
use Xibo\Helper\Environment;
use Xibo\Helper\SanitizerService;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\ConfigurationException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\LibraryFullException;
/**
* MediaService
*/
class MediaService implements MediaServiceInterface
{
/** @var ConfigServiceInterface */
private $configService;
/** @var LogServiceInterface */
private $log;
/** @var StorageServiceInterface */
private $store;
/** @var SanitizerService */
private $sanitizerService;
/** @var PoolInterface */
private $pool;
/** @var MediaFactory */
private $mediaFactory;
/** @var User */
private $user;
/** @var EventDispatcherInterface */
private $dispatcher;
/**
* @var FontFactory
*/
private $fontFactory;
/** @inheritDoc */
public function __construct(
ConfigServiceInterface $configService,
LogServiceInterface $logService,
StorageServiceInterface $store,
SanitizerService $sanitizerService,
PoolInterface $pool,
MediaFactory $mediaFactory,
FontFactory $fontFactory
) {
$this->configService = $configService;
$this->log = $logService;
$this->store = $store;
$this->sanitizerService = $sanitizerService;
$this->pool = $pool;
$this->mediaFactory = $mediaFactory;
$this->fontFactory = $fontFactory;
}
/** @inheritDoc */
public function setUser(User $user) : MediaServiceInterface
{
$this->user = $user;
return $this;
}
/** @inheritDoc */
public function getUser() : User
{
return $this->user;
}
public function getPool() : PoolInterface
{
return $this->pool;
}
/** @inheritDoc */
public function setDispatcher(EventDispatcherInterface $dispatcher): MediaServiceInterface
{
$this->dispatcher = $dispatcher;
return $this;
}
/** @inheritDoc */
public function libraryUsage(): int
{
$results = $this->store->select('SELECT IFNULL(SUM(FileSize), 0) AS SumSize FROM media', []);
return $this->sanitizerService->getSanitizer($results[0])->getInt('SumSize');
}
/** @inheritDoc */
public function initLibrary(): MediaServiceInterface
{
MediaService::ensureLibraryExists($this->configService->getSetting('LIBRARY_LOCATION'));
return $this;
}
/** @inheritDoc */
public function checkLibraryOrQuotaFull($isCheckUser = false): MediaServiceInterface
{
// Check that we have some space in our library
$librarySizeLimit = $this->configService->getSetting('LIBRARY_SIZE_LIMIT_KB') * 1024;
$librarySizeLimitMB = round(($librarySizeLimit / 1024) / 1024, 2);
if ($librarySizeLimit > 0 && $this->libraryUsage() > $librarySizeLimit) {
throw new LibraryFullException(sprintf(__('Your library is full. Library Limit: %s MB'), $librarySizeLimitMB));
}
if ($isCheckUser) {
$this->getUser()->isQuotaFullByUser();
}
return $this;
}
/** @inheritDoc */
public function checkMaxUploadSize($size): MediaServiceInterface
{
if (ByteFormatter::toBytes(Environment::getMaxUploadSize()) < $size) {
throw new InvalidArgumentException(
sprintf(__('This file size exceeds your environment Max Upload Size %s'), Environment::getMaxUploadSize()),
'size'
);
}
return $this;
}
/** @inheritDoc */
public function getDownloadInfo($url): array
{
$downloadInfo = [];
$guzzle = new Client($this->configService->getGuzzleProxy());
// first try to get the extension from pathinfo
$info = pathinfo(parse_url($url, PHP_URL_PATH));
$extension = $info['extension'] ?? '';
$size = -1;
try {
$head = $guzzle->head($url);
// First chance at getting the content length so that we can fail early.
// Will fail for downloads with redirects.
if ($head->hasHeader('Content-Length')) {
$contentLength = $head->getHeader('Content-Length');
foreach ($contentLength as $value) {
$size = $value;
}
}
if (empty($extension)) {
$contentType = $head->getHeaderLine('Content-Type');
$extension = $contentType;
if ($contentType === 'binary/octet-stream' && $head->hasHeader('x-amz-meta-filetype')) {
$amazonContentType = $head->getHeaderLine('x-amz-meta-filetype');
$extension = $amazonContentType;
}
// get the extension corresponding to the mime type
$mimeTypes = new MimeTypes();
$extension = $mimeTypes->getExtension($extension);
}
} catch (RequestException $e) {
$this->log->debug('Upload from url head request failed for URL ' . $url
. ' with following message ' . $e->getMessage());
}
$downloadInfo['size'] = $size;
$downloadInfo['extension'] = $extension;
$downloadInfo['filename'] = $info['filename'];
return $downloadInfo;
}
/** @inheritDoc */
public function updateFontsCss()
{
// delete local cms fonts.css from cache
$this->pool->deleteItem('localFontCss');
$this->log->debug('Regenerating player fonts.css file');
// Go through all installed fonts each time and regenerate.
$fontTemplate = '@font-face {
font-family: \'[family]\';
src: url(\'[url]\');
}';
// Save a fonts.css file to the library for use as a module
$fonts = $this->fontFactory->query();
$css = '';
// Check the library exists
$libraryLocation = $this->configService->getSetting('LIBRARY_LOCATION');
MediaService::ensureLibraryExists($this->configService->getSetting('LIBRARY_LOCATION'));
// Build our font strings.
foreach ($fonts as $font) {
// Css for the player contains the actual stored as location of the font.
$css .= str_replace('[url]', $font->fileName, str_replace('[family]', $font->familyName, $fontTemplate));
}
// If we're a full regenerate, we want to also update the fonts.css file.
$existingLibraryFontsCss = '';
if (file_exists($libraryLocation . 'fonts/fonts.css')) {
$existingLibraryFontsCss = file_get_contents($libraryLocation . 'fonts/fonts.css');
}
$tempFontsCss = $libraryLocation . 'temp/fonts.css';
file_put_contents($tempFontsCss, $css);
// Check to see if the existing file is different from the new one
if ($existingLibraryFontsCss == '' || md5($existingLibraryFontsCss) !== md5($tempFontsCss)) {
$this->log->info('Detected change in fonts.css file, dropping the Display cache');
rename($tempFontsCss, $libraryLocation . 'fonts/fonts.css');
// Clear the display cache
$this->pool->deleteItem('/display');
} else {
@unlink($tempFontsCss);
$this->log->debug('Newly generated fonts.css is the same as the old file. Ignoring.');
}
}
/** @inheritDoc */
public static function ensureLibraryExists($libraryFolder)
{
// Check that this location exists - and if not create it..
if (!file_exists($libraryFolder)) {
mkdir($libraryFolder, 0777, true);
}
if (!file_exists($libraryFolder . '/temp')) {
mkdir($libraryFolder . '/temp', 0777, true);
}
if (!file_exists($libraryFolder . '/cache')) {
mkdir($libraryFolder . '/cache', 0777, true);
}
if (!file_exists($libraryFolder . '/screenshots')) {
mkdir($libraryFolder . '/screenshots', 0777, true);
}
if (!file_exists($libraryFolder . '/attachment')) {
mkdir($libraryFolder . '/attachment', 0777, true);
}
if (!file_exists($libraryFolder . '/thumbs')) {
mkdir($libraryFolder . '/thumbs', 0777, true);
}
if (!file_exists($libraryFolder . '/fonts')) {
mkdir($libraryFolder . '/fonts', 0777, true);
}
if (!file_exists($libraryFolder . '/playersoftware')) {
mkdir($libraryFolder . '/playersoftware', 0777, true);
}
if (!file_exists($libraryFolder . '/playersoftware/chromeos')) {
mkdir($libraryFolder . '/playersoftware/chromeos', 0777, true);
}
if (!file_exists($libraryFolder . '/savedreport')) {
mkdir($libraryFolder . '/savedreport', 0777, true);
}
if (!file_exists($libraryFolder . '/assets')) {
mkdir($libraryFolder . '/assets', 0777, true);
}
if (!file_exists($libraryFolder . '/data_connectors')) {
mkdir($libraryFolder . '/data_connectors', 0777, true);
}
// Check that we are now writable - if not then error
if (!is_writable($libraryFolder)) {
throw new ConfigurationException(__('Library not writable'));
}
}
/** @inheritDoc */
public function removeTempFiles()
{
$libraryTemp = $this->configService->getSetting('LIBRARY_LOCATION') . 'temp';
if (!is_dir($libraryTemp)) {
return;
}
// Dump the files in the temp folder
foreach (scandir($libraryTemp) as $item) {
if ($item == '.' || $item == '..') {
continue;
}
// Path
$filePath = $libraryTemp . DIRECTORY_SEPARATOR . $item;
if (is_dir($filePath)) {
$this->log->debug('Skipping folder: ' . $item);
continue;
}
// Has this file been written to recently?
if (filemtime($filePath) > Carbon::now()->subSeconds(86400)->format('U')) {
$this->log->debug('Skipping active file: ' . $item);
continue;
}
$this->log->debug('Deleting temp file: ' . $item);
unlink($filePath);
}
}
/** @inheritDoc */
public function removeExpiredFiles()
{
// Get a list of all expired files and delete them
foreach ($this->mediaFactory->query(
null,
[
'expires' => Carbon::now()->format('U'),
'allModules' => 1,
'unlinkedOnly' => 1,
'length' => 100,
]
) as $entry) {
// If the media type is a module, then pretend it's a generic file
$this->log->info(sprintf('Removing Expired File %s', $entry->name));
$this->log->audit(
'Media',
$entry->mediaId,
'Removing Expired',
[
'mediaId' => $entry->mediaId,
'name' => $entry->name,
'expired' => Carbon::createFromTimestamp($entry->expires)
->format(DateFormatHelper::getSystemFormat())
]
);
$this->dispatcher->dispatch(new MediaDeleteEvent($entry), MediaDeleteEvent::$NAME);
$entry->delete();
}
}
}

View File

@@ -0,0 +1,144 @@
<?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\Service;
use Slim\Routing\RouteParser;
use Stash\Interfaces\PoolInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Xibo\Entity\User;
use Xibo\Factory\FontFactory;
use Xibo\Factory\MediaFactory;
use Xibo\Helper\SanitizerService;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\ConfigurationException;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\NotFoundException;
/**
* MediaServiceInterface
* Provides common functionality for library media
*/
interface MediaServiceInterface
{
/**
* MediaService constructor.
* @param ConfigServiceInterface $configService
* @param LogServiceInterface $logService
* @param StorageServiceInterface $store
* @param SanitizerService $sanitizerService
* @param PoolInterface $pool
* @param MediaFactory $mediaFactory
* @param FontFactory $fontFactory
*/
public function __construct(
ConfigServiceInterface $configService,
LogServiceInterface $logService,
StorageServiceInterface $store,
SanitizerService $sanitizerService,
PoolInterface $pool,
MediaFactory $mediaFactory,
FontFactory $fontFactory
);
/**
* @param User $user
*/
public function setUser(User $user): MediaServiceInterface;
/**
* @return User
*/
public function getUser(): User;
/**
* @return PoolInterface
*/
public function getPool() : PoolInterface;
/**
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
* @return MediaServiceInterface
*/
public function setDispatcher(EventDispatcherInterface $dispatcher): MediaServiceInterface;
/**
* Library Usage
* @return int
*/
public function libraryUsage(): int;
/**
* @return $this
* @throws \Xibo\Support\Exception\ConfigurationException
*/
public function initLibrary(): MediaServiceInterface;
/**
* @return $this
* @throws \Xibo\Support\Exception\LibraryFullException
*/
public function checkLibraryOrQuotaFull($isCheckUser = false): MediaServiceInterface;
/**
* @param $size
* @return \Xibo\Service\MediaService
* @throws \Xibo\Support\Exception\InvalidArgumentException
*/
public function checkMaxUploadSize($size): MediaServiceInterface;
/**
* Get download info for a URL
* we're looking for the file size and the extension
* @param $url
* @return array
*/
public function getDownloadInfo($url): array;
/**
* @return array|mixed
* @throws ConfigurationException
* @throws \Xibo\Support\Exception\DuplicateEntityException
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\InvalidArgumentException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function updateFontsCss();
/**
* @param $libraryFolder
* @throws ConfigurationException
*/
public static function ensureLibraryExists($libraryFolder);
/**
* Remove temporary files
*/
public function removeTempFiles();
/**
* Removes all expired media files
* @throws NotFoundException
* @throws GeneralException
*/
public function removeExpiredFiles();
}

View File

@@ -0,0 +1,240 @@
<?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\Service;
use Monolog\Logger;
use Psr\Log\LoggerInterface;
/**
* Class NullLogService
* @package Xibo\Service
*/
class NullLogService implements LogServiceInterface
{
/**
* @var \Psr\Log\LoggerInterface
*/
private $log;
/**
* @inheritdoc
*/
public function __construct($logger, $mode = 'production')
{
$this->log = $logger;
}
/** @inheritDoc */
public function getLoggerInterface(): LoggerInterface
{
return $this->log;
}
/**
* @inheritdoc
*/
public function setUserId($userId)
{
//
}
/**
* @inheritdoc
*/
public function setIpAddress($ip)
{
//
}
/**
* @inheritdoc
*/
public function setMode($mode)
{
//
}
/**
* @inheritdoc
*/
public function audit($entity, $entityId, $message, $object)
{
//
}
/**
* @param $sql
* @param $params
* @param false $logAsError
* @inheritdoc
*/
public function sql($sql, $params, $logAsError = false)
{
//
}
/**
* @inheritdoc
*/
public function debug($object)
{
// Get the calling class / function
$this->log->debug($this->prepare($object, func_get_args()));
}
/**
* @inheritdoc
*/
public function notice($object)
{
$this->log->notice($this->prepare($object, func_get_args()));
}
/**
* @inheritdoc
*/
public function info($object)
{
$this->log->info($this->prepare($object, func_get_args()));
}
/**
* @inheritdoc
*/
public function warning($object)
{
$this->log->warning($this->prepare($object, func_get_args()));
}
/**
* @inheritdoc
*/
public function error($object)
{
$this->log->error($this->prepare($object, func_get_args()));
}
/**
* @inheritdoc
*/
public function critical($object)
{
$this->log->critical($this->prepare($object, func_get_args()));
}
/**
* @inheritdoc
*/
public function alert($object)
{
$this->log->alert($this->prepare($object, func_get_args()));
}
/**
* @inheritdoc
*/
public function emergency($object)
{
$this->log->emergency($this->prepare($object, func_get_args()));
}
/**
* @inheritdoc
*/
private function prepare($object, $args)
{
if (is_string($object)) {
array_shift($args);
if (count($args) > 0)
$object = vsprintf($object, $args);
}
return $object;
}
/**
* @inheritdoc
*/
public static function resolveLogLevel($level)
{
switch (strtolower($level)) {
case 'emergency':
return Logger::EMERGENCY;
case 'alert':
return Logger::ALERT;
case 'critical':
return Logger::CRITICAL;
case 'warning':
return Logger::WARNING;
case 'notice':
return Logger::NOTICE;
case 'info':
return Logger::INFO;
case 'debug':
return Logger::DEBUG;
case 'error':
default:
return Logger::ERROR;
}
}
/** @inheritDoc */
public function setLevel($level)
{
//
}
public function getUserId(): ?int
{
return null;
}
public function getSessionHistoryId(): ?int
{
return null;
}
public function getRequestId(): ?int
{
return null;
}
public function setSessionHistoryId($sessionHistoryId)
{
//
}
public function setRequestId($requestId)
{
//
}
}

View File

@@ -0,0 +1,196 @@
<?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\Service;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Xibo\Entity\Display;
use Xibo\Support\Exception\ConfigurationException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\XMR\PlayerAction;
use Xibo\XMR\PlayerActionException;
/**
* Class PlayerActionService
* @package Xibo\Service
*/
class PlayerActionService implements PlayerActionServiceInterface
{
private ?string $xmrAddress;
/** @var PlayerAction[] */
private array $actions = [];
/**
* @inheritdoc
*/
public function __construct(
private readonly ConfigServiceInterface $config,
private readonly LogServiceInterface $log,
private readonly bool $triggerPlayerActions
) {
$this->xmrAddress = null;
}
/**
* Get Config
* @return ConfigServiceInterface
*/
private function getConfig(): ConfigServiceInterface
{
return $this->config;
}
/**
* @inheritdoc
*/
public function sendAction($displays, $action): void
{
if (!$this->triggerPlayerActions) {
return;
}
// XMR network address
if ($this->xmrAddress == null) {
$this->xmrAddress = $this->getConfig()->getSetting('XMR_ADDRESS');
}
if (empty($this->xmrAddress)) {
throw new InvalidArgumentException(__('XMR address is not set'), 'xmrAddress');
}
if (!is_array($displays)) {
$displays = [$displays];
}
// Send a message to all displays
foreach ($displays as $display) {
/* @var Display $display */
$isEncrypt = false;
if ($display->xmrChannel == '') {
throw new InvalidArgumentException(
sprintf(
__('%s is not configured or ready to receive push commands over XMR. Please contact your administrator.'),//phpcs:ignore
$display->display
),
'xmrChannel'
);
}
// If we are using the old ZMQ XMR service, we also need to encrypt the message
if (!$display->isWebSocketXmrSupported()) {
// We also need a xmrPubKey
$isEncrypt = true;
if ($display->xmrPubKey == '') {
throw new InvalidArgumentException(
sprintf(
__('%s is not configured or ready to receive push commands over XMR. Please contact your administrator.'),//phpcs:ignore
$display->display
),
'xmrPubKey'
);
}
}
// Do not send anything if XMR is disabled.
if (($isEncrypt && $this->getConfig()->getSetting('XMR_WS_ADDRESS') === 'DISABLED')
|| (!$isEncrypt && $this->getConfig()->getSetting('XMR_PUB_ADDRESS') === 'DISABLED')
) {
continue;
}
$displayAction = clone $action;
try {
$displayAction->setIdentity($display->xmrChannel, $isEncrypt, $display->xmrPubKey ?? null);
} catch (\Exception $exception) {
throw new InvalidArgumentException(
sprintf(
__('%s Invalid XMR registration'),
$display->display
),
'xmrPubKey'
);
}
// Add to collection
$this->actions[] = $displayAction;
}
}
/** @inheritDoc */
public function getQueue(): array
{
return $this->actions;
}
/**
* @inheritdoc
*/
public function processQueue(): void
{
if (count($this->actions) > 0) {
$this->log->debug('Player Action Service is looking to send %d actions', count($this->actions));
} else {
return;
}
// XMR network address
if ($this->xmrAddress == null) {
$this->xmrAddress = $this->getConfig()->getSetting('XMR_ADDRESS');
}
$client = new Client($this->config->getGuzzleProxy([
'base_uri' => $this->getConfig()->getSetting('XMR_ADDRESS'),
]));
$failures = 0;
// TODO: could I send them all in one request instead?
foreach ($this->actions as $action) {
/** @var PlayerAction $action */
try {
// Send each action
$client->post('/', [
'json' => $action->finaliseMessage(),
]);
} catch (GuzzleException | PlayerActionException $e) {
$this->log->error('Player action connection failed. E = ' . $e->getMessage());
$failures++;
}
}
if ($failures > 0) {
throw new ConfigurationException(
sprintf(
__('%d of %d player actions failed'),
$failures,
count($this->actions)
)
);
}
}
}

View File

@@ -0,0 +1,56 @@
<?php
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Service;
use Xibo\Entity\Display;
use Xibo\Support\Exception\GeneralException;
use Xibo\XMR\PlayerAction;
/**
* Interface PlayerActionServiceInterface
* @package Xibo\Service
*/
interface PlayerActionServiceInterface
{
/**
* PlayerActionHelper constructor.
*/
public function __construct(ConfigServiceInterface $config, LogServiceInterface $log, bool $triggerPlayerActions);
/**
* @param Display[]|Display $displays
* @param PlayerAction $action
* @throws GeneralException
*/
public function sendAction($displays, $action): void;
/**
* Get the queue
*/
public function getQueue(): array;
/**
* Process the Queue of Actions
* @throws GeneralException
*/
public function processQueue(): void;
}

View File

@@ -0,0 +1,431 @@
<?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\Service;
use Illuminate\Support\Str;
use Psr\Container\ContainerInterface;
use Slim\Http\ServerRequest as Request;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Xibo\Entity\ReportResult;
use Xibo\Event\ConnectorReportEvent;
use Xibo\Factory\SavedReportFactory;
use Xibo\Helper\SanitizerService;
use Xibo\Report\ReportInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Storage\TimeSeriesStoreInterface;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
/**
* Class ReportScheduleService
* @package Xibo\Service
*/
class ReportService implements ReportServiceInterface
{
/**
* @var ContainerInterface
*/
public $container;
/**
* @var StorageServiceInterface
*/
private $store;
/**
* @var TimeSeriesStoreInterface
*/
private $timeSeriesStore;
/**
* @var LogServiceInterface
*/
private $log;
/**
* @var ConfigServiceInterface
*/
private $config;
/**
* @var SanitizerService
*/
private $sanitizer;
/**
* @var SavedReportFactory
*/
private $savedReportFactory;
/** @var EventDispatcherInterface */
private $dispatcher;
/**
* @inheritdoc
*/
public function __construct($container, $store, $timeSeriesStore, $log, $config, $sanitizer, $savedReportFactory)
{
$this->container = $container;
$this->store = $store;
$this->timeSeriesStore = $timeSeriesStore;
$this->log = $log;
$this->config = $config;
$this->sanitizer = $sanitizer;
$this->savedReportFactory = $savedReportFactory;
}
/** @inheritDoc */
public function setDispatcher(EventDispatcherInterface $dispatcher): ReportServiceInterface
{
$this->dispatcher = $dispatcher;
return $this;
}
public function getDispatcher(): EventDispatcherInterface
{
if ($this->dispatcher === null) {
$this->dispatcher = new EventDispatcher();
}
return $this->dispatcher;
}
/**
* @inheritdoc
*/
public function listReports()
{
$reports = [];
$files = array_merge(glob(PROJECT_ROOT . '/reports/*.report'), glob(PROJECT_ROOT . '/custom/*.report'));
foreach ($files as $file) {
$config = json_decode(file_get_contents($file));
$config->file = Str::replaceFirst(PROJECT_ROOT, '', $file);
// Compatibility check
if (!isset($config->feature) || !isset($config->category)) {
continue;
}
// Check if only allowed for admin
if ($this->container->get('user')->userTypeId != 1) {
if (isset($config->adminOnly) && !empty($config->adminOnly)) {
continue;
}
}
// Check Permissions
if (!$this->container->get('user')->featureEnabled($config->feature)) {
continue;
}
$reports[$config->category][] = $config;
}
$this->log->debug('Reports found in total: '.count($reports));
// Get reports that are allowed by connectors
$event = new ConnectorReportEvent();
$this->getDispatcher()->dispatch($event, ConnectorReportEvent::$NAME);
$connectorReports = $event->getReports();
// Merge built in reports and connector reports
if (count($connectorReports) > 0) {
$reports = array_merge($reports, $connectorReports);
}
foreach ($reports as $k => $report) {
usort($report, function ($a, $b) {
if (empty($a->sort_order) || empty($b->sort_order)) {
return 0;
}
return $a->sort_order - $b->sort_order;
});
$reports[$k] = $report;
}
return $reports;
}
/**
* @inheritdoc
*/
public function getReportByName($reportName)
{
foreach ($this->listReports() as $reports) {
foreach ($reports as $report) {
if ($report->name == $reportName) {
$this->log->debug('Get report by name: '.json_encode($report, JSON_PRETTY_PRINT));
return $report;
}
}
}
//throw error
throw new NotFoundException(__('Get Report By Name: No file to return'));
}
/**
* @inheritdoc
*/
public function getReportClass($reportName)
{
foreach ($this->listReports() as $reports) {
foreach ($reports as $report) {
if ($report->name == $reportName) {
if ($report->class == '') {
throw new NotFoundException(__('Report class not found'));
}
$this->log->debug('Get report class: '.$report->class);
return $report->class;
}
}
}
// throw error
throw new NotFoundException(__('Get report class: No file to return'));
}
/**
* @inheritdoc
*/
public function createReportObject($className)
{
if (!\class_exists($className)) {
throw new NotFoundException(__('Class %s not found', $className));
}
/** @var ReportInterface $object */
$object = new $className();
$object
->setCommonDependencies(
$this->store,
$this->timeSeriesStore
)
->useLogger($this->log)
->setFactories($this->container);
return $object;
}
/**
* @inheritdoc
*/
public function getReportScheduleFormData($reportName, Request $request)
{
$this->log->debug('Populate form title and hidden fields');
$className = $this->getReportClass($reportName);
$object = $this->createReportObject($className);
// Populate form title and hidden fields
return $object->getReportScheduleFormData($this->sanitizer->getSanitizer($request->getParams()));
}
/**
* @inheritdoc
*/
public function setReportScheduleFormData($reportName, Request $request)
{
$this->log->debug('Set Report Schedule form data');
$className = $this->getReportClass($reportName);
$object = $this->createReportObject($className);
// Set Report Schedule form data
return $object->setReportScheduleFormData($this->sanitizer->getSanitizer($request->getParams()));
}
/**
* @inheritdoc
*/
public function generateSavedReportName($reportName, $filterCriteria)
{
$this->log->debug('Generate Saved Report name');
$className = $this->getReportClass($reportName);
$object = $this->createReportObject($className);
$filterCriteria = json_decode($filterCriteria, true);
return $object->generateSavedReportName($this->sanitizer->getSanitizer($filterCriteria));
}
/**
* @inheritdoc
*/
public function getSavedReportResults($savedreportId, $reportName)
{
$className = $this->getReportClass($reportName);
$object = $this->createReportObject($className);
$savedReport = $this->savedReportFactory->getById($savedreportId);
// Open a zipfile and read the json
$zipFile = $this->config->getSetting('LIBRARY_LOCATION') .'savedreport/'. $savedReport->fileName;
// Do some pre-checks on the arguments we have been provided
if (!file_exists($zipFile)) {
throw new InvalidArgumentException(__('File does not exist'));
}
// Open the Zip file
$zip = new \ZipArchive();
if (!$zip->open($zipFile)) {
throw new InvalidArgumentException(__('Unable to open ZIP'));
}
// Get the reportscheduledetails
$json = json_decode($zip->getFromName('reportschedule.json'), true);
// Retrieve the saved report result array
$results = $object->getSavedReportResults($json, $savedReport);
$this->log->debug('Saved Report results'. json_encode($results, JSON_PRETTY_PRINT));
// Return data to build chart
return $results;
}
/**
* @inheritdoc
*/
public function convertSavedReportResults($savedreportId, $reportName)
{
$className = $this->getReportClass($reportName);
$object = $this->createReportObject($className);
$savedReport = $this->savedReportFactory->getById($savedreportId);
// Open a zipfile and read the json
$zipFile = $this->config->getSetting('LIBRARY_LOCATION') . $savedReport->storedAs;
// Do some pre-checks on the arguments we have been provided
if (!file_exists($zipFile)) {
throw new InvalidArgumentException(__('File does not exist'));
}
// Open the Zip file
$zip = new \ZipArchive();
if (!$zip->open($zipFile)) {
throw new InvalidArgumentException(__('Unable to open ZIP'));
}
// Get the old json (saved report)
$oldjson = json_decode($zip->getFromName('reportschedule.json'), true);
// Restructure the old json to new json
$json = $object->restructureSavedReportOldJson($oldjson);
// Format the JSON as schemaVersion 2
$fileName = tempnam($this->config->getSetting('LIBRARY_LOCATION') . '/temp/', 'reportschedule');
$out = fopen($fileName, 'w');
fwrite($out, json_encode($json));
fclose($out);
$zip = new \ZipArchive();
$result = $zip->open($zipFile, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
if ($result !== true) {
throw new InvalidArgumentException(__('Can\'t create ZIP. Error Code: %s', $result));
}
$zip->addFile($fileName, 'reportschedule.json');
$zip->close();
// Remove the JSON file
unlink($fileName);
}
/**
* @inheritdoc
*/
public function runReport($reportName, $filterCriteria, $user)
{
$this->log->debug('Run the report to get results');
$className = $this->getReportClass($reportName);
$object = $this->createReportObject($className);
// Set userId
$object->setUser($user);
$filterCriteria = json_decode($filterCriteria, true);
// Retrieve the result array
return $object->getResults($this->sanitizer->getSanitizer($filterCriteria));
}
/**
* @inheritdoc
*/
public function getReportEmailTemplate($reportName)
{
$className = $this->getReportClass($reportName);
$object = $this->createReportObject($className);
// Set Report Schedule form data
return $object->getReportEmailTemplate();
}
/**
* @inheritdoc
*/
public function getSavedReportTemplate($reportName)
{
$className = $this->getReportClass($reportName);
$object = $this->createReportObject($className);
// Set Report Schedule form data
return $object->getSavedReportTemplate();
}
/**
* @inheritdoc
*/
public function getReportChartScript($savedreportId, $reportName)
{
/* @var ReportResult $results */
$results = $this->getSavedReportResults($savedreportId, $reportName);
$className = $this->getReportClass($reportName);
$object = $this->createReportObject($className);
// Set Report Schedule form data
return $object->getReportChartScript($results);
}
}

View File

@@ -0,0 +1,166 @@
<?php
/**
* Copyright (C) 2019 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\Service;
use Slim\Http\ServerRequest as Request;
use Xibo\Factory\SavedReportFactory;
use Xibo\Helper\SanitizerService;
use Xibo\Report\ReportInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Storage\TimeSeriesStoreInterface;
use Xibo\Support\Exception\GeneralException;
/**
* Interface ReportServiceInterface
* @package Xibo\Service
*/
interface ReportServiceInterface
{
/**
* ReportServiceInterface constructor.
* @param \Psr\Container\ContainerInterface $app
* @param StorageServiceInterface $store
* @param TimeSeriesStoreInterface $timeSeriesStore
* @param LogServiceInterface $log
* @param ConfigServiceInterface $config
* @param SanitizerService $sanitizer
* @param SavedReportFactory $savedReportFactory
*/
public function __construct(
$app,
$store,
$timeSeriesStore,
$log,
$config,
$sanitizer,
$savedReportFactory
);
/**
* List all reports that are available
* @return array
*/
public function listReports();
/**
* Get report by report name
* @param string $reportName
* @throws GeneralException
*/
public function getReportByName($reportName);
/**
* Get report class by report name
* @param string $reportName
* @throws GeneralException
*/
public function getReportClass($reportName);
/**
* Create the report object by report classname
* @param string $className
* @throws GeneralException
* @return ReportInterface
*/
public function createReportObject($className);
/**
* Populate form title and hidden fields
* @param string $reportName
* @param Request $request
* @throws GeneralException
* @return array
*/
public function getReportScheduleFormData($reportName, Request $request);
/**
* Set Report Schedule form data
* @param string $reportName
* @param Request $request
* @throws GeneralException
* @return array
*/
public function setReportScheduleFormData($reportName, Request $request);
/**
* Generate saved report name
* @param string $reportName
* @param string $filterCriteria
* @throws GeneralException
* @return string
*/
public function generateSavedReportName($reportName, $filterCriteria);
/**
* Get saved report results
* @param int $savedreportId
* @param string $reportName
* @throws GeneralException
* @return array
*/
public function getSavedReportResults($savedreportId, $reportName);
/**
* Convert saved report results from old schema 1 to schema version 2
* @param int $savedreportId
* @param string $reportName
* @throws GeneralException
* @return array
*/
public function convertSavedReportResults($savedreportId, $reportName);
/**
* Run the report
* @param string $reportName
* @param string $filterCriteria
* @param \Xibo\Entity\User $user
* @throws GeneralException
* @return array
*/
public function runReport($reportName, $filterCriteria, $user);
/**
* Get report email template twig file name
* @param string $reportName
* @throws GeneralException
* @return string
*/
public function getReportEmailTemplate($reportName);
/**
* Get report email template twig file name
* @param string $reportName
* @throws GeneralException
* @return string
*/
public function getSavedReportTemplate($reportName);
/**
* Get chart script
* @param int $savedreportId
* @param string $reportName
* @throws GeneralException
* @return string
*/
public function getReportChartScript($savedreportId, $reportName);
}

View File

@@ -0,0 +1,59 @@
<?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\Service;
use Xibo\Helper\ApplicationState;
use Xibo\Helper\UploadHandler;
/**
* Upload Service to scaffold an upload handler
*/
class UploadService
{
/**
* UploadService constructor.
* @param string $uploadDir
* @param array $settings
* @param LogServiceInterface $logger
* @param ApplicationState $state
*/
public function __construct(
private readonly string $uploadDir,
private readonly array $settings,
private readonly LogServiceInterface $logger,
private readonly ApplicationState $state
) {
}
/**
* Create a new upload handler
* @return UploadHandler
*/
public function createUploadHandler(): UploadHandler
{
// Blue imp requires an extra /
$handler = new UploadHandler($this->uploadDir, $this->logger->getLoggerInterface(), $this->settings, false);
return $handler->setState($this->state);
}
}