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,657 @@
<?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\Connector;
use Carbon\Carbon;
use GuzzleHttp\Exception\GuzzleException;
use Stash\Invalidation;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Xibo\Event\WidgetDataRequestEvent;
use Xibo\Event\WidgetEditOptionRequestEvent;
use Xibo\Support\Exception\ConfigurationException;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
use Xibo\Support\Sanitizer\SanitizerInterface;
use Xibo\Widget\Provider\DataProviderInterface;
/**
* A connector to get data from the AlphaVantage API for use by the Currencies and Stocks Widgets
*/
class AlphaVantageConnector implements ConnectorInterface
{
use ConnectorTrait;
public function registerWithDispatcher(EventDispatcherInterface $dispatcher): ConnectorInterface
{
$dispatcher->addListener(WidgetEditOptionRequestEvent::$NAME, [$this, 'onWidgetEditOption']);
$dispatcher->addListener(WidgetDataRequestEvent::$NAME, [$this, 'onDataRequest']);
return $this;
}
public function getSourceName(): string
{
return 'alphavantage';
}
public function getTitle(): string
{
return 'Alpha Vantage';
}
public function getDescription(): string
{
return 'Get Currencies and Stocks data';
}
public function getThumbnail(): string
{
return '';
}
public function getSettingsFormTwig(): string
{
return 'alphavantage-form-settings';
}
/**
* @param SanitizerInterface $params
* @param array $settings
* @return array
*/
public function processSettingsForm(SanitizerInterface $params, array $settings): array
{
if (!$this->isProviderSetting('apiKey')) {
$settings['apiKey'] = $params->getString('apiKey');
$settings['isPaidPlan'] = $params->getCheckbox('isPaidPlan');
$settings['cachePeriod'] = $params->getInt('cachePeriod');
}
return $settings;
}
/**
* If the requested dataSource is either Currencies or stocks, get the data, process it and add to dataProvider
*
* @param WidgetDataRequestEvent $event
* @return void
*/
public function onDataRequest(WidgetDataRequestEvent $event)
{
$dataProvider = $event->getDataProvider();
if ($dataProvider->getDataSource() === 'currencies' || $dataProvider->getDataSource() === 'stocks') {
if (empty($this->getSetting('apiKey'))) {
$this->getLogger()->debug('onDataRequest: Alpha Vantage not configured.');
return;
}
$event->stopPropagation();
try {
if ($dataProvider->getDataSource() === 'stocks') {
$this->getStockResults($dataProvider);
} else if ($dataProvider->getDataSource() === 'currencies') {
$this->getCurrenciesResults($dataProvider);
}
// If we've got data, then set our cache period.
$event->getDataProvider()->setCacheTtl($this->getSetting('cachePeriod', 3600));
} catch (\Exception $exception) {
$this->getLogger()->error('onDataRequest: Failed to get results. e = ' . $exception->getMessage());
if ($exception instanceof InvalidArgumentException) {
$dataProvider->addError($exception->getMessage());
} else {
$dataProvider->addError(__('Unable to contact the AlphaVantage API'));
}
}
}
}
/**
* If the Widget type is stocks, process it and update options
*
* @param WidgetEditOptionRequestEvent $event
* @return void
* @throws NotFoundException
*/
public function onWidgetEditOption(WidgetEditOptionRequestEvent $event): void
{
$this->getLogger()->debug('onWidgetEditOption');
// Pull the widget we're working with.
$widget = $event->getWidget();
if ($widget === null) {
throw new NotFoundException();
}
// We handle the stocks widget and the property with id="items"
if ($widget->type === 'stocks' && $event->getPropertyId() === 'items') {
if (empty($this->getSetting('apiKey'))) {
$this->getLogger()->debug('onWidgetEditOption: AlphaVantage API not configured.');
return;
}
try {
$results = [];
$bestMatches = $this->getSearchResults($event->getPropertyValue() ?? '');
$this->getLogger()->debug('onWidgetEditOption::getSearchResults => ' . var_export([
'bestMatches' => $bestMatches,
], true));
if ($bestMatches === false) {
$results[] = [
'name' => strtoupper($event->getPropertyValue()),
'type' => strtoupper(trim($event->getPropertyValue())),
'id' => $event->getPropertyId(),
];
} else if (count($bestMatches) > 0) {
foreach($bestMatches as $match) {
$results[] = [
'name' => implode(' ', [$match['1. symbol'], $match['2. name']]),
'type' => $match['1. symbol'],
'id' => $event->getPropertyId(),
];
}
}
$event->setOptions($results);
} catch (\Exception $exception) {
$this->getLogger()->error('onWidgetEditOption: Failed to get symbol search results. e = ' . $exception->getMessage());
}
}
}
/**
* Get Stocks data through symbol search
*
* @param string $keywords
* @return array|bool
* @throws GeneralException
*/
private function getSearchResults(string $keywords): array|bool
{
try {
$this->getLogger()->debug('AlphaVantage Connector : getSearchResults is served from the API.');
$request = $this->getClient()->request('GET', 'https://avg.signcdn.com/query', [
'query' => [
'function' => 'SYMBOL_SEARCH',
'keywords' => $keywords,
]
]);
$data = json_decode($request->getBody(), true);
if (array_key_exists('bestMatches', $data)) {
return $data['bestMatches'];
}
if (array_key_exists('Note', $data)) {
return false;
}
return [];
} catch (GuzzleException $guzzleException) {
throw new GeneralException(
'Guzzle exception getting Stocks data . E = '
. $guzzleException->getMessage(),
$guzzleException->getCode(),
$guzzleException
);
}
}
/**
* Get Stocks data, parse it to an array and add each item to the dataProvider
*
* @throws ConfigurationException
* @throws InvalidArgumentException|GeneralException
*/
private function getStockResults(DataProviderInterface $dataProvider): void
{
// Construct the YQL
// process items
$items = $dataProvider->getProperty('items');
if ($items == '') {
$this->getLogger()->error('Missing Items for Stocks Module with WidgetId ' . $dataProvider->getWidgetId());
throw new InvalidArgumentException(__('Add some stock symbols'), 'items');
}
// Parse items out into an array
$items = array_map('trim', explode(',', $items));
foreach ($items as $symbol) {
try {
// Does this symbol have any additional data
$parsedSymbol = explode('|', $symbol);
$symbol = $parsedSymbol[0];
$name = ($parsedSymbol[1] ?? $symbol);
$currency = ($parsedSymbol[2] ?? '');
$result = $this->getStockQuote($symbol, $this->getSetting('isPaidPlan'));
$this->getLogger()->debug(
'AlphaVantage Connector : getStockResults data: ' .
var_export($result, true)
);
$item = [];
foreach ($result['Time Series (Daily)'] as $series) {
$item = [
'Name' => $name,
'Symbol' => $symbol,
'time' => $result['Meta Data']['3. Last Refreshed'],
'LastTradePriceOnly' => round($series['4. close'], 4),
'RawLastTradePriceOnly' => $series['4. close'],
'YesterdayTradePriceOnly' => round($series['1. open'], 4),
'RawYesterdayTradePriceOnly' => $series['1. open'],
'TimeZone' => $result['Meta Data']['5. Time Zone'],
'Currency' => $currency
];
$item['Change'] = round($item['RawLastTradePriceOnly'] - $item['RawYesterdayTradePriceOnly'], 4);
$item['SymbolTrimmed'] = explode('.', $item['Symbol'])[0];
$item = $this->decorateWithReplacements($item);
break;
}
// Parse the result and add it to our data array
$dataProvider->addItem($item);
$dataProvider->setIsHandled();
} catch (InvalidArgumentException $invalidArgumentException) {
$this->getLogger()->error('Invalid symbol ' . $symbol . ', e: ' . $invalidArgumentException->getMessage());
throw new InvalidArgumentException(__('Invalid symbol ' . $symbol), 'items');
}
}
}
/**
* Call Alpha Vantage API to get Stocks data, different endpoint depending on the paidPlan
* cache results for cachePeriod defined in the Connector
*
* @param string $symbol
* @param ?int $isPaidPlan
* @return array
* @throws GeneralException
*/
protected function getStockQuote(string $symbol, ?int $isPaidPlan): array
{
try {
$cache = $this->getPool()->getItem('/widget/stock/api_'.md5($symbol));
$cache->setInvalidationMethod(Invalidation::SLEEP, 5000, 15);
$data = $cache->get();
if ($cache->isMiss()) {
$this->getLogger()->debug('AlphaVantage Connector : getStockQuote is served from the API.');
$request = $this->getClient()->request('GET', 'https://www.alphavantage.co/query', [
'query' => [
'function' => $isPaidPlan === 1 ? 'TIME_SERIES_DAILY_ADJUSTED' : 'TIME_SERIES_DAILY',
'symbol' => $symbol,
'apikey' => $this->getSetting('apiKey')
]
]);
$data = json_decode($request->getBody(), true);
if (!array_key_exists('Time Series (Daily)', $data)) {
$this->getLogger()->debug('getStockQuote Data: ' . var_export($data, true));
throw new InvalidArgumentException(__('Stocks data invalid'), 'Time Series (Daily)');
}
// Cache this and expire in the cache period
$cache->set($data);
$cache->expiresAt(Carbon::now()->addSeconds($this->getSetting('cachePeriod', 14400)));
$this->getPool()->save($cache);
} else {
$this->getLogger()->debug('AlphaVantage Connector : getStockQuote is served from the cache.');
}
return $data;
} catch (GuzzleException $guzzleException) {
throw new GeneralException(
'Guzzle exception getting Stocks data . E = ' .
$guzzleException->getMessage(),
$guzzleException->getCode(),
$guzzleException
);
}
}
/**
* Replacements shared between Stocks and Currencies
*
* @param array $item
* @return array
*/
private function decorateWithReplacements(array $item): array
{
if (($item['Change'] == null || $item['LastTradePriceOnly'] == null)) {
$item['ChangePercentage'] = '0';
} else {
// Calculate the percentage dividing the change by the ( previous value minus the change )
$percentage = $item['Change'] / ( $item['LastTradePriceOnly'] - $item['Change'] );
// Convert the value to percentage and round it
$item['ChangePercentage'] = round($percentage*100, 2);
}
if (($item['Change'] != null && $item['LastTradePriceOnly'] != null)) {
if ($item['Change'] > 0) {
$item['ChangeIcon'] = 'up-arrow';
$item['ChangeStyle'] = 'value-up';
} else if ($item['Change'] < 0) {
$item['ChangeIcon'] = 'down-arrow';
$item['ChangeStyle'] = 'value-down';
}
} else {
$item['ChangeStyle'] = 'value-equal';
$item['ChangeIcon'] = 'right-arrow';
}
return $item;
}
/**
* Get Currencies data from Alpha Vantage, parse it and add to dataProvider
*
* @param DataProviderInterface $dataProvider
* @return void
* @throws InvalidArgumentException
*/
private function getCurrenciesResults(DataProviderInterface $dataProvider): void
{
// What items/base currencies are we interested in?
$items = $dataProvider->getProperty('items');
$base = $dataProvider->getProperty('base');
if (empty($items) || empty($base)) {
$this->getLogger()->error(
'Missing Items for Currencies Module with WidgetId ' .
$dataProvider->getWidgetId()
);
throw new InvalidArgumentException(
__('Missing Items for Currencies Module. Please provide items in order to proceed.'),
'items'
);
}
// Does this require a reversed conversion?
$reverseConversion = ($dataProvider->getProperty('reverseConversion', 0) == 1);
// Is this paid plan?
$isPaidPlan = ($this->getSetting('isPaidPlan', 0) == 1);
// Parse items out into an array
$items = array_map('trim', explode(',', $items));
// Ensure base isn't also in the items list (Currencies)
if (in_array($base, $items)) {
$this->getLogger()->error(
'Invalid Currencies: Base "' . $base . '" also included in Items for ' .
'Currencies Module with WidgetId ' . $dataProvider->getWidgetId()
);
throw new InvalidArgumentException(
__('Base currency must not be included in the Currencies list. Please remove it and try again.'),
'items'
);
}
// Each item we want is a call to the results API
try {
foreach ($items as $currency) {
// Remove the multiplier if there's one (this is handled when we substitute the results into
// the template)
$currency = explode('|', $currency)[0];
// Do we need to reverse the from/to currency for this comparison?
$result = $reverseConversion
? $this->getCurrencyExchangeRate($currency, $base, $isPaidPlan)
: $this->getCurrencyExchangeRate($base, $currency, $isPaidPlan);
$this->getLogger()->debug(
'AlphaVantage Connector : getCurrenciesResults are: ' .
var_export($result, true)
);
if ($isPaidPlan) {
$item = [
'time' => $result['Realtime Currency Exchange Rate']['6. Last Refreshed'],
'ToName' => $result['Realtime Currency Exchange Rate']['3. To_Currency Code'],
'FromName' => $result['Realtime Currency Exchange Rate']['1. From_Currency Code'],
'Bid' => round($result['Realtime Currency Exchange Rate']['5. Exchange Rate'], 4),
'Ask' => round($result['Realtime Currency Exchange Rate']['5. Exchange Rate'], 4),
'LastTradePriceOnly' => round($result['Realtime Currency Exchange Rate']['5. Exchange Rate'], 4),
'RawLastTradePriceOnly' => $result['Realtime Currency Exchange Rate']['5. Exchange Rate'],
'TimeZone' => $result['Realtime Currency Exchange Rate']['7. Time Zone'],
];
} else {
$item = [
'time' => $result['Meta Data']['5. Last Refreshed'],
'ToName' => $result['Meta Data']['3. To Symbol'],
'FromName' => $result['Meta Data']['2. From Symbol'],
'Bid' => round(array_values($result['Time Series FX (Daily)'])[0]['1. open'], 4),
'Ask' => round(array_values($result['Time Series FX (Daily)'])[0]['1. open'], 4),
'LastTradePriceOnly' => round(array_values($result['Time Series FX (Daily)'])[0]['1. open'], 4),
'RawLastTradePriceOnly' => array_values($result['Time Series FX (Daily)'])[0]['1. open'],
'TimeZone' => $result['Meta Data']['6. Time Zone'],
];
}
// Set the name/currency to be the full name including the base currency
$item['Name'] = $item['FromName'] . '/' . $item['ToName'];
$currencyName = ($reverseConversion) ? $item['FromName'] : $item['ToName'];
$item['NameShort'] = $currencyName;
// work out the change when compared to the previous day
// We need to get the prior day for this pair only (reversed)
$priorDay = $reverseConversion
? $this->getCurrencyPriorDay($currency, $base, $isPaidPlan)
: $this->getCurrencyPriorDay($base, $currency, $isPaidPlan);
/*$this->getLog()->debug('Percentage change requested, prior day is '
. var_export($priorDay['Time Series FX (Daily)'], true));*/
$priorDay = count($priorDay['Time Series FX (Daily)']) < 2
? ['1. open' => 1]
: array_values($priorDay['Time Series FX (Daily)'])[1];
$item['YesterdayTradePriceOnly'] = $priorDay['1. open'];
$item['Change'] = $item['RawLastTradePriceOnly'] - $item['YesterdayTradePriceOnly'];
$item = $this->decorateWithReplacements($item);
$this->getLogger()->debug(
'AlphaVantage Connector : Parsed getCurrenciesResults are: ' .
var_export($item, true)
);
$dataProvider->addItem($item);
$dataProvider->setIsHandled();
}
} catch (GeneralException $requestException) {
$this->getLogger()->error('Problem getting currency information. E = ' . $requestException->getMessage());
$this->getLogger()->debug($requestException->getTraceAsString());
return;
}
}
/**
* Call Alpha Vantage API to get Currencies data, different endpoint depending on the paidPlan
* cache results for cachePeriod defined on the Connector
*
* @param string $fromCurrency
* @param string $toCurrency
* @param bool $isPaidPlan
* @return mixed
* @throws GeneralException
* @throws InvalidArgumentException
*/
private function getCurrencyExchangeRate(string $fromCurrency, string $toCurrency, bool $isPaidPlan)
{
try {
$cache = $this->getPool()->getItem('/widget/currency/' . md5($fromCurrency . $toCurrency . $isPaidPlan));
$cache->setInvalidationMethod(Invalidation::SLEEP, 5000, 15);
$data = $cache->get();
if ($cache->isMiss()) {
$this->getLogger()->debug('AlphaVantage Connector : getCurrencyExchangeRate is served from the API.');
// Use a different function depending on whether we have a paid plan or not.
if ($isPaidPlan) {
$query = [
'function' => 'CURRENCY_EXCHANGE_RATE',
'from_currency' => $fromCurrency,
'to_currency' => $toCurrency,
];
} else {
$query = [
'function' => 'FX_DAILY',
'from_symbol' => $fromCurrency,
'to_symbol' => $toCurrency,
];
}
$query['apikey'] = $this->getSetting('apiKey');
$request = $this->getClient()->request('GET', 'https://www.alphavantage.co/query', [
'query' => $query
]);
$data = json_decode($request->getBody(), true);
if ($isPaidPlan) {
if (!array_key_exists('Realtime Currency Exchange Rate', $data)) {
$this->getLogger()->debug('Data: ' . var_export($data, true));
throw new InvalidArgumentException(
__('Currency data invalid'),
'Realtime Currency Exchange Rate'
);
}
} else {
if (!array_key_exists('Meta Data', $data)) {
$this->getLogger()->debug('Data: ' . var_export($data, true));
throw new InvalidArgumentException(__('Currency data invalid'), 'Meta Data');
}
if (!array_key_exists('Time Series FX (Daily)', $data)) {
$this->getLogger()->debug('Data: ' . var_export($data, true));
throw new InvalidArgumentException(__('Currency data invalid'), 'Time Series FX (Daily)');
}
}
// Cache this and expire in the cache period
$cache->set($data);
$cache->expiresAt(Carbon::now()->addSeconds($this->getSetting('cachePeriod', 14400)));
$this->getPool()->save($cache);
} else {
$this->getLogger()->debug('AlphaVantage Connector : getCurrencyExchangeRate is served from the cache.');
}
return $data;
} catch (GuzzleException $guzzleException) {
throw new GeneralException(
'Guzzle exception getting currency exchange rate. E = ' .
$guzzleException->getMessage(),
$guzzleException->getCode(),
$guzzleException
);
}
}
/**
* Call Alpha Vantage API to get currencies data, cache results for a day
*
* @param $fromCurrency
* @param $toCurrency
* @param $isPaidPlan
* @return mixed
* @throws GeneralException
* @throws InvalidArgumentException
*/
private function getCurrencyPriorDay($fromCurrency, $toCurrency, $isPaidPlan)
{
if ($isPaidPlan) {
$key = md5($fromCurrency . $toCurrency . Carbon::yesterday()->format('Y-m-d') . '1');
} else {
$key = md5($fromCurrency . $toCurrency . '0');
}
try {
$cache = $this->getPool()->getItem('/widget/Currencies/' . $key);
$cache->setInvalidationMethod(Invalidation::SLEEP, 5000, 15);
$data = $cache->get();
if ($cache->isMiss()) {
$this->getLogger()->debug('AlphaVantage Connector : getPriorDay is served from the API.');
// Use a web request
$request = $this->getClient()->request('GET', 'https://www.alphavantage.co/query', [
'query' => [
'function' => 'FX_DAILY',
'from_symbol' => $fromCurrency,
'to_symbol' => $toCurrency,
'apikey' => $this->getSetting('apiKey')
]
]);
$data = json_decode($request->getBody(), true);
if (!array_key_exists('Meta Data', $data)) {
$this->getLogger()->debug('Data: ' . var_export($data, true));
throw new InvalidArgumentException(__('Currency data invalid'), 'Meta Data');
}
if (!array_key_exists('Time Series FX (Daily)', $data)) {
$this->getLogger()->debug('Data: ' . var_export($data, true));
throw new InvalidArgumentException(__('Currency data invalid'), 'Time Series FX (Daily)');
}
// Cache this and expire tomorrow (results are valid for the entire day regardless of settings)
$cache->set($data);
$cache->expiresAt(Carbon::tomorrow());
$this->getPool()->save($cache);
} else {
$this->getLogger()->debug('AlphaVantage Connector : getPriorDay is served from the cache.');
}
return $data;
} catch (GuzzleException $guzzleException) {
throw new GeneralException(
'Guzzle exception getting currency exchange rate. E = ' .
$guzzleException->getMessage(),
$guzzleException->getCode(),
$guzzleException
);
}
}
}

View File

@@ -0,0 +1,575 @@
<?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\Connector;
use Carbon\Carbon;
use DOMDocument;
use DOMElement;
use Exception;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\RequestException;
use Location\Coordinate;
use Location\Polygon;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Xibo\Event\ScheduleCriteriaRequestEvent;
use Xibo\Event\ScheduleCriteriaRequestInterface;
use Xibo\Event\WidgetDataRequestEvent;
use Xibo\Factory\DisplayFactory;
use Xibo\Support\Exception\ConfigurationException;
use Xibo\Support\Sanitizer\SanitizerInterface;
use Xibo\Widget\Provider\DataProviderInterface;
use Xibo\XMR\ScheduleCriteriaUpdateAction;
/**
* A connector to process Common Alerting Protocol (CAP) Data
*/
class CapConnector implements ConnectorInterface, EmergencyAlertInterface
{
use ConnectorTrait;
/** @var DOMDocument */
protected DOMDocument $capXML;
/** @var DOMElement */
protected DOMElement $infoNode;
/** @var DOMElement */
protected DOMElement $areaNode;
/** @var DisplayFactory */
private DisplayFactory $displayFactory;
/**
* @param ContainerInterface $container
* @return ConnectorInterface
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function setFactories(ContainerInterface $container): ConnectorInterface
{
$this->displayFactory = $container->get('displayFactory');
return $this;
}
public function registerWithDispatcher(EventDispatcherInterface $dispatcher): ConnectorInterface
{
$dispatcher->addListener(WidgetDataRequestEvent::$NAME, [$this, 'onDataRequest']);
$dispatcher->addListener(ScheduleCriteriaRequestEvent::$NAME, [$this, 'onScheduleCriteriaRequest']);
return $this;
}
public function getSourceName(): string
{
return 'cap-connector';
}
public function getTitle(): string
{
return 'CAP Connector';
}
public function getDescription(): string
{
return 'Common Alerting Protocol';
}
public function getThumbnail(): string
{
return 'theme/default/img/connectors/xibo-cap.png';
}
public function getSettingsFormTwig(): string
{
return '';
}
public function processSettingsForm(SanitizerInterface $params, array $settings): array
{
return [];
}
/**
* If the requested dataSource is emergency-alert, get the data, process it and add to dataProvider
*
* @param WidgetDataRequestEvent $event
* @return void
* @throws GuzzleException
*/
public function onDataRequest(WidgetDataRequestEvent $event): void
{
if ($event->getDataProvider()->getDataSource() !== 'emergency-alert') {
return;
}
$event->stopPropagation();
try {
// check if CAP URL is present
if (empty($event->getDataProvider()->getProperty('emergencyAlertUri'))) {
$this->getLogger()->debug('onDataRequest: Emergency alert not configured.');
$event->getDataProvider()->addError(__('Missing CAP URL'));
return;
}
// Set cache expiry date to 3 minutes from now
$cacheExpire = Carbon::now()->addMinutes(3);
// Fetch the CAP XML content from the given URL
$xmlContent = $this->fetchCapAlertFromUrl($event->getDataProvider(), $cacheExpire);
if ($xmlContent) {
// Initialize DOMDocument and load the XML content
$this->capXML = new DOMDocument();
$this->capXML->loadXML($xmlContent);
// Process and initialize CAP data
$this->processCapData($event->getDataProvider());
// Initialize update interval
$updateIntervalMinute = $event->getDataProvider()->getProperty('updateInterval');
// Convert the $updateIntervalMinute to seconds
$updateInterval = $updateIntervalMinute * 60;
// If we've got data, then set our cache period.
$event->getDataProvider()->setCacheTtl($updateInterval);
$event->getDataProvider()->setIsHandled();
$capStatus = $this->getCapXmlData('status');
$category = $this->getCapXmlData('category');
} else {
$capStatus = 'No Alerts';
$category = '';
}
// initialize status for schedule criteria push message
if ($capStatus == 'Actual') {
$status = self::ACTUAL_ALERT;
} elseif ($capStatus == 'No Alerts') {
$status = self::NO_ALERT;
} else {
$status = self::TEST_ALERT;
}
$this->getLogger()->debug('Schedule criteria push message: status = ' . $status
. ', category = ' . $category);
// Set ttl expiry to 180s since widget sync task runs every 180s and add a bit of buffer
$ttl = max($updateInterval ?? 180, 180) + 60;
// Set schedule criteria update
$action = new ScheduleCriteriaUpdateAction();
// Adjust the QOS value lower than the data update QOS to ensure it arrives first
$action->setQos(3);
$action->setCriteriaUpdates([
['metric' => 'emergency_alert_status', 'value' => $status, 'ttl' => $ttl],
['metric' => 'emergency_alert_category', 'value' => $category, 'ttl' => $ttl]
]);
// Initialize the display
$displayId = $event->getDataProvider()->getDisplayId();
$display = $this->displayFactory->getById($displayId);
// Criteria push message
$this->getPlayerActionService()->sendAction($display, $action);
} catch (Exception $exception) {
$this->getLogger()
->error('onDataRequest: Failed to get results. e = ' . $exception->getMessage());
$event->getDataProvider()->addError(__('Unable to get Common Alerting Protocol (CAP) results.'));
}
}
/**
* Get and process the CAP data
*
* @throws Exception
*/
private function processCapData(DataProviderInterface $dataProvider): void
{
// Array to store configuration data
$config = [];
// Initialize configuration data
$config['status'] = $dataProvider->getProperty('status');
$config['msgType'] = $dataProvider->getProperty('msgType');
$config['scope'] = $dataProvider->getProperty('scope');
$config['category'] = $dataProvider->getProperty('category');
$config['responseType'] = $dataProvider->getProperty('responseType');
$config['urgency'] = $dataProvider->getProperty('urgency');
$config['severity'] = $dataProvider->getProperty('severity');
$config['certainty'] = $dataProvider->getProperty('certainty');
$config['isAreaSpecific'] = $dataProvider->getProperty('isAreaSpecific');
// Retrieve specific values from the CAP XML for filtering
$status = $this->getCapXmlData('status');
$msgType = $this->getCapXmlData('msgType');
$scope = $this->getCapXmlData('scope');
// Check if the retrieved CAP data matches the configuration filters
if (!$this->matchesFilter($status, $config['status']) ||
!$this->matchesFilter($msgType, $config['msgType']) ||
!$this->matchesFilter($scope, $config['scope'])) {
return;
}
// Array to store CAP values
$cap = [];
// Initialize CAP values
$cap['source'] = $this->getCapXmlData('source');
$cap['note'] = $this->getCapXmlData('note');
// Get all <info> elements
$infoNodes = $this->capXML->getElementsByTagName('info');
foreach ($infoNodes as $infoNode) {
$this->infoNode = $infoNode;
// Extract values from the current <info> node for filtering
$category = $this->getInfoData('category');
$responseType = $this->getInfoData('responseType');
$urgency = $this->getInfoData('urgency');
$severity = $this->getInfoData('severity');
$certainty = $this->getInfoData('certainty');
// Check if the current <info> node matches all filters
if (!$this->matchesFilter($category, $config['category']) ||
!$this->matchesFilter($responseType, $config['responseType']) ||
!$this->matchesFilter($urgency, $config['urgency']) ||
!$this->matchesFilter($severity, $config['severity']) ||
!$this->matchesFilter($certainty, $config['certainty'])) {
continue;
}
// Initialize the rest of the CAP values
$cap['event'] = $this->getInfoData('event');
$cap['urgency'] = $this->getInfoData('urgency');
$cap['severity'] = $this->getInfoData('severity');
$cap['certainty'] = $this->getInfoData('certainty');
$cap['dateTimeEffective'] = $this->getInfoData('effective');
$cap['dateTimeOnset'] = $this->getInfoData('onset');
$cap['dateTimeExpires'] = $this->getInfoData('expires');
$cap['senderName'] = $this->getInfoData('senderName');
$cap['headline'] = $this->getInfoData('headline');
$cap['description'] = $this->getInfoData('description');
$cap['instruction'] = $this->getInfoData('instruction');
$cap['contact'] = $this->getInfoData('contact');
// Retrieve all <area> elements within the current <info> element
$areaNodes = $this->infoNode->getElementsByTagName('area');
if (empty($areaNodes->length)) {
// If we don't have <area> elements, then provide CAP without the Area
$dataProvider->addItem($cap);
} else {
// Iterate through each <area> element
foreach ($areaNodes as $areaNode) {
$this->areaNode = $areaNode;
$circle = $this->getAreaData('circle');
$polygon = $this->getAreaData('polygon');
$cap['areaDesc'] = $this->getAreaData('areaDesc');
// Check if the area-specific filter is enabled
if ($config['isAreaSpecific']) {
if ($circle || $polygon) {
// Get the current display coordinates
$displayLatitude = $dataProvider->getDisplayLatitude();
$displayLongitude = $dataProvider->getDisplayLongitude();
// Retrieve area coordinates (circle or polygon) from CAP XML
$areaCoordinates = $this->getAreaCoordinates();
// Check if display coordinates matches the CAP alert area
if ($this->isWithinArea($displayLatitude, $displayLongitude, $areaCoordinates)) {
$dataProvider->addItem($cap);
}
} else {
// Provide CAP data if no coordinate/s is provided
$dataProvider->addItem($cap);
}
} else {
// Provide CAP data if area-specific filter is disabled
$dataProvider->addItem($cap);
}
}
}
}
}
/**
* Fetches the CAP (Common Alerting Protocol) XML data from the provided emergency alert URL.
*
* @param DataProviderInterface $dataProvider
* @param Carbon $cacheExpiresAt
*
* @return string|null
* @throws GuzzleException
*/
private function fetchCapAlertFromUrl(DataProviderInterface $dataProvider, Carbon $cacheExpiresAt): string|null
{
$emergencyAlertUrl = $dataProvider->getProperty('emergencyAlertUri');
$cache = $this->pool->getItem('/emergency-alert/cap/' . md5($emergencyAlertUrl));
$data = $cache->get();
if ($cache->isMiss()) {
$cache->lock();
$this->getLogger()->debug('Getting CAP data from CAP Feed');
$httpOptions = [
'timeout' => 20, // Wait no more than 20 seconds
];
try {
// Make a GET request to the CAP URL using Guzzle HTTP client with defined options
$response = $dataProvider
->getGuzzleClient($httpOptions)
->get($emergencyAlertUrl);
$this->getLogger()->debug('CAP Feed: uri: ' . $emergencyAlertUrl . ' httpOptions: '
. json_encode($httpOptions));
// Get the response body as a string
$data = $response->getBody()->getContents();
// Cache
$cache->set($data);
$cache->expiresAt($cacheExpiresAt);
$this->pool->saveDeferred($cache);
} catch (RequestException $e) {
// Log the error with a message specific to CAP data fetching
$this->getLogger()->error('Unable to reach the CAP feed URL: '
. $emergencyAlertUrl . ' Error: ' . $e->getMessage());
// Throw a more specific exception message
$dataProvider->addError(__('Failed to retrieve CAP data from the specified URL.'));
}
} else {
$this->getLogger()->debug('Getting CAP data from cache');
}
return $data;
}
/**
* Get the value of a specified tag from the CAP XML document.
*
* @param string $tagName
* @return string|null
*/
private function getCapXmlData(string $tagName): ?string
{
// Ensure the XML is loaded and the tag exists
$node = $this->capXML->getElementsByTagName($tagName)->item(0);
// Return the node value if the node exists, otherwise return an empty string
return $node ? $node->nodeValue : '';
}
/**
* Get the value of a specified tag from the current <info> node.
*
* @param string $tagName
* @return string|null
*/
private function getInfoData(string $tagName): ?string
{
// Ensure the tag exists within the provided <info> node
$node = $this->infoNode->getElementsByTagName($tagName)->item(0);
// Return the node value if the node exists, otherwise return an empty string
return $node ? $node->nodeValue : '';
}
/**
* Get the value of a specified tag from the current <area> node.
*
* @param string $tagName
* @return string|null
*/
private function getAreaData(string $tagName): ?string
{
// Ensure the tag exists within the provided <area> node
$node = $this->areaNode->getElementsByTagName($tagName)->item(0);
// Return the node value if the node exists, otherwise return an empty string
return $node ? $node->nodeValue : '';
}
/**
* Check if the value of a CAP XML element matches the expected filter value.
*
* @param string $actualValue
* @param string $expectedValue
*
* @return bool
*/
private function matchesFilter(string $actualValue, string $expectedValue): bool
{
// If the expected value is 'Any' (empty string) or matches the actual value, the filter passes
if (empty($expectedValue) || $expectedValue == $actualValue) {
return true;
}
return false;
}
/**
* Get area coordinates from CAP XML data.
*
* Determines if the area is defined as a circle or polygon
* and returns the relevant data.
*
* @return array An array with the area type and coordinates.
*/
private function getAreaCoordinates(): array
{
// array to store coordinates data
$area = [];
// Check for a circle area element
$circle = $this->getAreaData('circle');
if ($circle) {
// Split the circle data into center coordinates and radius
$circleParts = explode(' ', $circle);
$center = explode(',', $circleParts[0]); // "latitude,longitude"
$radius = $circleParts[1];
$area['type'] = 'circle';
$area['center'] = ['lat' => $center[0], 'lon' => $center[1]];
$area['radius'] = $radius;
return $area;
}
// Check for a polygon area element
$polygon = $this->getAreaData('polygon');
if ($polygon) {
// Split the polygon data into multiple points ("lat1,lon1 lat2,lon2 ...")
$points = explode(' ', $polygon);
// Array to store multiple coordinates
$polygonPoints = [];
foreach ($points as $point) {
$coords = explode(',', $point);
$polygonPoints[] = ['lat' => $coords[0], 'lon' => $coords[1]];
}
$area['type'] = 'polygon';
$area['points'] = $polygonPoints;
}
return $area;
}
/**
* Checks if the provided display coordinates are inside a defined area (circle or polygon).
* If no area coordinates are available, it returns false.
*
* @param float $displayLatitude
* @param float $displayLongitude
* @param array $areaCoordinates The coordinates defining the area (circle or polygon).
*
* @return bool
*/
private function isWithinArea(float $displayLatitude, float $displayLongitude, array $areaCoordinates): bool
{
if (empty($areaCoordinates)) {
// No area coordinates available
return false;
}
// Initialize the display coordinate
$displayCoordinate = new Coordinate($displayLatitude, $displayLongitude);
if ($areaCoordinates['type'] == 'circle') {
// Initialize the circle's coordinate and radius
$centerCoordinate = new Coordinate($areaCoordinates['center']['lat'], $areaCoordinates['center']['lon']);
$radius = $areaCoordinates['radius'];
// Check if the display is within the specified radius of the center coordinate
if ($centerCoordinate->hasSameLocation($displayCoordinate, $radius)) {
return true;
}
} else {
// Initialize a new polygon
$geofence = new Polygon();
// Add each point to the polygon
foreach ($areaCoordinates['points'] as $point) {
$geofence->addPoint(new Coordinate($point['lat'], $point['lon']));
}
// Check if the display is within the polygon
if ($geofence->contains($displayCoordinate)) {
return true;
}
}
return false;
}
/**
* @param ScheduleCriteriaRequestInterface $event
* @return void
* @throws ConfigurationException
*/
public function onScheduleCriteriaRequest(ScheduleCriteriaRequestInterface $event): void
{
// Initialize Emergency Alerts schedule criteria parameters
$event->addType('emergency_alert', __('Emergency Alerts'))
->addMetric('emergency_alert_status', __('Status'))
->addCondition([
'eq' => __('Equal to')
])
->addValues('dropdown', [
self::ACTUAL_ALERT => __('Actual Alerts'),
self::TEST_ALERT => __('Test Alerts'),
self::NO_ALERT => __('No Alerts')
])
->addMetric('emergency_alert_category', __('Category'))
->addCondition([
'eq' => __('Equal to')
])
->addValues('dropdown', [
'Geo' => __('Geo'),
'Met' => __('Met'),
'Safety' => __('Safety'),
'Security' => __('Security'),
'Rescue' => __('Rescue'),
'Fire' => __('Fire'),
'Health' => __('Health'),
'Env' => __('Env'),
'Transport' => __('Transport'),
'Infra' => __('Infra'),
'CBRNE' => __('CBRNE'),
'Other' => __('Other'),
]);
}
}

View File

@@ -0,0 +1,57 @@
<?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\Connector;
use GuzzleHttp\Client;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Stash\Interfaces\PoolInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Xibo\Service\JwtServiceInterface;
use Xibo\Service\PlayerActionServiceInterface;
use Xibo\Support\Sanitizer\SanitizerInterface;
/**
* Connector Interface
*/
interface ConnectorInterface
{
public function setFactories(ContainerInterface $container): ConnectorInterface;
public function registerWithDispatcher(EventDispatcherInterface $dispatcher): ConnectorInterface;
public function useLogger(LoggerInterface $logger): ConnectorInterface;
public function useSettings(array $settings, bool $isProvider = true): ConnectorInterface;
public function usePool(PoolInterface $pool): ConnectorInterface;
public function useHttpOptions(array $httpOptions): ConnectorInterface;
public function useJwtService(JwtServiceInterface $jwtService): ConnectorInterface;
public function usePlayerActionService(PlayerActionServiceInterface $playerActionService): ConnectorInterface;
public function getClient(): Client;
public function getSourceName(): string;
public function getTitle(): string;
public function getDescription(): string;
public function getThumbnail(): string;
public function getSetting($setting, $default = null);
public function isProviderSetting($setting): bool;
public function getSettingsFormTwig(): string;
public function getSettingsFormJavaScript(): string;
public function processSettingsForm(SanitizerInterface $params, array $settings): array;
}

View File

@@ -0,0 +1,202 @@
<?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\Connector;
use GuzzleHttp\Client;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Stash\Interfaces\PoolInterface;
use Xibo\Service\JwtServiceInterface;
use Xibo\Service\PlayerActionServiceInterface;
/**
* Connector trait to assist with basic scaffolding and utility methods.
* we recommend all connectors use this trait.
*/
trait ConnectorTrait
{
/** @var \Psr\Log\LoggerInterface */
private $logger;
/** @var array */
private $settings = [];
/** @var array The keys for all provider settings */
private $providerSettings = [];
/** @var PoolInterface|null */
private $pool;
/** @var array */
private $httpOptions = [];
/** @var array */
private $keys = [];
/** @var JwtServiceInterface */
private $jwtService;
/** @var PlayerActionServiceInterface */
private $playerActionService;
/**
* @param \Psr\Log\LoggerInterface $logger
* @return \Xibo\Connector\ConnectorInterface
*/
public function useLogger(LoggerInterface $logger): ConnectorInterface
{
$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 array $settings
* @param bool $provider
* @return ConnectorInterface
*/
public function useSettings(array $settings, bool $provider = false): ConnectorInterface
{
if ($provider) {
$this->providerSettings = array_keys($settings);
}
$this->settings = array_merge($this->settings, $settings);
return $this;
}
/**
* @param $setting
* @return bool
*/
public function isProviderSetting($setting): bool
{
return in_array($setting, $this->providerSettings);
}
/**
* @param $setting
* @param null $default
* @return string|null
*/
public function getSetting($setting, $default = null)
{
$this->logger->debug('getSetting: ' . $setting);
if (!array_key_exists($setting, $this->settings)) {
$this->logger->debug('getSetting: ' . $setting . ' not present.');
return $default;
}
return $this->settings[$setting] ?: $default;
}
/**
* @param \Stash\Interfaces\PoolInterface $pool
* @return \Xibo\Connector\ConnectorInterface
*/
public function usePool(PoolInterface $pool): ConnectorInterface
{
$this->pool = $pool;
return $this;
}
/**
* @return \Stash\Interfaces\PoolInterface
*/
private function getPool(): PoolInterface
{
return $this->pool;
}
/**
* @param array $options
* @return \Xibo\Connector\ConnectorInterface
*/
public function useHttpOptions(array $options): ConnectorInterface
{
$this->httpOptions = $options;
return $this;
}
public function useJwtService(JwtServiceInterface $jwtService): ConnectorInterface
{
$this->jwtService = $jwtService;
return $this;
}
protected function getJwtService(): JwtServiceInterface
{
return $this->jwtService;
}
public function usePlayerActionService(PlayerActionServiceInterface $playerActionService): ConnectorInterface
{
$this->playerActionService = $playerActionService;
return $this;
}
protected function getPlayerActionService(): PlayerActionServiceInterface
{
return $this->playerActionService;
}
public function setFactories($container): ConnectorInterface
{
return $this;
}
public function getSettingsFormJavaScript(): string
{
return '';
}
/**
* Get an HTTP client with the default proxy settings, etc
* @return \GuzzleHttp\Client
*/
public function getClient(): Client
{
return new Client($this->httpOptions);
}
/**
* Return a layout preview URL for the provided connector token
* this can be used in a data request and is decorated by the previewing function.
* @param string $token
* @return string
*/
public function getTokenUrl(string $token): string
{
return '[[connector='.$token.']]';
}
}

View File

@@ -0,0 +1,53 @@
<?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\Connector;
/**
* Interface for handling the DataConnectorScriptRequestEvent.
*
* Provides methods for connectors to supply their data connector JS code.
*
* These methods should be used together:
* - Use getConnectorId() to retrieve the unique identifier of the connector provided in the event.
* - Check if the connector's ID matches the ID provided in the event.
* - If the IDs match, use setScript() to provide the JavaScript code for the data connector.
*
* This ensures that the correct script is supplied by the appropriate connector.
*/
interface DataConnectorScriptProviderInterface
{
/**
* Get the unique identifier of the connector that is selected as the data source for the dataset.
*
* @return string
*/
public function getConnectorId(): string;
/**
* Set the data connector JavaScript code provided by the connector. Requires real time.
*
* @param string $script JavaScript code
* @return void
*/
public function setScript(string $script): void;
}

View File

@@ -0,0 +1,43 @@
<?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\Connector;
use InvalidArgumentException;
/**
* Interface for handling the DataConnectorSourceRequestEvent.
*
* Registers connectors that provide data connector JavaScript (JS).
*/
interface DataConnectorSourceProviderInterface
{
/**
* Adds/Registers a connector, that would provide a data connector JS, to the event.
* Implementations should use $this->getSourceName() as the $id and $this->getTitle() as the $name.
*
* @param string $id
* @param string $name
* @throws InvalidArgumentException if a duplicate ID or name is found.
*/
public function addDataConnectorSource(string $id, string $name): void;
}

View File

@@ -0,0 +1,45 @@
<?php
/*
* Copyright (C) 2025 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\Connector;
/**
* Connector Interface for Emergency Alerts
*/
interface EmergencyAlertInterface
{
/**
* Represents the status when there is at least one alert of type "Actual".
*/
public const ACTUAL_ALERT = 'actual_alerts';
/**
* Represents the status when there are no alerts of any type.
*/
public const NO_ALERT = 'no_alerts';
/**
* Represents the status when there is at least one test alert
* (e.g., Exercise, System, Test, Draft).
*/
public const TEST_ALERT = 'test_alerts';
}

View File

@@ -0,0 +1,418 @@
<?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\Connector;
use Carbon\Carbon;
use DOMDocument;
use DOMElement;
use Exception;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\RequestException;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Xibo\Event\ScheduleCriteriaRequestEvent;
use Xibo\Event\ScheduleCriteriaRequestInterface;
use Xibo\Event\WidgetDataRequestEvent;
use Xibo\Factory\DisplayFactory;
use Xibo\Support\Exception\ConfigurationException;
use Xibo\Support\Sanitizer\SanitizerInterface;
use Xibo\Widget\Provider\DataProviderInterface;
use Xibo\XMR\ScheduleCriteriaUpdateAction;
/**
* A connector to process National Weather Alert (NWS) - Atom feed data
*/
class NationalWeatherServiceConnector implements ConnectorInterface, EmergencyAlertInterface
{
use ConnectorTrait;
/** @var DOMDocument */
protected DOMDocument $atomFeedXML;
/** @var DOMElement */
protected DOMElement $feedNode;
/** @var DOMElement */
protected DOMElement $entryNode;
/** @var DisplayFactory */
private DisplayFactory $displayFactory;
/**
* @param ContainerInterface $container
* @return ConnectorInterface
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function setFactories(ContainerInterface $container): ConnectorInterface
{
$this->displayFactory = $container->get('displayFactory');
return $this;
}
public function registerWithDispatcher(EventDispatcherInterface $dispatcher): ConnectorInterface
{
$dispatcher->addListener(WidgetDataRequestEvent::$NAME, [$this, 'onDataRequest']);
$dispatcher->addListener(ScheduleCriteriaRequestEvent::$NAME, [$this, 'onScheduleCriteriaRequest']);
return $this;
}
public function getSourceName(): string
{
return 'national-weather-service-connector';
}
public function getTitle(): string
{
return 'National Weather Service Connector';
}
public function getDescription(): string
{
return 'National Weather Service (NWS)';
}
public function getThumbnail(): string
{
return 'theme/default/img/connectors/xibo-nws.png';
}
public function getSettingsFormTwig(): string
{
return 'national-weather-service-form-settings';
}
public function processSettingsForm(SanitizerInterface $params, array $settings): array
{
if (!$this->isProviderSetting('atomFeedUri')) {
$settings['atomFeedUri'] = $params->getString('atomFeedUri');
}
return $settings;
}
/**
* If the requested dataSource is national-weather-service, get the data, process it and add to dataProvider
*
* @param WidgetDataRequestEvent $event
* @return void
* @throws GuzzleException
*/
public function onDataRequest(WidgetDataRequestEvent $event): void
{
if ($event->getDataProvider()->getDataSource() === 'national-weather-service') {
if (empty($this->getSetting('atomFeedUri'))) {
$this->getLogger()->debug('onDataRequest: National Weather Service Connector not configured.');
return;
}
$event->stopPropagation();
try {
// Set cache expiry date to 3 minutes from now
$cacheExpire = Carbon::now()->addMinutes(3);
// Fetch the Atom Feed XML content
$xmlContent = $this->getFeedFromUrl($event->getDataProvider(), $cacheExpire);
// Initialize DOMDocument and load the XML content
$this->atomFeedXML = new DOMDocument();
$this->atomFeedXML->loadXML($xmlContent);
// Ensure the root element is <feed>
$feedNode = $this->atomFeedXML->getElementsByTagName('feed')->item(0);
if ($feedNode instanceof DOMElement) {
$this->feedNode = $feedNode;
} else {
throw new \Exception('The root <feed> element is missing.');
}
// Get all <entry> nodes within the <feed> element
$entryNodes = $this->feedNode->getElementsByTagName('entry');
// Are there any?
if ($entryNodes->length) {
// Process and initialize Atom Feed data
$this->processAtomFeedData($event->getDataProvider());
// Initialize update interval
$updateIntervalMinute = $event->getDataProvider()->getProperty('updateInterval');
// Convert the $updateIntervalMinute to seconds
$updateInterval = $updateIntervalMinute * 60;
// If we've got data, then set our cache period.
$event->getDataProvider()->setCacheTtl($updateInterval);
$event->getDataProvider()->setIsHandled();
// Define priority arrays for status (higher priority = lower index)
$statusPriority = ['Actual', 'Exercise', 'System', 'Test', 'Draft'];
$highestStatus = null;
// Iterate through each <entry> node to find the highest-priority status
foreach ($entryNodes as $entryNode) {
$this->entryNode = $entryNode;
// Get the status for the current entry
$entryStatus = $this->getEntryData('status');
// Check if the current status has a higher priority
if ($entryStatus !== null && (
$highestStatus === null ||
array_search($entryStatus, $statusPriority) < array_search($highestStatus, $statusPriority)
)) {
$highestStatus = $entryStatus;
}
}
$capStatus = $highestStatus;
$category = 'Met';
} else {
$capStatus = 'No Alerts';
$category = '';
$event->getDataProvider()->addError(__('No alerts are available for the selected area at the moment.'));//phpcs:ignore
}
// initialize status for schedule criteria push message
if ($capStatus == 'Actual') {
$status = self::ACTUAL_ALERT;
} elseif ($capStatus == 'No Alerts') {
$status = self::NO_ALERT;
} else {
$status = self::TEST_ALERT;
}
$this->getLogger()->debug('Schedule criteria push message: status = ' . $status
. ', category = ' . $category);
// Set schedule criteria update
$action = new ScheduleCriteriaUpdateAction();
$action->setCriteriaUpdates([
['metric' => 'emergency_alert_status', 'value' => $status, 'ttl' => 60],
['metric' => 'emergency_alert_category', 'value' => $category, 'ttl' => 60]
]);
// Initialize the display
$displayId = $event->getDataProvider()->getDisplayId();
$display = $this->displayFactory->getById($displayId);
// Criteria push message
$this->getPlayerActionService()->sendAction($display, $action);
} catch (Exception $exception) {
$this->getLogger()
->error('onDataRequest: Failed to get results. e = ' . $exception->getMessage());
}
}
}
/**
* Get and process the NWS Atom Feed data
*
* @throws Exception
*/
private function processAtomFeedData(DataProviderInterface $dataProvider): void
{
// Array to store configuration data
$config = [];
// Initialize configuration data
$config['status'] = $dataProvider->getProperty('status');
$config['msgType'] = $dataProvider->getProperty('msgType');
$config['urgency'] = $dataProvider->getProperty('urgency');
$config['severity'] = $dataProvider->getProperty('severity');
$config['certainty'] = $dataProvider->getProperty('certainty');
// Get all <entry> nodes within the <feed> element
$entryNodes = $this->feedNode->getElementsByTagName('entry');
// Iterate through each <entry> node
foreach ($entryNodes as $entryNode) {
$this->entryNode = $entryNode;
// Retrieve specific values from the CAP XML for filtering
$status = $this->getEntryData('status');
$msgType = $this->getEntryData('msgType');
$urgency = $this->getEntryData('urgency');
$severity = $this->getEntryData('severity');
$certainty = $this->getEntryData('certainty');
// Check if the retrieved CAP data matches the configuration filters
if (!$this->matchesFilter($status, $config['status']) ||
!$this->matchesFilter($msgType, $config['msgType']) ||
!$this->matchesFilter($urgency, $config['urgency']) ||
!$this->matchesFilter($severity, $config['severity']) ||
!$this->matchesFilter($certainty, $config['certainty'])
) {
continue;
}
// Array to store CAP values
$cap = [];
// Initialize CAP values
$cap['source'] = $this->getEntryData('source');
$cap['note'] = $this->getEntryData('note');
$cap['event'] = $this->getEntryData('event');
$cap['urgency'] = $this->getEntryData('urgency');
$cap['severity'] = $this->getEntryData('severity');
$cap['certainty'] = $this->getEntryData('certainty');
$cap['dateTimeEffective'] = $this->getEntryData('effective');
$cap['dateTimeOnset'] = $this->getEntryData('onset');
$cap['dateTimeExpires'] = $this->getEntryData('expires');
$cap['headline'] = $this->getEntryData('headline');
$cap['description'] = $this->getEntryData('summary');
$cap['instruction'] = $this->getEntryData('instruction');
$cap['contact'] = $this->getEntryData('contact');
$cap['areaDesc'] = $this->getEntryData('areaDesc');
// Add CAP data to data provider
$dataProvider->addItem($cap);
}
}
/**
* Fetches the National Weather Service's Atom Feed XML data from the Atom Feed URL provided by the connector.
*
* @param DataProviderInterface $dataProvider
* @param Carbon $cacheExpiresAt
*
* @return string|null
* @throws GuzzleException
*/
private function getFeedFromUrl(DataProviderInterface $dataProvider, Carbon $cacheExpiresAt): string|null
{
$atomFeedUri = $this->getSetting('atomFeedUri');
$area = $dataProvider->getProperty('area');
// Construct the Atom feed url
if (empty($area)) {
$url = $atomFeedUri;
} else {
$url = $atomFeedUri . '?area=' . $area;
}
$cache = $this->pool->getItem('/national-weather-service/alerts/' . md5($url));
$data = $cache->get();
if ($cache->isMiss()) {
$cache->lock();
$this->getLogger()->debug('Getting alerts from National Weather Service Atom feed');
$httpOptions = [
'timeout' => 20, // Wait no more than 20 seconds
];
try {
// Make a GET request to the Atom Feed URL using Guzzle HTTP client with defined options
$response = $dataProvider
->getGuzzleClient($httpOptions)
->get($url);
$this->getLogger()->debug('NWS Atom Feed uri: ' . $url . ' httpOptions: '
. json_encode($httpOptions));
// Get the response body as a string
$data = $response->getBody()->getContents();
// Cache
$cache->set($data);
$cache->expiresAt($cacheExpiresAt);
$this->pool->saveDeferred($cache);
} catch (RequestException $e) {
// Log the error with a message specific to NWS Alert data fetching
$this->getLogger()->error('Unable to reach the NWS Atom feed URL: '
. $url . ' Error: ' . $e->getMessage());
// Throw a more specific exception message
$dataProvider->addError(__('Failed to retrieve NWS alerts from specified Atom Feed URL.'));
}
} else {
$this->getLogger()->debug('Getting NWS Alert data from cache');
}
return $data;
}
/**
* Get the value of a specified tag from the current <entry> node.
*
* @param string $tagName
* @return string|null
*/
private function getEntryData(string $tagName): ?string
{
// Ensure the tag exists within the provided <entry> node
$node = $this->entryNode->getElementsByTagName($tagName)->item(0);
// Return the node value if the node exists, otherwise return an empty string
return $node ? $node->nodeValue : '';
}
/**
* Check if the value of XML element matches the expected filter value.
*
* @param string $actualValue
* @param string $expectedValue
*
* @return bool
*/
private function matchesFilter(string $actualValue, string $expectedValue): bool
{
// If the expected value is 'Any' (empty string) or matches the actual value, the filter passes
if (empty($expectedValue) || $expectedValue == $actualValue) {
return true;
}
return false;
}
/**
* @param ScheduleCriteriaRequestInterface $event
* @return void
* @throws ConfigurationException
*/
public function onScheduleCriteriaRequest(ScheduleCriteriaRequestInterface $event): void
{
// Initialize Emergency Alerts schedule criteria parameters but with limited category
$event->addType('emergency_alert', __('Emergency Alerts'))
->addMetric('emergency_alert_status', __('Status'))
->addCondition([
'eq' => __('Equal to')
])
->addValues('dropdown', [
self::ACTUAL_ALERT => __('Actual Alerts'),
self::TEST_ALERT => __('Test Alerts'),
self::NO_ALERT => __('No Alerts')
])
->addMetric('emergency_alert_category', __('Category'))
->addCondition([
'eq' => __('Equal to')
])
->addValues('dropdown', [
'Met' => __('Met')
]);
}
}

View File

@@ -0,0 +1,951 @@
<?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\Connector;
use Carbon\Carbon;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Str;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Xibo\Event\ScheduleCriteriaRequestEvent;
use Xibo\Event\ScheduleCriteriaRequestInterface;
use Xibo\Event\WidgetDataRequestEvent;
use Xibo\Event\XmdsWeatherRequestEvent;
use Xibo\Support\Exception\ConfigurationException;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Sanitizer\SanitizerInterface;
use Xibo\Widget\DataType\Forecast;
use Xibo\Widget\Provider\DataProviderInterface;
/**
* A connector to get data from the Open Weather Map API for use by the Weather Widget
*/
class OpenWeatherMapConnector implements ConnectorInterface
{
use ConnectorTrait;
private $apiUrl = 'https://api.openweathermap.org/data/';
private $forecastCurrent = '2.5/weather';
private $forecast3Hourly = '2.5/forecast';
private $forecastDaily = '2.5/forecast/daily';
private $forecastCombinedV3 = '3.0/onecall';
/** @var string */
protected $timezone;
/** @var \Xibo\Widget\DataType\Forecast */
protected $currentDay;
public function registerWithDispatcher(EventDispatcherInterface $dispatcher): ConnectorInterface
{
$dispatcher->addListener(WidgetDataRequestEvent::$NAME, [$this, 'onDataRequest']);
$dispatcher->addListener(ScheduleCriteriaRequestEvent::$NAME, [$this, 'onScheduleCriteriaRequest']);
$dispatcher->addListener(XmdsWeatherRequestEvent::$NAME, [$this, 'onXmdsWeatherRequest']);
return $this;
}
public function getSourceName(): string
{
return 'openweathermap';
}
public function getTitle(): string
{
return 'Open Weather Map';
}
public function getDescription(): string
{
return 'Get Weather data from Open Weather Map API';
}
public function getThumbnail(): string
{
return 'theme/default/img/connectors/owm.png';
}
public function getSettingsFormTwig(): string
{
return 'openweathermap-form-settings';
}
public function processSettingsForm(SanitizerInterface $params, array $settings): array
{
if (!$this->isProviderSetting('owmApiKey')) {
$settings['owmApiKey'] = $params->getString('owmApiKey');
$settings['owmIsPaidPlan'] = $params->getCheckbox('owmIsPaidPlan');
$settings['cachePeriod'] = $params->getInt('cachePeriod');
$settings['xmdsCachePeriod'] = $params->getInt('xmdsCachePeriod');
}
return $settings;
}
/**
* If the requested dataSource is forecastio, get the data, process it and add to dataProvider
*
* @param WidgetDataRequestEvent $event
* @return void
*/
public function onDataRequest(WidgetDataRequestEvent $event)
{
if ($event->getDataProvider()->getDataSource() === 'forecastio') {
if (empty($this->getSetting('owmApiKey'))) {
$this->getLogger()->debug('onDataRequest: Open Weather Map not configured.');
return;
}
$event->stopPropagation();
if ($this->isProviderSetting('apiUrl')) {
$this->apiUrl = $this->getSetting('apiUrl');
}
try {
$this->getWeatherData($event->getDataProvider());
// If we've got data, then set our cache period.
$event->getDataProvider()->setCacheTtl($this->getSetting('cachePeriod', 3600));
$event->getDataProvider()->setIsHandled();
} catch (\Exception $exception) {
$this->getLogger()->error('onDataRequest: Failed to get results. e = ' . $exception->getMessage());
$event->getDataProvider()->addError(__('Unable to get weather results.'));
}
}
}
/**
* Get a combined forecast
* @throws \Xibo\Support\Exception\GeneralException
*/
private function getWeatherData(DataProviderInterface $dataProvider)
{
// Convert units to an acceptable format
$units = in_array($dataProvider->getProperty('units', 'auto'), ['auto', 'us', 'uk2']) ? 'imperial' : 'metric';
// Temperature and Wind Speed Unit Mappings
$unit = $this->getUnit($dataProvider->getProperty('units'));
if ($dataProvider->getProperty('useDisplayLocation') == 0) {
$providedLat = $dataProvider->getProperty('latitude', $dataProvider->getDisplayLatitude());
$providedLon = $dataProvider->getProperty('longitude', $dataProvider->getDisplayLongitude());
} else {
$providedLat = $dataProvider->getDisplayLatitude();
$providedLon = $dataProvider->getDisplayLongitude();
}
// Build the URL
$url = '?lat=' . $providedLat
. '&lon=' . $providedLon
. '&units=' . $units
. '&lang=' . $dataProvider->getProperty('lang', 'en')
. '&appid=[API_KEY]';
// Cache expiry date
$cacheExpire = Carbon::now()->addSeconds($this->getSetting('cachePeriod'));
if ($this->getSetting('owmIsPaidPlan') ?? 0 == 1) {
// We build our data from multiple API calls
// Current data first.
$data = $this->queryApi($this->apiUrl . $this->forecastCurrent . $url, $cacheExpire);
$data['current'] = $this->parseCurrentIntoFormat($data);
// initialize timezone
$timezoneOffset = (int)$data['timezone'];
// Calculate the number of whole hours in the offset
$offsetHours = floor($timezoneOffset / 3600);
// Calculate the remaining minutes after extracting the whole hours
$offsetMinutes = ($timezoneOffset % 3600) / 60;
// Determine the sign of the offset (positive or negative)
$sign = $offsetHours < 0 ? '-' : '+';
// Ensure the format is as follows: +/-hh:mm
$formattedOffset = sprintf("%s%02d:%02d", $sign, abs($offsetHours), abs($offsetMinutes));
// Get the timezone name
$this->timezone = (new \DateTimeZone($formattedOffset))->getName();
// Pick out the country
$country = $data['sys']['country'] ?? null;
$this->getLogger()->debug('Trying to determine units for Country: ' . $country);
// If we don't have a unit, then can we base it on the timezone we got back?
if ($dataProvider->getProperty('units', 'auto') === 'auto' && $country !== null) {
// Pick out some countries to set the units
if ($country === 'GB') {
$unit = $this->getUnit('uk2');
} else if ($country === 'US') {
$unit = $this->getUnit('us');
} else if ($country === 'CA') {
$unit = $this->getUnit('ca');
} else {
$unit = $this->getUnit('si');
}
}
// Then the 16 day forecast API, which we will cache a day
$data['daily'] = $this->queryApi(
$this->apiUrl . $this->forecastDaily . $url,
$cacheExpire->copy()->addDay()->startOfDay()
)['list'];
} else {
// We use one call API 3.0
$data = $this->queryApi($this->apiUrl . $this->forecastCombinedV3 . $url, $cacheExpire);
$this->timezone = $data['timezone'];
// Country based on timezone (this is harder than using the real country)
if ($dataProvider->getProperty('units', 'auto') === 'auto') {
if (Str::startsWith($this->timezone, 'America')) {
$unit = $this->getUnit('us');
} else if ($this->timezone === 'Europe/London') {
$unit = $this->getUnit('uk2');
} else {
$unit = $this->getUnit('si');
}
}
}
// Using units:
$this->getLogger()->debug('Using units: ' . json_encode($unit));
$forecasts = [];
// Parse into our forecast.
// Load this data into our objects
$this->currentDay = new Forecast();
$this->currentDay->temperatureUnit = $unit['tempUnit'] ?: 'C';
$this->currentDay->windSpeedUnit = $unit['windUnit'] ?: 'KPH';
$this->currentDay->visibilityDistanceUnit = $unit['visibilityUnit'] ?: 'km';
$this->currentDay->location = $data['name'] ?? '';
$this->processItemIntoDay($this->currentDay, $data['current'], $units, true);
$countForecast = 0;
// Process each day into a forecast
foreach ($data['daily'] as $dayItem) {
// Skip first item as this is the currentDay
if ($countForecast++ === 0) {
continue;
}
$day = new Forecast();
$day->temperatureUnit = $this->currentDay->temperatureUnit;
$day->windSpeedUnit = $this->currentDay->windSpeedUnit;
$day->visibilityDistanceUnit = $this->currentDay->visibilityDistanceUnit;
$day->location = $this->currentDay->location;
$this->processItemIntoDay($day, $dayItem, $units);
$forecasts[] = $day;
}
// Enhance the currently with the high/low from the first daily forecast
$this->currentDay->temperatureHigh = $forecasts[0]->temperatureHigh;
$this->currentDay->temperatureMaxRound = $forecasts[0]->temperatureMaxRound;
$this->currentDay->temperatureLow = $forecasts[0]->temperatureLow;
$this->currentDay->temperatureMinRound = $forecasts[0]->temperatureMinRound;
$this->currentDay->temperatureMorning = $forecasts[0]->temperatureMorning;
$this->currentDay->temperatureMorningRound = $forecasts[0]->temperatureMorningRound;
$this->currentDay->temperatureNight = $forecasts[0]->temperatureNight;
$this->currentDay->temperatureNightRound = $forecasts[0]->temperatureNightRound;
$this->currentDay->temperatureEvening = $forecasts[0]->temperatureEvening;
$this->currentDay->temperatureEveningRound = $forecasts[0]->temperatureEveningRound;
$this->currentDay->temperatureMean = $forecasts[0]->temperatureMean;
$this->currentDay->temperatureMeanRound = $forecasts[0]->temperatureMeanRound;
if ($dataProvider->getProperty('dayConditionsOnly', 0) == 1) {
// Swap the night icons for their day equivalents
$this->currentDay->icon = str_replace('-night', '', $this->currentDay->icon);
$this->currentDay->wicon = str_replace('-night', '', $this->currentDay->wicon);
}
$dataProvider->addItem($this->currentDay);
if (count($forecasts) > 0) {
foreach ($forecasts as $forecast) {
$dataProvider->addItem($forecast);
}
}
$dataProvider->addOrUpdateMeta('Attribution', 'Powered by OpenWeather');
}
/**
* @param string $url
* @param Carbon $cacheExpiresAt
* @return array
* @throws \Xibo\Support\Exception\GeneralException
*/
private function queryApi(string $url, Carbon $cacheExpiresAt): array
{
$cache = $this->pool->getItem('/weather/owm/' . md5($url));
$data = $cache->get();
if ($cache->isMiss()) {
$cache->lock();
$this->getLogger()->debug('Getting Forecast from API');
$url = str_replace('[API_KEY]', $this->getSetting('owmApiKey'), $url);
try {
$response = $this->getClient()->get($url);
// Success?
if ($response->getStatusCode() != 200) {
throw new GeneralException('Non-200 response from Open Weather Map');
}
// Parse out header and body
$data = json_decode($response->getBody(), true);
// Cache
$cache->set($data);
$cache->expiresAt($cacheExpiresAt);
$this->pool->saveDeferred($cache);
} catch (RequestException $e) {
$this->getLogger()->error('Unable to reach Open Weather Map API: '
. str_replace($this->getSetting('owmApiKey'), '[API_KEY]', $e->getMessage()));
throw new GeneralException('API responded with an error.');
}
} else {
$this->getLogger()->debug('Getting Forecast from cache');
}
return $data;
}
/**
* Parse the response from the current API into the format provided by the onecall API
* this means easier processing down the line
* @param array $source
* @return array
*/
private function parseCurrentIntoFormat(array $source): array
{
return [
'timezone' => $source['timezone'],
'dt' => $source['dt'],
'sunrise' => $source['sys']['sunrise'],
'sunset' => $source['sys']['sunset'],
'temp' => $source['main']['temp'],
'feels_like' => $source['main']['feels_like'],
'pressure' => $source['main']['pressure'],
'humidity' => $source['main']['humidity'],
'dew_point' => null,
'uvi' => null,
'clouds' => $source['clouds']['all'],
'visibility' => $source['visibility'] ?? 0,
'wind_speed' => $source['wind']['speed'],
'wind_deg' => $source['wind']['deg'] ?? 0,
'weather' => $source['weather'],
];
}
/**
* @param \Xibo\Weather\Forecast $day
* @param array $item
* @param $requestUnit
* @param bool $isCurrent
*/
private function processItemIntoDay($day, $item, $requestUnit, $isCurrent = false)
{
$day->time = $item['dt'];
$day->sunRise = $item['sunrise'];
$day->sunSet = $item['sunset'];
$day->summary = ucfirst($item['weather'][0]['description']);
// Temperature
// imperial = F
// metric = C
if ($isCurrent) {
$day->temperature = $item['temp'];
$day->apparentTemperature = $item['feels_like'];
$day->temperatureHigh = $day->temperature;
$day->temperatureLow = $day->temperature;
$day->temperatureNight = $day->temperature;
$day->temperatureEvening = $day->temperature;
$day->temperatureMorning = $day->temperature;
} else {
$day->temperature = $item['temp']['day'];
$day->apparentTemperature = $item['feels_like']['day'];
$day->temperatureHigh = $item['temp']['max'] ?? $day->temperature;
$day->temperatureLow = $item['temp']['min'] ?? $day->temperature;
$day->temperatureNight = $item['temp']['night'];
$day->temperatureEvening = $item['temp']['eve'];
$day->temperatureMorning = $item['temp']['morn'];
}
if ($requestUnit === 'metric' && $day->temperatureUnit === 'F') {
// Convert C to F
$day->temperature = ($day->temperature) * 9 / 5 + 32;
$day->apparentTemperature = ($day->apparentTemperature) * 9 / 5 + 32;
$day->temperatureHigh = ($day->temperatureHigh) * 9 / 5 + 32;
$day->temperatureLow = ($day->temperatureLow) * 9 / 5 + 32;
$day->temperatureNight = ($day->temperatureNight) * 9 / 5 + 32;
$day->temperatureEvening = ($day->temperatureEvening) * 9 / 5 + 32;
$day->temperatureMorning = ($day->temperatureMorning) * 9 / 5 + 32;
} else if ($requestUnit === 'imperial' && $day->temperatureUnit === 'C') {
// Convert F to C
$day->temperature = ($day->temperature - 32) * 5 / 9;
$day->apparentTemperature = ($day->apparentTemperature - 32) * 5 / 9;
$day->temperatureHigh = ($day->temperatureHigh - 32) * 5 / 9;
$day->temperatureLow = ($day->temperatureLow - 32) * 5 / 9;
$day->temperatureNight = ($day->temperatureNight - 32) * 5 / 9;
$day->temperatureEvening = ($day->temperatureEvening - 32) * 5 / 9;
$day->temperatureMorning = ($day->temperatureMorning - 32) * 5 / 9;
}
// Work out the mean
$day->temperatureMean = ($day->temperatureHigh + $day->temperatureLow) / 2;
// Round those off
$day->temperatureRound = round($day->temperature, 0);
$day->temperatureNightRound = round($day->temperatureNight, 0);
$day->temperatureMorningRound = round($day->temperatureMorning, 0);
$day->temperatureEveningRound = round($day->temperatureEvening, 0);
$day->apparentTemperatureRound = round($day->apparentTemperature, 0);
$day->temperatureMaxRound = round($day->temperatureHigh, 0);
$day->temperatureMinRound = round($day->temperatureLow, 0);
$day->temperatureMeanRound = round($day->temperatureMean, 0);
// Humidity
$day->humidityPercent = $item['humidity'];
$day->humidity = $day->humidityPercent / 100;
// Pressure
// received in hPa, display in mB
$day->pressure = $item['pressure'] / 100;
// Wind
// metric = meters per second
// imperial = miles per hour
$day->windSpeed = $item['wind_speed'] ?? $item['speed'] ?? null;
$day->windBearing = $item['wind_deg'] ?? $item['deg'] ?? null;
if ($requestUnit === 'metric' && $day->windSpeedUnit !== 'MPS') {
// We have MPS and need to go to something else
if ($day->windSpeedUnit === 'MPH') {
// Convert MPS to MPH
$day->windSpeed = round($day->windSpeed * 2.237, 2);
} else if ($day->windSpeedUnit === 'KPH') {
// Convert MPS to KPH
$day->windSpeed = round($day->windSpeed * 3.6, 2);
}
} else if ($requestUnit === 'imperial' && $day->windSpeedUnit !== 'MPH') {
if ($day->windSpeedUnit === 'MPS') {
// Convert MPH to MPS
$day->windSpeed = round($day->windSpeed / 2.237, 2);
} else if ($day->windSpeedUnit === 'KPH') {
// Convert MPH to KPH
$day->windSpeed = round($day->windSpeed * 1.609344, 2);
}
}
// Wind direction
$day->windDirection = '--';
if ($day->windBearing !== null && $day->windBearing !== 0) {
foreach (self::cardinalDirections() as $dir => $angles) {
if ($day->windBearing >= $angles[0] && $day->windBearing < $angles[1]) {
$day->windDirection = $dir;
break;
}
}
}
// Clouds
$day->cloudCover = $item['clouds'];
// Visibility
// metric = meters
// imperial = meters?
$day->visibility = $item['visibility'] ?? '--';
if ($day->visibility !== '--') {
// Always in meters
if ($day->visibilityDistanceUnit === 'mi') {
// Convert meters to miles
$day->visibility = $day->visibility / 1609;
} else {
if ($day->visibilityDistanceUnit === 'km') {
// Convert meters to KM
$day->visibility = $day->visibility / 1000;
}
}
}
// not available
$day->dewPoint = $item['dew_point'] ?? '--';
$day->uvIndex = $item['uvi'] ?? '--';
$day->ozone = '--';
// Map icon
$icons = self::iconMap();
$icon = $item['weather'][0]['icon'];
$day->icon = $icons['backgrounds'][$icon] ?? 'wi-na';
$day->wicon = $icons['weather-icons'][$icon] ?? 'wi-na';
}
/**
* @inheritDoc
*/
public static function supportedLanguages()
{
return [
['id' => 'af', 'value' => __('Afrikaans')],
['id' => 'ar', 'value' => __('Arabic')],
['id' => 'az', 'value' => __('Azerbaijani')],
['id' => 'bg', 'value' => __('Bulgarian')],
['id' => 'ca', 'value' => __('Catalan')],
['id' => 'zh_cn', 'value' => __('Chinese Simplified')],
['id' => 'zh_tw', 'value' => __('Chinese Traditional')],
['id' => 'cz', 'value' => __('Czech')],
['id' => 'da', 'value' => __('Danish')],
['id' => 'de', 'value' => __('German')],
['id' => 'el', 'value' => __('Greek')],
['id' => 'en', 'value' => __('English')],
['id' => 'eu', 'value' => __('Basque')],
['id' => 'fa', 'value' => __('Persian (Farsi)')],
['id' => 'fi', 'value' => __('Finnish')],
['id' => 'fr', 'value' => __('French')],
['id' => 'gl', 'value' => __('Galician')],
['id' => 'he', 'value' => __('Hebrew')],
['id' => 'hi', 'value' => __('Hindi')],
['id' => 'hr', 'value' => __('Croatian')],
['id' => 'hu', 'value' => __('Hungarian')],
['id' => 'id', 'value' => __('Indonesian')],
['id' => 'it', 'value' => __('Italian')],
['id' => 'ja', 'value' => __('Japanese')],
['id' => 'kr', 'value' => __('Korean')],
['id' => 'la', 'value' => __('Latvian')],
['id' => 'lt', 'value' => __('Lithuanian')],
['id' => 'mk', 'value' => __('Macedonian')],
['id' => 'no', 'value' => __('Norwegian')],
['id' => 'nl', 'value' => __('Dutch')],
['id' => 'pl', 'value' => __('Polish')],
['id' => 'pt', 'value' => __('Portuguese')],
['id' => 'pt_br', 'value' => __('Português Brasil')],
['id' => 'ro', 'value' => __('Romanian')],
['id' => 'ru', 'value' => __('Russian')],
['id' => 'se', 'value' => __('Swedish')],
['id' => 'sk', 'value' => __('Slovak')],
['id' => 'sl', 'value' => __('Slovenian')],
['id' => 'es', 'value' => __('Spanish')],
['id' => 'sr', 'value' => __('Serbian')],
['id' => 'th', 'value' => __('Thai')],
['id' => 'tr', 'value' => __('Turkish')],
['id' => 'uk', 'value' => __('Ukrainian')],
['id' => 'vi', 'value' => __('Vietnamese')],
['id' => 'zu', 'value' => __('Zulu')]
];
}
/**
* @return array
*/
private function iconMap()
{
return [
'weather-icons' => [
'01d' => 'wi-day-sunny',
'01n' => 'wi-night-clear',
'02d' => 'wi-day-cloudy',
'02n' => 'wi-night-partly-cloudy',
'03d' => 'wi-cloudy',
'03n' => 'wi-night-cloudy',
'04d' => 'wi-day-cloudy',
'04n' => 'wi-night-partly-cloudy',
'09d' => 'wi-rain',
'09n' => 'wi-night-rain',
'10d' => 'wi-rain',
'10n' => 'wi-night-rain',
'11d' => 'wi-day-thunderstorm',
'11n' => 'wi-night-thunderstorm',
'13d' => 'wi-day-snow',
'13n' => 'wi-night-snow',
'50d' => 'wi-day-fog',
'50n' => 'wi-night-fog'
],
'backgrounds' => [
'01d' => 'clear-day',
'01n' => 'clear-night',
'02d' => 'partly-cloudy-day',
'02n' => 'partly-cloudy-night',
'03d' => 'cloudy',
'03n' => 'cloudy',
'04d' => 'partly-cloudy-day',
'04n' => 'partly-cloudy-night',
'09d' => 'rain',
'09n' => 'rain',
'10d' => 'rain',
'10n' => 'rain',
'11d' => 'wind',
'11n' => 'wind',
'13d' => 'snow',
'13n' => 'snow',
'50d' => 'fog',
'50n' => 'fog'
]
];
}
/** @inheritDoc */
public static function unitsAvailable()
{
return [
['id' => 'auto', 'value' => 'Automatically select based on geographic location', 'tempUnit' => '', 'windUnit' => '', 'visibilityUnit' => ''],
['id' => 'ca', 'value' => 'Canada', 'tempUnit' => 'C', 'windUnit' => 'KPH', 'visibilityUnit' => 'km'],
['id' => 'si', 'value' => 'Standard International Units', 'tempUnit' => 'C', 'windUnit' => 'MPS', 'visibilityUnit' => 'km'],
['id' => 'uk2', 'value' => 'United Kingdom', 'tempUnit' => 'C', 'windUnit' => 'MPH', 'visibilityUnit' => 'mi'],
['id' => 'us', 'value' => 'United States', 'tempUnit' => 'F', 'windUnit' => 'MPH', 'visibilityUnit' => 'mi'],
];
}
/**
* @param $code
* @return mixed|null
*/
public function getUnit($code)
{
foreach (self::unitsAvailable() as $unit) {
if ($unit['id'] == $code) {
return $unit;
}
}
return null;
}
/**
* @return array
*/
private static function cardinalDirections()
{
return [
'N' => [337.5, 22.5],
'NE' => [22.5, 67.5],
'E' => [67.5, 112.5],
'SE' => [112.5, 157.5],
'S' => [157.5, 202.5],
'SW' => [202.5, 247.5],
'W' => [247.5, 292.5],
'NW' => [292.5, 337.5]
];
}
/**
* @param ScheduleCriteriaRequestInterface $event
* @return void
* @throws ConfigurationException
*/
public function onScheduleCriteriaRequest(ScheduleCriteriaRequestInterface $event): void
{
// Initialize Open Weather Schedule Criteria parameters
$event->addType('weather', __('Weather'))
->addMetric('weather_condition', __('Weather Condition'))
->addCondition([
'eq' => __('Equal to')
])
->addValues('dropdown', [
'thunderstorm' => __('Thunderstorm'),
'drizzle' => __('Drizzle'),
'rain' => __('Rain'),
'snow' => __('Snow'),
'clear' => __('Clear'),
'clouds' => __('Clouds')
])
->addMetric('weather_temp_imperial', __('Temperature (Imperial)'))
->addCondition([
'lt' => __('Less than'),
'lte' => __('Less than or equal to'),
'eq' => __('Equal to'),
'gte' => __('Greater than or equal to'),
'gt' => __('Greater than')
])
->addValues('number', [])
->addMetric('weather_temp_metric', __('Temperature (Metric)'))
->addCondition([
'lt' => __('Less than'),
'lte' => __('Less than or equal to'),
'eq' => __('Equal to'),
'gte' => __('Greater than or equal to'),
'gt' => __('Greater than')
])
->addValues('number', [])
->addMetric('weather_feels_like_imperial', __('Apparent Temperature (Imperial)'))
->addCondition([
'lt' => __('Less than'),
'lte' => __('Less than or equal to'),
'eq' => __('Equal to'),
'gte' => __('Greater than or equal to'),
'gt' => __('Greater than')
])
->addValues('number', [])
->addMetric('weather_feels_like_metric', __('Apparent Temperature (Metric)'))
->addCondition([
'lt' => __('Less than'),
'lte' => __('Less than or equal to'),
'eq' => __('Equal to'),
'gte' => __('Greater than or equal to'),
'gt' => __('Greater than')
])
->addValues('number', [])
->addMetric('weather_wind_speed', __('Wind Speed'))
->addCondition([
'lt' => __('Less than'),
'lte' => __('Less than or equal to'),
'eq' => __('Equal to'),
'gte' => __('Greater than or equal to'),
'gt' => __('Greater than')
])
->addValues('number', [])
->addMetric('weather_wind_direction', __('Wind Direction'))
->addCondition([
'eq' => __('Equal to')
])
->addValues('dropdown', [
'N' => __('North'),
'NE' => __('Northeast'),
'E' => __('East'),
'SE' => __('Southeast'),
'S' => __('South'),
'SW' => __('Southwest'),
'W' => __('West'),
'NW' => __('Northwest'),
])
->addMetric('weather_wind_degrees', __('Wind Direction (degrees)'))
->addCondition([
'lt' => __('Less than'),
'lte' => __('Less than or equal to'),
'eq' => __('Equal to'),
'gte' => __('Greater than or equal to'),
'gt' => __('Greater than')
])
->addValues('number', [])
->addMetric('weather_humidity', __('Humidity (Percent)'))
->addCondition([
'lt' => __('Less than'),
'lte' => __('Less than or equal to'),
'eq' => __('Equal to'),
'gte' => __('Greater than or equal to'),
'gt' => __('Greater than')
])
->addValues('number', [])
->addMetric('weather_pressure', __('Pressure'))
->addCondition([
'lt' => __('Less than'),
'lte' => __('Less than or equal to'),
'eq' => __('Equal to'),
'gte' => __('Greater than or equal to'),
'gt' => __('Greater than')
])
->addValues('number', [])
->addMetric('weather_visibility', __('Visibility (meters)'))
->addCondition([
'lt' => __('Less than'),
'lte' => __('Less than or equal to'),
'eq' => __('Equal to'),
'gte' => __('Greater than or equal to'),
'gt' => __('Greater than')
])
->addValues('number', []);
}
/**
* @param $item
* @param $unit
* @param $requestUnit
* @return array
*/
private function processXmdsWeatherData($item, $unit, $requestUnit): array
{
$windSpeedUnit = $unit['windUnit'] ?? 'KPH';
$visibilityDistanceUnit = $unit['visibilityUnit'] ?? 'km';
// var to store output/response
$data = array();
// format the weather condition
$data['weather_condition'] = str_replace(' ', '_', strtolower($item['weather'][0]['main']));
// Temperature
// imperial = F
// metric = C
$tempImperial = $item['temp'];
$apparentTempImperial = $item['feels_like'];
// Convert F to C
$tempMetric = ($tempImperial - 32) * 5 / 9;
$apparentTempMetric = ($apparentTempImperial - 32) * 5 / 9;
// Round those temperature values
$data['weather_temp_imperial'] = round($tempImperial, 0);
$data['weather_feels_like_imperial'] = round($apparentTempImperial, 0);
$data['weather_temp_metric'] = round($tempMetric, 0);
$data['weather_feels_like_metric'] = round($apparentTempMetric, 0);
// Humidity
$data['weather_humidity'] = $item['humidity'];
// Pressure
// received in hPa, display in mB
$data['weather_pressure'] = $item['pressure'] / 100;
// Wind
// metric = meters per second
// imperial = miles per hour
$data['weather_wind_speed'] = $item['wind_speed'] ?? $item['speed'] ?? null;
$data['weather_wind_degrees'] = $item['wind_deg'] ?? $item['deg'] ?? null;
if ($requestUnit === 'metric' && $windSpeedUnit !== 'MPS') {
// We have MPS and need to go to something else
if ($windSpeedUnit === 'MPH') {
// Convert MPS to MPH
$data['weather_wind_degrees'] = round($data['weather_wind_degrees'] * 2.237, 2);
} else if ($windSpeedUnit === 'KPH') {
// Convert MPS to KPH
$data['weather_wind_degrees'] = round($data['weather_wind_degrees'] * 3.6, 2);
}
} else if ($requestUnit === 'imperial' && $windSpeedUnit !== 'MPH') {
if ($windSpeedUnit === 'MPS') {
// Convert MPH to MPS
$data['weather_wind_degrees'] = round($data['weather_wind_degrees'] / 2.237, 2);
} else if ($windSpeedUnit === 'KPH') {
// Convert MPH to KPH
$data['weather_wind_degrees'] = round($data['weather_wind_degrees'] * 1.609344, 2);
}
}
// Wind direction
$data['weather_wind_direction'] = '--';
if ($data['weather_wind_degrees'] !== null && $data['weather_wind_degrees'] !== 0) {
foreach (self::cardinalDirections() as $dir => $angles) {
if ($data['weather_wind_degrees'] >= $angles[0] && $data['weather_wind_degrees'] < $angles[1]) {
$data['weather_wind_direction'] = $dir;
break;
}
}
}
// Visibility
// metric = meters
// imperial = meters?
$data['weather_visibility'] = $item['visibility'] ?? '--';
if ($data['weather_visibility'] !== '--') {
// Always in meters
if ($visibilityDistanceUnit === 'mi') {
// Convert meters to miles
$data['weather_visibility'] = $data['weather_visibility'] / 1609;
} else {
if ($visibilityDistanceUnit === 'km') {
// Convert meters to KM
$data['weather_visibility'] = $data['weather_visibility'] / 1000;
}
}
}
return $data;
}
/**
* @param XmdsWeatherRequestEvent $event
* @return void
* @throws GeneralException|\SoapFault
*/
public function onXmdsWeatherRequest(XmdsWeatherRequestEvent $event): void
{
// check for API Key
if (empty($this->getSetting('owmApiKey'))) {
$this->getLogger()->debug('onXmdsWeatherRequest: Open Weather Map not configured.');
throw new \SoapFault(
'Receiver',
'Open Weather Map API key is not configured'
);
}
$latitude = $event->getLatitude();
$longitude = $event->getLongitude();
// Cache expiry date
$cacheExpire = Carbon::now()->addHours($this->getSetting('xmdsCachePeriod'));
// use imperial as the default units, so we can get the right value when converting to metric
$units = 'imperial';
// Temperature and Wind Speed Unit Mappings
$unit = $this->getUnit('auto');
// Build the URL
$url = '?lat=' . $latitude
. '&lon=' . $longitude
. '&units=' . $units
. '&appid=[API_KEY]';
// check API plan
if ($this->getSetting('owmIsPaidPlan') ?? 0 == 1) {
// use weather data endpoints for Paid Plan
$data = $this->queryApi($this->apiUrl . $this->forecastCurrent . $url, $cacheExpire);
$data['current'] = $this->parseCurrentIntoFormat($data);
// Pick out the country
$country = $data['sys']['country'] ?? null;
// If we don't have a unit, then can we base it on the timezone we got back?
if ($country !== null) {
// Pick out some countries to set the units
if ($country === 'GB') {
$unit = $this->getUnit('uk2');
} else if ($country === 'US') {
$unit = $this->getUnit('us');
} else if ($country === 'CA') {
$unit = $this->getUnit('ca');
} else {
$unit = $this->getUnit('si');
}
}
} else {
// We use one call API 3.0 for Free Plan
$data = $this->queryApi($this->apiUrl . $this->forecastCombinedV3 . $url, $cacheExpire);
// Country based on timezone (this is harder than using the real country)
if (Str::startsWith($data['timezone'], 'America')) {
$unit = $this->getUnit('us');
} else if ($data['timezone'] === 'Europe/London') {
$unit = $this->getUnit('uk2');
} else {
$unit = $this->getUnit('si');
}
}
// process weather data
$weatherData = $this->processXmdsWeatherData($data['current'], $unit, 'imperial');
// Set the processed weather data in the event as a JSON-encoded string
$event->setWeatherData(json_encode($weatherData));
}
}

View File

@@ -0,0 +1,324 @@
<?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\Connector;
use Carbon\Carbon;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Xibo\Entity\SearchResult;
use Xibo\Event\LibraryProviderEvent;
use Xibo\Event\LibraryProviderImportEvent;
use Xibo\Event\LibraryProviderListEvent;
use Xibo\Support\Sanitizer\SanitizerInterface;
/**
* Pixabay Connector
* This connector acts as a data provider for the Media Toolbar in the Layout/Playlist editor user interface
*/
class PixabayConnector implements ConnectorInterface
{
use ConnectorTrait;
public function registerWithDispatcher(EventDispatcherInterface $dispatcher): ConnectorInterface
{
$dispatcher->addListener('connector.provider.library', [$this, 'onLibraryProvider']);
$dispatcher->addListener('connector.provider.library.import', [$this, 'onLibraryImport']);
$dispatcher->addListener('connector.provider.library.list', [$this, 'onLibraryList']);
return $this;
}
public function getSourceName(): string
{
return 'pixabay';
}
public function getTitle(): string
{
return 'Pixabay';
}
public function getDescription(): string
{
return 'Show Pixabay images and videos in the Layout editor toolbar and download them to the library for use on your Layouts.';
}
public function getThumbnail(): string
{
return 'theme/default/img/connectors/pixabay_square_green.png';
}
public function getFilters(): array
{
return [
[
'name' => 'name',
'type' => 'string',
'key' => 'media'
],
[
'label' => 'type',
'type' => 'dropdown',
'options' => [
[
'name' => 'Image',
'value' => 'image'
],
[
'name' => 'Video',
'value' => 'video'
]
]
],
[
'label' => 'orientation',
'type' => 'dropdown',
'options' => [
[
'name' => 'All',
'value' => ''
],
[
'name' => 'Landscape',
'value' => 'landscape'
],
[
'name' => 'Portrait',
'value' => 'portrait'
]
],
'visibility' => [
'field' => 'type',
'type' => 'eq',
'value' => 'image'
]
]
];
}
public function getSettingsFormTwig(): string
{
return 'pixabay-form-settings';
}
public function processSettingsForm(SanitizerInterface $params, array $settings): array
{
if (!$this->isProviderSetting('apiKey')) {
$settings['apiKey'] = $params->getString('apiKey');
}
return $settings;
}
/**
* @param \Xibo\Event\LibraryProviderEvent $event
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function onLibraryProvider(LibraryProviderEvent $event)
{
$this->getLogger()->debug('onLibraryProvider');
// Do we have an alternative URL (we may proxy requests for cache)
$baseUrl = $this->getSetting('baseUrl');
if (empty($baseUrl)) {
$baseUrl = 'https://pixabay.com/api/';
}
// Do we have an API key?
$apiKey = $this->getSetting('apiKey');
if (empty($apiKey)) {
$this->getLogger()->debug('onLibraryProvider: No api key');
return;
}
// was Pixabay requested?
if ($event->getProviderName() === $this->getSourceName()) {
// We do! Let's get some results from Pixabay
// first we look at paging
$start = $event->getStart();
$perPage = $event->getLength();
if ($start == 0) {
$page = 1;
} else {
$page = floor($start / $perPage) + 1;
}
$query = [
'key' => $apiKey,
'page' => $page,
'per_page' => $perPage,
'safesearch' => 'true'
];
// Now we handle any other search
if ($event->getOrientation() === 'landscape') {
$query['orientation'] = 'horizontal';
} else if ($event->getOrientation() === 'portrait') {
$query['orientation'] = 'vertical';
}
if (!empty($event->getSearch())) {
$query['q'] = urlencode($event->getSearch());
}
// Pixabay either returns images or videos, not both.
if (count($event->getTypes()) !== 1) {
return;
}
$type = $event->getTypes()[0];
if (!in_array($type, ['image', 'video'])) {
return;
}
// Pixabay require a 24-hour cache of each result set.
$key = md5($type . '_' . json_encode($query));
$cache = $this->getPool()->getItem($key);
$body = $cache->get();
if ($cache->isMiss()) {
$this->getLogger()->debug('onLibraryProvider: cache miss, generating.');
// Make the request
$request = $this->getClient()->request('GET', $baseUrl . ($type === 'video' ? 'videos' : ''), [
'query' => $query
]);
$body = $request->getBody()->getContents();
if (empty($body)) {
$this->getLogger()->debug('onLibraryProvider: Empty body');
return;
}
$body = json_decode($body);
if ($body === null || $body === false) {
$this->getLogger()->debug('onLibraryProvider: non-json body or empty body returned.');
return;
}
// Cache for next time
$cache->set($body);
$cache->expiresAt(Carbon::now()->addHours(24));
$this->getPool()->saveDeferred($cache);
} else {
$this->getLogger()->debug('onLibraryProvider: serving from cache.');
}
$providerDetails = new ProviderDetails();
$providerDetails->id = 'pixabay';
$providerDetails->link = 'https://pixabay.com';
$providerDetails->logoUrl = '/theme/default/img/connectors/pixabay_logo.svg';
$providerDetails->iconUrl = '/theme/default/img/connectors/pixabay_logo_square.svg';
$providerDetails->backgroundColor = '';
$providerDetails->filters = $this->getFilters();
// Process each hit into a search result and add it to the overall results we've been given.
foreach ($body->hits as $result) {
$searchResult = new SearchResult();
$searchResult->source = $this->getSourceName();
$searchResult->id = $result->id;
$searchResult->title = $result->tags;
$searchResult->provider = $providerDetails;
if ($type === 'video') {
$searchResult->type = 'video';
$searchResult->thumbnail = $result->videos->tiny->url;
$searchResult->duration = $result->duration;
// As per Pixabay, medium videos are usually 1080p but in some cases,
// it might be larger (ie 2560x1440) so we need to do an additional validation
if (!empty($result->videos->medium) && $result->videos->medium->width <= 1920
&& $result->videos->medium->height <= 1920
) {
$searchResult->download = $result->videos->medium->url;
$searchResult->width = $result->videos->medium->width;
$searchResult->height = $result->videos->medium->height;
$searchResult->fileSize = $result->videos->medium->size;
} else if (!empty($result->videos->small)) {
$searchResult->download = $result->videos->small->url;
$searchResult->width = $result->videos->small->width;
$searchResult->height = $result->videos->small->height;
$searchResult->fileSize = $result->videos->small->size;
} else {
$searchResult->download = $result->videos->tiny->url;
$searchResult->width = $result->videos->tiny->width;
$searchResult->height = $result->videos->tiny->height;
$searchResult->fileSize = $result->videos->tiny->size;
}
if (!empty($result->picture_id ?? null)) {
// Try the old way (at some point this stopped working and went to the thumbnail approach above
$searchResult->videoThumbnailUrl = str_replace(
'pictureId',
$result->picture_id,
'https://i.vimeocdn.com/video/pictureId_960x540.png'
);
} else {
// Use the medium thumbnail if we have it, otherwise the tiny one.
$searchResult->videoThumbnailUrl = $result->videos->medium->thumbnail
?? $result->videos->tiny->thumbnail;
}
} else {
$searchResult->type = 'image';
$searchResult->thumbnail = $result->previewURL;
$searchResult->download = $result->fullHDURL ?? $result->largeImageURL;
$searchResult->width = $result->imageWidth;
$searchResult->height = $result->imageHeight;
$searchResult->fileSize = $result->imageSize;
}
$event->addResult($searchResult);
}
}
}
/**
* @param \Xibo\Event\LibraryProviderImportEvent $event
*/
public function onLibraryImport(LibraryProviderImportEvent $event)
{
foreach ($event->getItems() as $providerImport) {
if ($providerImport->searchResult->provider->id === $this->getSourceName()) {
// Configure this import, setting the URL, etc.
$providerImport->configureDownload();
}
}
}
public function onLibraryList(LibraryProviderListEvent $event)
{
$this->getLogger()->debug('onLibraryList:event');
if (empty($this->getSetting('apiKey'))) {
$this->getLogger()->debug('onLibraryList: No api key');
return;
}
$providerDetails = new ProviderDetails();
$providerDetails->id = 'pixabay';
$providerDetails->link = 'https://pixabay.com';
$providerDetails->logoUrl = '/theme/default/img/connectors/pixabay_logo.svg';
$providerDetails->iconUrl = '/theme/default/img/connectors/pixabay_logo_square.svg';
$providerDetails->backgroundColor = '';
$providerDetails->mediaTypes = ['image', 'video'];
$providerDetails->filters = $this->getFilters();
$event->addProvider($providerDetails);
}
}

View File

@@ -0,0 +1,52 @@
<?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\Connector;
/**
* Provider Details
*/
class ProviderDetails implements \JsonSerializable
{
public $id;
public $message;
public $link;
public $logoUrl;
public $iconUrl;
public $backgroundColor;
public $mediaTypes;
public $filters;
public function jsonSerialize(): array
{
return [
'id' => $this->id,
'message' => $this->message,
'link' => $this->link,
'logoUrl' => $this->logoUrl,
'iconUrl' => $this->iconUrl,
'backgroundColor' => $this->backgroundColor,
'mediaTypes' => $this->mediaTypes,
'filters' => $this->filters
];
}
}

View File

@@ -0,0 +1,86 @@
<?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\Connector;
/**
* A provider import request/result.
* This is used to exchange a search result from a provider for a mediaId in the library.
*/
class ProviderImport implements \JsonSerializable
{
/** @var \Xibo\Entity\SearchResult */
public $searchResult;
/** @var \Xibo\Entity\Media media */
public $media;
/** @var bool has this been configured for import */
public $isConfigured = false;
/** @var string the URL to use for the download */
public $url;
/** @var bool has this been uploaded */
public $isUploaded = false;
/** @var bool is error state? */
public $isError = false;
/** @var string error message, if in error state */
public $error;
/**
* @return \Xibo\Connector\ProviderImport
*/
public function configureDownload(): ProviderImport
{
$this->isConfigured = true;
$this->url = $this->searchResult->download;
return $this;
}
/**
* @param $message
* @return $this
*/
public function setError($message): ProviderImport
{
$this->isUploaded = false;
$this->isError = true;
$this->error = $message;
return $this;
}
/**
* @return array
*/
public function jsonSerialize(): array
{
return [
'item' => $this->searchResult,
'media' => $this->media,
'isUploaded' => $this->isUploaded,
'isError' => $this->isError,
'error' => $this->error
];
}
}

View File

@@ -0,0 +1,977 @@
<?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\Connector;
use Carbon\Carbon;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Exception\ServerException;
use Psr\Container\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Xibo\Entity\User;
use Xibo\Event\ConnectorReportEvent;
use Xibo\Event\MaintenanceRegularEvent;
use Xibo\Event\ReportDataEvent;
use Xibo\Factory\CampaignFactory;
use Xibo\Factory\DisplayFactory;
use Xibo\Helper\DateFormatHelper;
use Xibo\Helper\SanitizerService;
use Xibo\Service\ConfigServiceInterface;
use Xibo\Storage\TimeSeriesStoreInterface;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
use Xibo\Support\Sanitizer\SanitizerInterface;
class XiboAudienceReportingConnector implements ConnectorInterface
{
use ConnectorTrait;
/** @var User */
private $user;
/** @var TimeSeriesStoreInterface */
private $timeSeriesStore;
/** @var SanitizerService */
private $sanitizer;
/** @var ConfigServiceInterface */
private $config;
/** @var CampaignFactory */
private $campaignFactory;
/** @var DisplayFactory */
private $displayFactory;
/**
* @param \Psr\Container\ContainerInterface $container
* @return \Xibo\Connector\ConnectorInterface
*/
public function setFactories(ContainerInterface $container): ConnectorInterface
{
$this->user = $container->get('user');
$this->timeSeriesStore = $container->get('timeSeriesStore');
$this->sanitizer = $container->get('sanitizerService');
$this->config = $container->get('configService');
$this->campaignFactory = $container->get('campaignFactory');
$this->displayFactory = $container->get('displayFactory');
return $this;
}
public function registerWithDispatcher(EventDispatcherInterface $dispatcher): ConnectorInterface
{
$dispatcher->addListener(MaintenanceRegularEvent::$NAME, [$this, 'onRegularMaintenance']);
$dispatcher->addListener(ReportDataEvent::$NAME, [$this, 'onRequestReportData']);
$dispatcher->addListener(ConnectorReportEvent::$NAME, [$this, 'onListReports']);
return $this;
}
public function getSourceName(): string
{
return 'xibo-audience-reporting-connector';
}
public function getTitle(): string
{
return 'Xibo Audience Reporting Connector';
}
/**
* Get the service url, either from settings or a default
* @return string
*/
private function getServiceUrl(): string
{
return $this->getSetting('serviceUrl', 'https://exchange.xibo-adspace.com/api');
}
public function getDescription(): string
{
return 'Enhance your reporting with audience data, impressions and more.';
}
public function getThumbnail(): string
{
return 'theme/default/img/connectors/xibo-audience-reporting.png';
}
public function getSettingsFormTwig(): string
{
return 'xibo-audience-connector-form-settings';
}
public function getSettingsFormJavaScript(): string
{
return 'xibo-audience-connector-form-javascript';
}
public function processSettingsForm(SanitizerInterface $params, array $settings): array
{
if (!$this->isProviderSetting('apiKey')) {
$settings['apiKey'] = $params->getString('apiKey');
}
// Get this connector settings, etc.
$this->getOptionsFromAxe($settings['apiKey'], true);
return $settings;
}
// <editor-fold desc="Listeners">
/**
* @throws NotFoundException
* @throws GeneralException
*/
public function onRegularMaintenance(MaintenanceRegularEvent $event)
{
// We should only do this if the connector is enabled and if we have an API key
$apiKey = $this->getSetting('apiKey');
if (empty($apiKey)) {
$this->getLogger()->debug('onRegularMaintenance: No api key');
return;
}
$event->addMessage('## Audience Connector');
// Set displays on DMAs
foreach ($this->dmaSearch($this->sanitizer->getSanitizer([]))['data'] as $dma) {
if ($dma['displayGroupId'] !== null) {
$this->setDisplaysForDma($dma['_id'], $dma['displayGroupId']);
}
}
// Handle sending stats to the audience connector service API
try {
$defaultTimezone = $this->config->getSetting('defaultTimezone');
// Get Watermark (might be null - start from beginning)
$watermark = $this->getWatermark();
// Loop over 5000 stat records
// Only interested in layout stats which belong to a parent campaign
$params = [
'type' => 'layout',
'start' => 0,
'length' => $this->getSetting('batchSize', 5000),
'mustHaveParentCampaign' => true,
];
// If the watermark is not empty, we go from this point
if (!empty($watermark)) {
$params['statId'] = $watermark;
}
$this->getLogger()->debug('onRegularMaintenance: Processing batch of stats with params: '
. json_encode($params));
// Call the time series interface getStats
$resultSet = $this->timeSeriesStore->getStats($params, true);
// Array of campaigns for which we will update the total spend, impresssions, and plays
$campaigns = [];
$adCampaignCache = [];
$listCampaignCache = [];
$displayCache = [];
$displayIdsDeleted = [];
$erroredCampaign = [];
$rows = [];
$updateWatermark = null;
// Process the stats one by one
while ($row = $resultSet->getNextRow()) {
try {
$sanitizedRow = $this->sanitizer->getSanitizer($row);
$parentCampaignId = $sanitizedRow->getInt('parentCampaignId', ['default' => 0]);
$displayId = $sanitizedRow->getInt('displayId');
$statId = $resultSet->getIdFromRow($row);
// Keep this watermark, so we update it later
$updateWatermark = $statId;
// Skip records we're not interested in, or records that have already been discounted before.
if (empty($parentCampaignId)
|| empty($displayId)
|| in_array($displayId, $displayIdsDeleted)
|| array_key_exists($parentCampaignId, $erroredCampaign)
|| array_key_exists($parentCampaignId, $listCampaignCache)
) {
// Comment out this log to save recording messages unless we need to troubleshoot in dev
//$this->getLogger()->debug('onRegularMaintenance: Campaign is a list campaign '
// . $parentCampaignId);
continue;
}
// Build an array to represent the row we want to send.
$entry = [
'id' => $statId,
'parentCampaignId' => $parentCampaignId,
'displayId' => $displayId,
];
// --------
// Get Campaign
// Campaign start and end date
if (array_key_exists($parentCampaignId, $adCampaignCache)) {
$entry['campaignStart'] = $adCampaignCache[$parentCampaignId]['start'];
$entry['campaignEnd'] = $adCampaignCache[$parentCampaignId]['end'];
} else {
// Get Campaign
try {
$parentCampaign = $this->campaignFactory->getById($parentCampaignId);
} catch (\Exception $exception) {
$this->getLogger()->error('onRegularMaintenance: campaign with ID '
. $parentCampaignId . ' not found');
$erroredCampaign[$parentCampaignId] = $entry['id']; // first stat id
continue;
}
if ($parentCampaign->type == 'ad') {
$adCampaignCache[$parentCampaignId]['type'] = $parentCampaign->type;
} else {
$this->getLogger()->debug('onRegularMaintenance: campaign is a list '
. $parentCampaignId);
$listCampaignCache[$parentCampaignId] = $parentCampaignId;
continue;
}
if (!empty($parentCampaign->getStartDt()) && !empty($parentCampaign->getEndDt())) {
$adCampaignCache[$parentCampaignId]['start'] = $parentCampaign->getStartDt()
->format(DateFormatHelper::getSystemFormat());
$adCampaignCache[$parentCampaignId]['end'] = $parentCampaign->getEndDt()
->format(DateFormatHelper::getSystemFormat());
$entry['campaignStart'] = $adCampaignCache[$parentCampaignId]['start'];
$entry['campaignEnd'] = $adCampaignCache[$parentCampaignId]['end'];
} else {
$this->getLogger()->error('onRegularMaintenance: campaign without dates '
. $parentCampaignId);
$erroredCampaign[$parentCampaignId] = $entry['id']; // first stat id
continue;
}
}
// Get Display
// -----------
// Cost per play and impressions per play
if (!array_key_exists($displayId, $displayCache)) {
try {
$display = $this->displayFactory->getById($displayId);
$displayCache[$displayId]['costPerPlay'] = $display->costPerPlay;
$displayCache[$displayId]['impressionsPerPlay'] = $display->impressionsPerPlay;
$displayCache[$displayId]['timeZone'] = empty($display->timeZone) ? $defaultTimezone : $display->timeZone;
} catch (NotFoundException $notFoundException) {
$this->getLogger()->error('onRegularMaintenance: display not found with ID: '
. $displayId);
$displayIdsDeleted[] = $displayId;
continue;
}
}
$entry['costPerPlay'] = $displayCache[$displayId]['costPerPlay'];
$entry['impressionsPerPlay'] = $displayCache[$displayId]['impressionsPerPlay'];
// Converting the date into the format expected by the API
// --------
// We know that player's local dates were stored in the CMS's configured timezone
// Dates were saved in Unix timestamps in MySQL
// Dates were saved in UTC format in MongoDB
// The main difference is that MySQL stores dates in the timezone of the CMS,
// while MongoDB converts those dates to UTC before storing them.
// -----MySQL
// Carbon::createFromTimestamp() always applies the CMS timezone
// ------MongoDB
// $date->toDateTime() returns a PHP DateTime object from MongoDB BSON Date type (UTC)
// Carbon::instance() keeps the timezone as UTC
try {
$start = $resultSet->getDateFromValue($row['start']);
$end = $resultSet->getDateFromValue($row['end']);
} catch (\Exception $exception) {
$this->getLogger()->error('onRegularMaintenance: Date convert failed for ID '
. $entry['id'] . ' with error: '. $exception->getMessage());
continue;
}
// Convert dates to display timezone
$entry['start'] = $start->timezone($displayCache[$displayId]['timeZone'])->format(DateFormatHelper::getSystemFormat());
$entry['end'] = $end->timezone($displayCache[$displayId]['timeZone'])->format(DateFormatHelper::getSystemFormat());
$entry['layoutId'] = $sanitizedRow->getInt('layoutId', ['default' => 0]);
$entry['numberPlays'] = $sanitizedRow->getInt('count', ['default' => 0]);
$entry['duration'] = $sanitizedRow->getInt('duration', ['default' => 0]);
$entry['engagements'] = $resultSet->getEngagementsFromRow($row);
$rows[] = $entry;
// Campaign list in array
if (!in_array($parentCampaignId, $campaigns)) {
$campaigns[] = $parentCampaignId;
}
} catch (\Exception $exception) {
$this->getLogger()->error('onRegularMaintenance: unexpected exception processing row '
. ($entry['id'] ?? null) . ', e: ' . $exception->getMessage());
}
}
if (count($erroredCampaign) > 0) {
$event->addMessage(sprintf(
__('There were %d campaigns which failed. A summary is in the error log.'),
count($erroredCampaign)
));
$this->getLogger()->error('onRegularMaintenance: Failure summary of campaignId and first statId:'
. json_encode($erroredCampaign));
}
$this->getLogger()->debug('onRegularMaintenance: Records to send: ' . count($rows)
. ', Watermark: ' . $watermark);
$this->getLogger()->debug('onRegularMaintenance: Campaigns: ' . json_encode($campaigns));
// If we have rows, send them.
if (count($rows) > 0) {
// All outcomes from here are either a break; or an exception to stop the loop.
try {
$response = $this->getClient()->post($this->getServiceUrl() . '/audience/receiveStats', [
'timeout' => $this->getSetting('receiveStatsTimeout', 300), // 5 minutes
'headers' => [
'X-API-KEY' => $this->getSetting('apiKey')
],
'json' => $rows
]);
$statusCode = $response->getStatusCode();
$this->getLogger()->debug('onRegularMaintenance: Receive Stats StatusCode: ' . $statusCode);
// Get Campaign Total
if ($statusCode == 204) {
$this->getAndUpdateCampaignTotal($campaigns);
}
$event->addMessage('Added ' . count($rows) . ' to audience API');
} catch (RequestException $requestException) {
// If a request fails completely, we should stop and log the error.
$this->getLogger()->error('onRegularMaintenance: Audience receiveStats: failed e = '
. $requestException->getMessage());
throw new GeneralException(__('Failed to send stats to audience API'));
}
}
// Update the last statId of the block as the watermark
if (!empty($updateWatermark)) {
$this->setWatermark($updateWatermark);
}
} catch (GeneralException $exception) {
// We should have recorded in the error log already, so we just append to the event message for task
// last run status.
$event->addMessage($exception->getMessage());
}
}
/**
* Get the watermark representing how far we've processed already
* @return mixed|null
* @throws \Xibo\Support\Exception\GeneralException
*/
private function getWatermark()
{
// If the watermark request fails, we should error.
try {
$this->getLogger()->debug('onRegularMaintenance: Get Watermark');
$response = $this->getClient()->get($this->getServiceUrl() . '/audience/watermark', [
'headers' => [
'X-API-KEY' => $this->getSetting('apiKey')
],
]);
$body = $response->getBody()->getContents();
$json = json_decode($body, true);
return $json['watermark'] ?? null;
} catch (RequestException $requestException) {
$this->getLogger()->error('getWatermark: failed e = ' . $requestException->getMessage());
throw new GeneralException(__('Cannot get watermark'));
}
}
/**
* Set the watermark representing how far we've processed already
* @return void
* @throws \Xibo\Support\Exception\GeneralException
*/
private function setWatermark($watermark)
{
// If the watermark set fails, we should error.
try {
$this->getLogger()->debug('onRegularMaintenance: Set Watermark ' . $watermark);
$this->getClient()->post($this->getServiceUrl() . '/audience/watermark', [
'headers' => [
'X-API-KEY' => $this->getSetting('apiKey')
],
'json' => ['watermark' => $watermark]
]);
} catch (RequestException $requestException) {
$this->getLogger()->error('setWatermark: failed e = ' . $requestException->getMessage());
throw new GeneralException(__('Cannot set watermark'));
}
}
/**
* @param array $campaigns
* @return void
* @throws \Xibo\Support\Exception\GeneralException
*/
private function getAndUpdateCampaignTotal(array $campaigns)
{
$this->getLogger()->debug('onRegularMaintenance: Get Campaign Total');
try {
$response = $this->getClient()->get($this->getServiceUrl() . '/audience/campaignTotal', [
'headers' => [
'X-API-KEY' => $this->getSetting('apiKey')
],
'query' => [
'campaigns' => $campaigns
]
]);
$body = $response->getBody()->getContents();
$results = json_decode($body, true);
$this->getLogger()->debug('onRegularMaintenance: Campaign Total Results: ' . json_encode($results));
foreach ($results as $item) {
try {
// Save the total in the campaign
$campaign = $this->campaignFactory->getById($item['id']);
$this->getLogger()->debug('onRegularMaintenance: Campaign Id: ' . $item['id']
. ' Spend: ' . $campaign->spend . ' Impressions: ' . $campaign->impressions);
$campaign->spend = $item['spend'];
$campaign->impressions = $item['impressions'];
$campaign->overwritePlays();
$this->getLogger()->debug('onRegularMaintenance: Campaign Id: ' . $item['id']
. ' Spend(U): ' . $campaign->spend . ' Impressions(U): ' . $campaign->impressions);
} catch (NotFoundException $notFoundException) {
$this->getLogger()->error('onRegularMaintenance: campaignId '
. $item['id']. ' should have existed, but did not.');
throw new GeneralException(sprintf(__('Cannot update campaign status for %d'), $item['id']));
}
}
} catch (RequestException $requestException) {
$this->getLogger()->error('Campaign total: e = ' . $requestException->getMessage());
throw new GeneralException(__('Failed to update campaign totals.'));
}
}
/**
* Request Report results from the audience report service
*/
public function onRequestReportData(ReportDataEvent $event)
{
$this->getLogger()->debug('onRequestReportData');
$type = $event->getReportType();
$typeUrl = [
'campaignProofofplay' => $this->getServiceUrl() . '/audience/campaign/proofofplay',
'mobileProofofplay' => $this->getServiceUrl() . '/audience/campaign/proofofplay/mobile',
'displayAdPlay' => $this->getServiceUrl() . '/audience/display/adplays',
'displayPercentage' => $this->getServiceUrl() . '/audience/display/percentage'
];
if (array_key_exists($type, $typeUrl)) {
$json = [];
switch ($type) {
case 'campaignProofofplay':
// Get campaign proofofplay result
try {
$response = $this->getClient()->get($typeUrl[$type], [
'headers' => [
'X-API-KEY' => $this->getSetting('apiKey')
],
'query' => $event->getParams()
]);
$body = $response->getBody()->getContents();
$json = json_decode($body, true);
} catch (RequestException $requestException) {
$this->getLogger()->error('Get '. $type.': failed. e = ' . $requestException->getMessage());
$error = 'Failed to get campaign proofofplay result: '.$requestException->getMessage();
}
break;
case 'mobileProofofplay':
// Get mobile proofofplay result
try {
$response = $this->getClient()->get($typeUrl[$type], [
'headers' => [
'X-API-KEY' => $this->getSetting('apiKey')
],
'query' => $event->getParams()
]);
$body = $response->getBody()->getContents();
$json = json_decode($body, true);
} catch (RequestException $requestException) {
$this->getLogger()->error('Get '. $type.': failed. e = ' . $requestException->getMessage());
$error = 'Failed to get mobile proofofplay result: '.$requestException->getMessage();
}
break;
case 'displayAdPlay':
// Get display adplays result
try {
$response = $this->getClient()->get($typeUrl[$type], [
'headers' => [
'X-API-KEY' => $this->getSetting('apiKey')
],
'query' => $event->getParams()
]);
$body = $response->getBody()->getContents();
$json = json_decode($body, true);
} catch (RequestException $requestException) {
$this->getLogger()->error('Get '. $type.': failed. e = ' . $requestException->getMessage());
$error = 'Failed to get display adplays result: '.$requestException->getMessage();
}
break;
case 'displayPercentage':
// Get display played percentage result
try {
$response = $this->getClient()->get($typeUrl[$type], [
'headers' => [
'X-API-KEY' => $this->getSetting('apiKey')
],
'query' => $event->getParams()
]);
$body = $response->getBody()->getContents();
$json = json_decode($body, true);
} catch (RequestException $requestException) {
$this->getLogger()->error('Get '. $type.': failed. e = ' . $requestException->getMessage());
$error = 'Failed to get display played percentage result: '.$requestException->getMessage();
}
break;
default:
$this->getLogger()->error('Connector Report not found ');
}
$event->setResults([
'json' => $json,
'error' => $error ?? null
]);
}
}
/**
* Get this connector reports
* @param ConnectorReportEvent $event
* @return void
*/
public function onListReports(ConnectorReportEvent $event)
{
$this->getLogger()->debug('onListReports');
$connectorReports = [
[
'name'=> 'campaignProofOfPlay',
'description'=> 'Campaign Proof of Play',
'class'=> '\\Xibo\\Report\\CampaignProofOfPlay',
'type'=> 'Report',
'output_type'=> 'table',
'color'=> 'gray',
'fa_icon'=> 'fa-th',
'category'=> 'Connector Reports',
'feature'=> 'campaign-proof-of-play',
'adminOnly'=> 0,
'sort_order' => 1
],
[
'name'=> 'mobileProofOfPlay',
'description'=> 'Mobile Proof of Play',
'class'=> '\\Xibo\\Report\\MobileProofOfPlay',
'type'=> 'Report',
'output_type'=> 'table',
'color'=> 'green',
'fa_icon'=> 'fa-th',
'category'=> 'Connector Reports',
'feature'=> 'mobile-proof-of-play',
'adminOnly'=> 0,
'sort_order' => 2
],
[
'name'=> 'displayPercentage',
'description'=> 'Display Played Percentage',
'class'=> '\\Xibo\\Report\\DisplayPercentage',
'type'=> 'Chart',
'output_type'=> 'both',
'color'=> 'blue',
'fa_icon'=> 'fa-pie-chart',
'category'=> 'Connector Reports',
'feature'=> 'display-report',
'adminOnly'=> 0,
'sort_order' => 3
],
// [
// 'name'=> 'revenueByDisplayReport',
// 'description'=> 'Revenue by Display',
// 'class'=> '\\Xibo\\Report\\RevenueByDisplay',
// 'type'=> 'Report',
// 'output_type'=> 'table',
// 'color'=> 'green',
// 'fa_icon'=> 'fa-th',
// 'category'=> 'Connector Reports',
// 'feature'=> 'display-report',
// 'adminOnly'=> 0,
// 'sort_order' => 4
// ],
[
'name'=> 'displayAdPlay',
'description'=> 'Display Ad Plays',
'class'=> '\\Xibo\\Report\\DisplayAdPlay',
'type'=> 'Chart',
'output_type'=> 'both',
'color'=> 'red',
'fa_icon'=> 'fa-bar-chart',
'category'=> 'Connector Reports',
'feature'=> 'display-report',
'adminOnly'=> 0,
'sort_order' => 5
],
];
$reports = [];
foreach ($connectorReports as $connectorReport) {
// Compatibility check
if (!isset($connectorReport['feature']) || !isset($connectorReport['category'])) {
continue;
}
// Check if only allowed for admin
if ($this->user->userTypeId != 1) {
if (isset($connectorReport['adminOnly']) && !empty($connectorReport['adminOnly'])) {
continue;
}
}
$reports[$connectorReport['category']][] = (object) $connectorReport;
}
if (count($reports) > 0) {
$event->addReports($reports);
}
}
// </editor-fold>
// <editor-fold desc="Proxy methods">
public function dmaSearch(SanitizerInterface $params): array
{
try {
$response = $this->getClient()->get($this->getServiceUrl() . '/dma', [
'timeout' => 120,
'headers' => [
'X-API-KEY' => $this->getSetting('apiKey'),
],
]);
$body = json_decode($response->getBody()->getContents(), true);
if (!$body) {
throw new GeneralException(__('No response'));
}
return [
'data' => $body,
'recordsTotal' => count($body),
];
} catch (\Exception $e) {
$this->getLogger()->error('activity: e = ' . $e->getMessage());
}
return [
'data' => [],
'recordsTotal' => 0,
];
}
/**
* @throws \Xibo\Support\Exception\InvalidArgumentException
* @throws \Xibo\Support\Exception\GeneralException
*/
public function dmaAdd(SanitizerInterface $params): array
{
$startDate = $params->getDate('startDate');
if ($startDate !== null) {
$startDate = $startDate->format('Y-m-d');
}
$endDate = $params->getDate('endDate');
if ($endDate !== null) {
$endDate = $endDate->format('Y-m-d');
}
try {
$response = $this->getClient()->post($this->getServiceUrl() . '/dma', [
'timeout' => 120,
'headers' => [
'X-API-KEY' => $this->getSetting('apiKey'),
],
'json' => [
'name' => $params->getString('name'),
'costPerPlay' => $params->getDouble('costPerPlay'),
'impressionSource' => $params->getString('impressionSource'),
'impressionsPerPlay' => $params->getDouble('impressionsPerPlay'),
'startDate' => $startDate,
'endDate' => $endDate,
'daysOfWeek' => $params->getIntArray('daysOfWeek'),
'startTime' => $params->getString('startTime'),
'endTime' => $params->getString('endTime'),
'geoFence' => json_decode($params->getString('geoFence'), true),
'priority' => $params->getInt('priority'),
'displayGroupId' => $params->getInt('displayGroupId'),
],
]);
$body = json_decode($response->getBody()->getContents(), true);
if (!$body) {
throw new GeneralException(__('No response'));
}
// Set the displays
$this->setDisplaysForDma($body['_id'], $params->getInt('displayGroupId'));
return $body;
} catch (\Exception $e) {
$this->handleException($e);
}
}
/**
* @throws \Xibo\Support\Exception\InvalidArgumentException
* @throws \Xibo\Support\Exception\GeneralException
*/
public function dmaEdit(SanitizerInterface $params): array
{
$startDate = $params->getDate('startDate');
if ($startDate !== null) {
$startDate = $startDate->format('Y-m-d');
}
$endDate = $params->getDate('endDate');
if ($endDate !== null) {
$endDate = $endDate->format('Y-m-d');
}
try {
$response = $this->getClient()->put($this->getServiceUrl() . '/dma/' . $params->getString('_id'), [
'timeout' => 120,
'headers' => [
'X-API-KEY' => $this->getSetting('apiKey'),
],
'json' => [
'name' => $params->getString('name'),
'costPerPlay' => $params->getDouble('costPerPlay'),
'impressionSource' => $params->getString('impressionSource'),
'impressionsPerPlay' => $params->getDouble('impressionsPerPlay'),
'startDate' => $startDate,
'endDate' => $endDate,
'daysOfWeek' => $params->getIntArray('daysOfWeek'),
'startTime' => $params->getString('startTime'),
'endTime' => $params->getString('endTime'),
'geoFence' => json_decode($params->getString('geoFence'), true),
'priority' => $params->getInt('priority'),
'displayGroupId' => $params->getInt('displayGroupId'),
],
]);
$body = json_decode($response->getBody()->getContents(), true);
if (!$body) {
throw new GeneralException(__('No response'));
}
// Set the displays
$this->setDisplaysForDma($body['_id'], $params->getInt('displayGroupId'));
return $body;
} catch (\Exception $e) {
$this->handleException($e);
}
}
/**
* @throws \Xibo\Support\Exception\InvalidArgumentException
* @throws \Xibo\Support\Exception\GeneralException
*/
public function dmaDelete(SanitizerInterface $params)
{
try {
$this->getClient()->delete($this->getServiceUrl() . '/dma/' . $params->getString('_id'), [
'timeout' => 120,
'headers' => [
'X-API-KEY' => $this->getSetting('apiKey'),
],
]);
return null;
} catch (\Exception $e) {
$this->handleException($e);
}
}
// </editor-fold>
/**
* @throws \Xibo\Support\Exception\InvalidArgumentException
* @throws \Xibo\Support\Exception\GeneralException
*/
public function getOptionsFromAxe($apiKey = null, $throw = false)
{
$apiKey = $apiKey ?? $this->getSetting('apiKey');
if (empty($apiKey)) {
if ($throw) {
throw new InvalidArgumentException(__('Please provide an API key'));
} else {
return [
'error' => true,
'message' => __('Please provide an API key'),
];
}
}
try {
$response = $this->getClient()->get($this->getServiceUrl() . '/options', [
'timeout' => 120,
'headers' => [
'X-API-KEY' => $apiKey,
],
]);
return json_decode($response->getBody()->getContents(), true);
} catch (\Exception $e) {
try {
$this->handleException($e);
} catch (\Exception $exception) {
if ($throw) {
throw $exception;
} else {
return [
'error' => true,
'message' => $exception->getMessage() ?: __('Unknown Error'),
];
}
}
}
}
private function setDisplaysForDma($dmaId, $displayGroupId)
{
// Get displays
$displayIds = [];
foreach ($this->displayFactory->getByDisplayGroupId($displayGroupId) as $display) {
$displayIds[] = $display->displayId;
}
// Make a blind call to update this DMA.
try {
$this->getClient()->post($this->getServiceUrl() . '/dma/' . $dmaId . '/displays', [
'headers' => [
'X-API-KEY' => $this->getSetting('apiKey')
],
'json' => [
'displays' => $displayIds,
]
]);
} catch (\Exception $e) {
$this->getLogger()->error('Exception updating Displays for dmaId: ' . $dmaId
. ', e: ' . $e->getMessage());
}
}
/**
* @param \Exception $exception
* @return void
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\InvalidArgumentException
*/
private function handleException($exception)
{
$this->getLogger()->debug('handleException: ' . $exception->getMessage());
$this->getLogger()->debug('handleException: ' . $exception->getTraceAsString());
if ($exception instanceof ClientException) {
if ($exception->hasResponse()) {
$body = $exception->getResponse()->getBody() ?? null;
if (!empty($body)) {
$decodedBody = json_decode($body, true);
$message = $decodedBody['message'] ?? $body;
} else {
$message = __('An unknown error has occurred.');
}
switch ($exception->getResponse()->getStatusCode()) {
case 422:
throw new InvalidArgumentException($message);
case 404:
throw new NotFoundException($message);
case 401:
throw new AccessDeniedException(__('Access denied, please check your API key'));
default:
throw new GeneralException(sprintf(
__('Unknown client exception processing your request, error code is %s'),
$exception->getResponse()->getStatusCode()
));
}
} else {
throw new InvalidArgumentException(__('Invalid request'));
}
} elseif ($exception instanceof ServerException) {
$this->getLogger()->error('handleException:' . $exception->getMessage());
throw new GeneralException(__('There was a problem processing your request, please try again'));
} else {
throw new GeneralException(__('Unknown Error'));
}
}
}

View File

@@ -0,0 +1,551 @@
<?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\Connector;
use GuzzleHttp\Exception\RequestException;
use Nyholm\Psr7\Factory\Psr17Factory;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Xibo\Event\DashboardDataRequestEvent;
use Xibo\Event\MaintenanceRegularEvent;
use Xibo\Event\WidgetEditOptionRequestEvent;
use Xibo\Event\XmdsConnectorFileEvent;
use Xibo\Event\XmdsConnectorTokenEvent;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
use Xibo\Support\Sanitizer\SanitizerInterface;
/**
* Xibo Dashboard Service connector.
* This connector collects credentials and sends them off to the dashboard service
*/
class XiboDashboardConnector implements ConnectorInterface
{
use ConnectorTrait;
/** @var float|int The token TTL */
const TOKEN_TTL_SECONDS = 3600 * 24 * 2;
/** @var string Used when rendering the form */
private $errorMessage;
/** @var array Cache of available services */
private $availableServices = null;
/** @var string Cache key for credential states */
private $cacheKey = 'connector/xibo_dashboard_connector_statuses';
/** @var array Cache of error types */
private $cachedErrorTypes = null;
public function registerWithDispatcher(EventDispatcherInterface $dispatcher): ConnectorInterface
{
$dispatcher->addListener(MaintenanceRegularEvent::$NAME, [$this, 'onRegularMaintenance']);
$dispatcher->addListener(XmdsConnectorFileEvent::$NAME, [$this, 'onXmdsFile']);
$dispatcher->addListener(XmdsConnectorTokenEvent::$NAME, [$this, 'onXmdsToken']);
$dispatcher->addListener(WidgetEditOptionRequestEvent::$NAME, [$this, 'onWidgetEditOption']);
$dispatcher->addListener(DashboardDataRequestEvent::$NAME, [$this, 'onDataRequest']);
return $this;
}
public function getSourceName(): string
{
return 'xibo-dashboard-connector';
}
public function getTitle(): string
{
return 'Xibo Dashboard Service';
}
public function getDescription(): string
{
return 'Add your dashboard credentials for use in the Dashboard widget.';
}
public function getThumbnail(): string
{
return 'theme/default/img/connectors/xibo-dashboards.png';
}
public function getSettingsFormTwig(): string
{
return 'xibo-dashboard-form-settings';
}
/**
* Get the service url, either from settings or a default
* @return string
*/
public function getServiceUrl(): string
{
return $this->getSetting('serviceUrl', 'https://api.dashboards.xibosignage.com');
}
/**
* @throws \Xibo\Support\Exception\InvalidArgumentException
*/
public function processSettingsForm(SanitizerInterface $params, array $settings): array
{
// Remember the old service URL
$existingApiKey = $this->getSetting('apiKey');
if (!$this->isProviderSetting('apiKey')) {
$settings['apiKey'] = $params->getString('apiKey');
}
// What if the user changes their API key?
// Handle existing credentials
if ($existingApiKey !== $settings['apiKey']) {
// Test the new API key.
$services = $this->getAvailableServices(true, $settings['apiKey']);
if (!is_array($services)) {
throw new InvalidArgumentException($services);
}
// The new key is valid, clear out the old key's credentials.
if (!empty($existingApiKey)) {
foreach ($this->getCredentials() as $type => $credential) {
try {
$this->getClient()->delete(
$this->getServiceUrl() . '/services/' . $type . '/' . $credential['id'],
[
'headers' => [
'X-API-KEY' => $existingApiKey
]
]
);
} catch (RequestException $requestException) {
$this->getLogger()->error('getAvailableServices: delete failed. e = '
. $requestException->getMessage());
}
}
}
$credentials = [];
} else {
$credentials = $this->getCredentials();
}
$this->getLogger()->debug('Processing credentials');
foreach ($this->getAvailableServices(false, $settings['apiKey']) as $service) {
// Pull in the parameters for this service.
$id = $params->getString($service['type'] . '_id');
$isMarkedForRemoval = $params->getCheckbox($service['type'] . '_remove') == 1;
if (empty($id)) {
$userName = $params->getString($service['type'] . '_userName');
} else {
$userName = $credentials[$service['type']]['userName'] ?? null;
// This shouldn't happen because we had it when the form opened.
if ($userName === null) {
$isMarkedForRemoval = true;
}
}
$password = $params->getParam($service['type'] . '_password');
$twoFactorSecret = $params->getString($service['type'] . '_twoFactorSecret');
$isUrl = isset($service['isUrl']);
$url = ($isUrl) ? $params->getString($service['type' ]. '_url') : '';
if (!empty($id) && $isMarkedForRemoval) {
// Existing credential marked for removal
try {
$this->getClient()->delete($this->getServiceUrl() . '/services/' . $service['type'] . '/' . $id, [
'headers' => [
'X-API-KEY' => $this->getSetting('apiKey')
]
]);
} catch (RequestException $requestException) {
$this->getLogger()->error('getAvailableServices: delete failed. e = '
. $requestException->getMessage());
}
unset($credentials[$service['type']]);
} else if (!empty($userName) && !empty($password)) {
// A new service or an existing service with a changed password.
// Make a request to our service URL.
try {
$response = $this->getClient()->post(
$this->getServiceUrl() . '/services/' . $service['type'],
[
'headers' => [
'X-API-KEY' => $this->getSetting('apiKey')
],
'json' => [
'username' => $userName,
'password' => $password,
'totp' => $twoFactorSecret,
'url' => $url
],
'timeout' => 120
]
);
$json = json_decode($response->getBody()->getContents(), true);
if (empty($json)) {
throw new InvalidArgumentException(__('Empty response from the dashboard service'), $service['type']);
}
$credentialId = $json['id'];
$credentials[$service['type']] = [
'userName' => $userName,
'id' => $credentialId,
'status' => true
];
} catch (RequestException $requestException) {
$this->getLogger()->error('getAvailableServices: e = ' . $requestException->getMessage());
throw new InvalidArgumentException(__('Cannot register those credentials.'), $service['type']);
}
}
}
// Set the credentials
$settings['credentials'] = $credentials;
return $settings;
}
public function getCredentialForType(string $type)
{
return $this->settings['credentials'][$type] ?? null;
}
public function getCredentials(): array
{
return $this->settings['credentials'] ?? [];
}
/**
* Used by the Twig template
* @param string $type
* @return bool
*/
public function isCredentialInErrorState(string $type): bool
{
if ($this->cachedErrorTypes === null) {
$item = $this->getPool()->getItem($this->cacheKey);
if ($item->isHit()) {
$this->cachedErrorTypes = $item->get();
} else {
$this->cachedErrorTypes = [];
}
}
return in_array($type, $this->cachedErrorTypes);
}
/**
* @return array|mixed|string|null
*/
public function getAvailableServices(bool $isReturnError = true, ?string $withApiKey = null)
{
if ($withApiKey) {
$apiKey = $withApiKey;
} else {
$apiKey = $this->getSetting('apiKey');
if (empty($apiKey)) {
return [];
}
}
if ($this->availableServices === null) {
$this->getLogger()->debug('getAvailableServices: Requesting available services.');
try {
$response = $this->getClient()->get($this->getServiceUrl() . '/services', [
'headers' => [
'X-API-KEY' => $apiKey
]
]);
$body = $response->getBody()->getContents();
$this->getLogger()->debug('getAvailableServices: ' . $body);
$json = json_decode($body, true);
if (empty($json)) {
throw new InvalidArgumentException(__('Empty response from the dashboard service'));
}
$this->availableServices = $json;
} catch (RequestException $e) {
$this->getLogger()->error('getAvailableServices: e = ' . $e->getMessage());
$message = json_decode($e->getResponse()->getBody()->getContents(), true);
if ($isReturnError) {
return empty($message)
? __('Cannot contact dashboard service, please try again shortly.')
: $message['message'];
} else {
return [];
}
} catch (\Exception $e) {
$this->getLogger()->error('getAvailableServices: e = ' . $e->getMessage());
if ($isReturnError) {
return __('Cannot contact dashboard service, please try again shortly.');
} else {
return [];
}
}
}
return $this->availableServices;
}
public function onRegularMaintenance(MaintenanceRegularEvent $event)
{
$this->getLogger()->debug('onRegularMaintenance');
$credentials = $this->getCredentials();
if (count($credentials) <= 0) {
$this->getLogger()->debug('onRegularMaintenance: No credentials configured, nothing to do.');
return;
}
$services = [];
foreach ($credentials as $credential) {
// Build up a request to ping the service.
$services[] = $credential['id'];
}
try {
$response = $this->getClient()->post(
$this->getServiceUrl() . '/services',
[
'headers' => [
'X-API-KEY' => $this->getSetting('apiKey')
],
'json' => $services
]
);
$body = $response->getBody()->getContents();
if (empty($body)) {
throw new NotFoundException('Empty response');
}
$json = json_decode($body, true);
if (!is_array($json)) {
throw new GeneralException('Invalid response body: ' . $body);
}
// Parse the response and activate/deactivate services accordingly.
$erroredTypes = [];
foreach ($credentials as $type => $credential) {
// Get this service from the response.
foreach ($json as $item) {
if ($item['id'] === $credential['id']) {
if ($item['status'] !== true) {
$this->getLogger()->error($type . ' credential is in error state');
$erroredTypes[] = $type;
}
continue 2;
}
}
$erroredTypes[] = $type;
$this->getLogger()->error($type . ' credential is not present');
}
// Cache the errored types.
if (count($erroredTypes) > 0) {
$item = $this->getPool()->getItem($this->cacheKey);
$item->set($erroredTypes);
$item->expiresAfter(3600 * 4);
$this->getPool()->save($item);
} else {
$this->getPool()->deleteItem($this->cacheKey);
}
} catch (\Exception $e) {
$event->addMessage(__('Error calling Dashboard service'));
$this->getLogger()->error('onRegularMaintenance: dashboard service e = ' . $e->getMessage());
}
}
public function onXmdsToken(XmdsConnectorTokenEvent $event)
{
$this->getLogger()->debug('onXmdsToken');
// We are either generating a new token, or verifying an old one.
if (empty($event->getToken())) {
$this->getLogger()->debug('onXmdsToken: empty token, generate a new one');
// Generate a new token
$token = $this->getJwtService()->generateJwt(
$this->getTitle(),
$this->getSourceName(),
$event->getWidgetId(),
$event->getDisplayId(),
$event->getTtl()
);
$event->setToken($token->toString());
} else {
$this->getLogger()->debug('onXmdsToken: Validate the token weve been given');
try {
$token = $this->getJwtService()->validateJwt($event->getToken());
if ($token === null) {
throw new NotFoundException(__('Cannot decode token'));
}
if ($this->getSourceName() === $token->claims()->get('aud')) {
$this->getLogger()->debug('onXmdsToken: Token not for this connector');
return;
}
// Configure the event with details from this token
$displayId = intval($token->claims()->get('sub'));
$widgetId = intval($token->claims()->get('jti'));
$event->setTargets($displayId, $widgetId);
$this->getLogger()->debug('onXmdsToken: Configured event with displayId: ' . $displayId
. ', widgetId: ' . $widgetId);
} catch (\Exception $exception) {
$this->getLogger()->error('onXmdsToken: Invalid token, e = ' . $exception->getMessage());
}
}
}
public function onXmdsFile(XmdsConnectorFileEvent $event)
{
$this->getLogger()->debug('onXmdsFile');
try {
// Get the widget
$widget = $event->getWidget();
if ($widget === null) {
throw new NotFoundException();
}
// We want options, so load the widget
$widget->load();
$type = $widget->getOptionValue('type', 'powerbi');
// Get the credentials for this type.
$credentials = $this->getCredentialForType($type);
if ($credentials === null) {
throw new NotFoundException(sprintf(__('No credentials logged for %s'), $type));
}
// Add headers
$headers = [
'X-API-KEY' => $this->getSetting('apiKey')
];
$response = $this->getClient()->get($this->getServiceUrl() . '/services/' . $type, [
'headers' => $headers,
'query' => [
'credentialId' => $credentials['id'],
'url' => $widget->getOptionValue('url', ''),
'interval' => $widget->getOptionValue('updateInterval', 60) * 60,
'debug' => $event->isDebug()
]
]);
// Create a response
$factory = new Psr17Factory();
$event->setResponse(
$factory->createResponse(200)
->withHeader('Content-Type', $response->getHeader('Content-Type'))
->withHeader('Cache-Control', $response->getHeader('Cache-Control'))
->withHeader('Last-Modified', $response->getHeader('Last-Modified'))
->withBody($response->getBody())
);
} catch (\Exception $exception) {
// We log any error and return empty
$this->getLogger()->error('onXmdsFile: unknown error: ' . $exception->getMessage());
}
}
public function onWidgetEditOption(WidgetEditOptionRequestEvent $event)
{
$this->getLogger()->debug('onWidgetEditOption');
// Pull the widget we're working with.
$widget = $event->getWidget();
if ($widget === null) {
throw new NotFoundException();
}
// Pull in existing information
$existingType = $event->getPropertyValue();
$options = $event->getOptions();
// We handle the dashboard widget and the property with id="type"
if ($widget->type === 'dashboard' && $event->getPropertyId() === 'type') {
// get available services
$services = $this->getAvailableServices(true, $this->getSetting('apiKey'));
foreach ($services as $option) {
// Filter the list of options by the property value provided (if there is one).
if (empty($existingType) || $option['type'] === $existingType) {
$options[] = $option;
}
}
// Set these options on the event.
$event->setOptions($options);
}
}
public function onDataRequest(DashboardDataRequestEvent $event, $eventName, EventDispatcherInterface $dispatcher)
{
$this->getLogger()->debug('onDataRequest');
// Validate that we're configured.
if (empty($this->getSetting('apiKey'))) {
$event->getDataProvider()->addError(__('Dashboard Connector not configured'));
return;
}
// Always generate a token
try {
$tokenEvent = new XmdsConnectorTokenEvent();
$tokenEvent->setTargets($event->getDataProvider()->getDisplayId(), $event->getDataProvider()->getWidgetId());
$tokenEvent->setTtl(self::TOKEN_TTL_SECONDS);
$dispatcher->dispatch($tokenEvent, XmdsConnectorTokenEvent::$NAME);
$token = $tokenEvent->getToken();
if (empty($token)) {
$event->getDataProvider()->addError(__('No token returned'));
return;
}
} catch (\Exception $e) {
$this->getLogger()->error('onDataRequest: Failed to get token. e = ' . $e->getMessage());
$event->getDataProvider()->addError(__('No token returned'));
return;
}
// We return a single data item which contains our URL, token and whether we're a preview
$item = [];
$item['url'] = $this->getTokenUrl($token);
$item['token'] = $token;
$item['isPreview'] = $event->getDataProvider()->isPreview();
// We make sure our data cache expires shortly before the token itself expires (so that we have a new token
// generated for it).
$event->getDataProvider()->setCacheTtl(self::TOKEN_TTL_SECONDS - 3600);
// Add our item and set handled
$event->getDataProvider()->addItem($item);
$event->getDataProvider()->setIsHandled();
}
}

View File

@@ -0,0 +1,265 @@
<?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\Connector;
use Carbon\Carbon;
use GuzzleHttp\Client;
use Illuminate\Support\Str;
use Parsedown;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Xibo\Entity\SearchResult;
use Xibo\Event\TemplateProviderEvent;
use Xibo\Event\TemplateProviderImportEvent;
use Xibo\Event\TemplateProviderListEvent;
use Xibo\Support\Sanitizer\SanitizerInterface;
/**
* XiboExchangeConnector
* ---------------------
* This connector will consume the Xibo Layout Exchange API and offer pre-built templates for selection when adding
* a new layout.
*/
class XiboExchangeConnector implements ConnectorInterface
{
use ConnectorTrait;
/**
* @param EventDispatcherInterface $dispatcher
* @return ConnectorInterface
*/
public function registerWithDispatcher(EventDispatcherInterface $dispatcher): ConnectorInterface
{
$dispatcher->addListener('connector.provider.template', [$this, 'onTemplateProvider']);
$dispatcher->addListener('connector.provider.template.import', [$this, 'onTemplateProviderImport']);
$dispatcher->addListener('connector.provider.template.list', [$this, 'onTemplateList']);
return $this;
}
public function getSourceName(): string
{
return 'xibo-exchange';
}
public function getTitle(): string
{
return 'Xibo Exchange';
}
public function getDescription(): string
{
return 'Show Templates provided by the Xibo Exchange in the add new Layout form.';
}
public function getThumbnail(): string
{
return 'theme/default/img/connectors/xibo-exchange.png';
}
public function getSettingsFormTwig(): string
{
return 'connector-form-edit';
}
public function processSettingsForm(SanitizerInterface $params, array $settings): array
{
return $settings;
}
/**
* Get layouts available in Layout exchange and add them to the results
* This is triggered in Template Controller search function
* @param TemplateProviderEvent $event
*/
public function onTemplateProvider(TemplateProviderEvent $event)
{
$this->getLogger()->debug('XiboExchangeConnector: onTemplateProvider');
// Get a cache of the layouts.json file, or request one from download.
$uri = 'https://download.xibosignage.com/layouts_v4_1.json';
$key = md5($uri);
$cache = $this->getPool()->getItem($key);
$body = $cache->get();
if ($cache->isMiss()) {
$this->getLogger()->debug('onTemplateProvider: cache miss, generating.');
// Make the request
$request = $this->getClient()->request('GET', $uri);
$body = $request->getBody()->getContents();
if (empty($body)) {
$this->getLogger()->debug('onTemplateProvider: Empty body');
return;
}
$body = json_decode($body);
if ($body === null || $body === false) {
$this->getLogger()->debug('onTemplateProvider: non-json body or empty body returned.');
return;
}
// Cache for next time
$cache->set($body);
$cache->expiresAt(Carbon::now()->addHours(24));
$this->getPool()->saveDeferred($cache);
} else {
$this->getLogger()->debug('onTemplateProvider: serving from cache.');
}
// We have the whole file locally, so handle paging
$start = $event->getStart();
$perPage = $event->getLength();
// Create a provider to add to each search result
$providerDetails = new ProviderDetails();
$providerDetails->id = $this->getSourceName();
$providerDetails->logoUrl = $this->getThumbnail();
$providerDetails->iconUrl = $this->getThumbnail();
$providerDetails->message = $this->getTitle();
$providerDetails->backgroundColor = '';
// parse the templates based on orientation filter.
if (!empty($event->getOrientation())) {
$templates = [];
foreach ($body as $template) {
if (!empty($template->orientation) &&
Str::contains($template->orientation, $event->getOrientation(), true)
) {
$templates[] = $template;
}
}
} else {
$templates = $body;
}
// Filter the body based on search param.
if (!empty($event->getSearch())) {
$filtered = [];
foreach ($templates as $template) {
if (Str::contains($template->title, $event->getSearch(), true)) {
$filtered[] = $template;
continue;
}
if (!empty($template->description) &&
Str::contains($template->description, $event->getSearch(), true)
) {
$filtered[] = $template;
continue;
}
if (property_exists($template, 'tags') && count($template->tags) > 0) {
if (in_array($event->getSearch(), $template->tags)) {
$filtered[] = $template;
}
}
}
} else {
$filtered = $templates;
}
// sort, featured first, otherwise alphabetically.
usort($filtered, function ($a, $b) {
if (property_exists($a, 'isFeatured') && property_exists($b, 'isFeatured')) {
return $b->isFeatured <=> $a->isFeatured;
} else {
return $a->title <=> $b->title;
}
});
for ($i = $start; $i < ($start + $perPage - 1) && $i < count($filtered); $i++) {
$searchResult = $this->createSearchResult($filtered[$i]);
$searchResult->provider = $providerDetails;
$event->addResult($searchResult);
}
}
/**
* When remote source Template is selected on Layout add,
* we need to get the zip file from specified url and import it to the CMS
* imported Layout object is set on the Event and retrieved later in Layout controller
* @param TemplateProviderImportEvent $event
*/
public function onTemplateProviderImport(TemplateProviderImportEvent $event)
{
$downloadUrl = $event->getDownloadUrl();
$client = new Client();
$tempFile = $event->getLibraryLocation() . 'temp/' . $event->getFileName();
$client->request('GET', $downloadUrl, ['sink' => $tempFile]);
$event->setFilePath($tempFile);
}
/**
* @param $template
* @return SearchResult
*/
private function createSearchResult($template) : SearchResult
{
$searchResult = new SearchResult();
$searchResult->id = $template->fileName;
$searchResult->source = 'remote';
$searchResult->title = $template->title;
$searchResult->description = empty($template->description)
? null
: Parsedown::instance()->setSafeMode(true)->line($template->description);
// Optional data
if (property_exists($template, 'tags') && count($template->tags) > 0) {
$searchResult->tags = $template->tags;
}
if (property_exists($template, 'orientation')) {
$searchResult->orientation = $template->orientation;
}
if (property_exists($template, 'isFeatured')) {
$searchResult->isFeatured = $template->isFeatured;
}
// Thumbnail
$searchResult->thumbnail = $template->thumbnailUrl;
$searchResult->download = $template->downloadUrl;
return $searchResult;
}
/**
* Add this connector to the list of providers.
* @param \Xibo\Event\TemplateProviderListEvent $event
* @return void
*/
public function onTemplateList(TemplateProviderListEvent $event): void
{
$this->getLogger()->debug('onTemplateList:event');
$providerDetails = new ProviderDetails();
$providerDetails->id = $this->getSourceName();
$providerDetails->link = 'https://xibosignage.com';
$providerDetails->logoUrl = $this->getThumbnail();
$providerDetails->iconUrl = 'exchange-alt';
$providerDetails->message = $this->getTitle();
$providerDetails->backgroundColor = '';
$providerDetails->mediaTypes = ['xlf'];
$event->addProvider($providerDetails);
}
}

View File

@@ -0,0 +1,582 @@
<?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\Connector;
use Carbon\Carbon;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Str;
use Psr\Container\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Xibo\Event\MaintenanceRegularEvent;
use Xibo\Event\WidgetEditOptionRequestEvent;
use Xibo\Service\ConfigServiceInterface;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
use Xibo\Support\Sanitizer\SanitizerInterface;
/**
* Xibo SSP Connector
* communicates with the Xibo Ad Exchange to register displays with connected SSPs and manage ad requests
*/
class XiboSspConnector implements ConnectorInterface
{
use ConnectorTrait;
/** @var string */
private $formError;
/** @var array */
private $partners;
/** @var \Xibo\Factory\DisplayFactory */
private $displayFactory;
/**
* @param \Psr\Container\ContainerInterface $container
* @return \Xibo\Connector\ConnectorInterface
*/
public function setFactories(ContainerInterface $container): ConnectorInterface
{
$this->displayFactory = $container->get('displayFactory');
return $this;
}
public function registerWithDispatcher(EventDispatcherInterface $dispatcher): ConnectorInterface
{
$dispatcher->addListener(MaintenanceRegularEvent::$NAME, [$this, 'onRegularMaintenance']);
$dispatcher->addListener(WidgetEditOptionRequestEvent::$NAME, [$this, 'onWidgetEditOption']);
return $this;
}
public function getSourceName(): string
{
return 'xibo-ssp-connector';
}
public function getTitle(): string
{
return 'Xibo SSP Connector';
}
public function getDescription(): string
{
return 'Connect to world leading Supply Side Platforms (SSPs) and monetise your network.';
}
public function getThumbnail(): string
{
return 'theme/default/img/connectors/xibo-ssp.png';
}
public function getSettingsFormTwig(): string
{
return 'xibo-ssp-connector-form-settings';
}
public function getSettingsFormJavaScript(): string
{
return 'xibo-ssp-connector-form-javascript';
}
public function getFormError(): string
{
return $this->formError ?? __('Unknown error');
}
public function processSettingsForm(SanitizerInterface $params, array $settings): array
{
$existingApiKey = $this->getSetting('apiKey');
if (!$this->isProviderSetting('apiKey')) {
$settings['apiKey'] = $params->getString('apiKey');
}
$existingCmsUrl = $this->getSetting('cmsUrl');
if (!$this->isProviderSetting('cmsUrl')) {
$settings['cmsUrl'] = trim($params->getString('cmsUrl'), '/');
if (empty($settings['cmsUrl']) || !Str::startsWith($settings['cmsUrl'], 'http')) {
throw new InvalidArgumentException(
__('Please enter a CMS URL, including http(s)://'),
'cmsUrl'
);
}
}
// If our API key was empty, then do not set partners.
if (empty($existingApiKey) || empty($settings['apiKey'])) {
return $settings;
}
// Set partners.
$partners = [];
$available = $this->getAvailablePartners(true, $settings['apiKey']);
// Pull in expected fields.
foreach ($available as $partnerId => $partner) {
$partners[] = [
'name' => $partnerId,
'enabled' => $params->getCheckbox($partnerId . '_enabled'),
'isTest' => $params->getCheckbox($partnerId . '_isTest'),
'isUseWidget' => $params->getCheckbox($partnerId . '_isUseWidget'),
'currency' => $params->getString($partnerId . '_currency'),
'key' => $params->getString($partnerId . '_key'),
'sov' => $params->getInt($partnerId . '_sov'),
'mediaTypesAllowed' => $params->getString($partnerId . '_mediaTypesAllowed'),
'duration' => $params->getInt($partnerId . '_duration'),
'minDuration' => $params->getInt($partnerId . '_minDuration'),
'maxDuration' => $params->getInt($partnerId . '_maxDuration'),
];
// Also grab the displayGroupId if one has been set.
$displayGroupId = $params->getInt($partnerId . '_displayGroupId');
if (empty($displayGroupId)) {
unset($settings[$partnerId . '_displayGroupId']);
} else {
$settings[$partnerId . '_displayGroupId'] = $displayGroupId;
}
$settings[$partnerId . '_sspIdField'] = $params->getString($partnerId . '_sspIdField');
}
// Update API config.
$this->setPartners($settings['apiKey'], $partners);
try {
// If the API key has changed during this request, clear out displays on the old API key
if ($existingApiKey !== $settings['apiKey']) {
// Clear all displays for this CMS on the existing key
$this->setDisplays($existingApiKey, $existingCmsUrl, [], $settings);
} else if (!empty($existingCmsUrl) && $existingCmsUrl !== $settings['cmsUrl']) {
// Clear all displays for this CMS on the existing key
$this->setDisplays($settings['apiKey'], $existingCmsUrl, [], $settings);
}
} catch (\Exception $e) {
$this->getLogger()->error('Failed to set displays '. $e->getMessage());
}
// Add displays on the new API key (maintenance also does this, but do it now).
$this->setDisplays($settings['apiKey'], $settings['cmsUrl'], $partners, $settings);
return $settings;
}
/**
* @throws InvalidArgumentException
* @throws GeneralException
*/
public function getAvailablePartners(bool $isThrowError = false, ?string $withApiKey = null)
{
if ($this->partners === null) {
// Make a call to the API to see what we've currently got configured and what is available.
if ($withApiKey) {
$apiKey = $withApiKey;
} else {
$apiKey = $this->getSetting('apiKey');
if (empty($apiKey)) {
return [];
}
}
$this->getLogger()->debug('getAvailablePartners: Requesting available services.');
try {
$response = $this->getClient()->get($this->getServiceUrl() . '/configure', [
'headers' => [
'X-API-KEY' => $apiKey
]
]);
$body = $response->getBody()->getContents();
$this->getLogger()->debug('getAvailablePartners: ' . $body);
$json = json_decode($body, true);
if (empty($json)) {
$this->formError = __('Empty response from the dashboard service');
throw new InvalidArgumentException($this->formError);
}
$this->partners = $json;
} catch (RequestException $e) {
$this->getLogger()->error('getAvailablePartners: e = ' . $e->getMessage());
if ($e->getResponse()->getStatusCode() === 401) {
$this->formError = __('API key not valid');
if ($isThrowError) {
throw new InvalidArgumentException($this->formError, 'apiKey');
} else {
return null;
}
}
$message = json_decode($e->getResponse()->getBody()->getContents(), true);
$this->formError = empty($message)
? __('Cannot contact SSP service, please try again shortly.')
: $message['message'];
if ($isThrowError) {
throw new GeneralException($this->formError);
} else {
return null;
}
} catch (\Exception $e) {
$this->getLogger()->error('getAvailableServices: e = ' . $e->getMessage());
$this->formError = __('Cannot contact SSP service, please try again shortly.');
if ($isThrowError) {
throw new GeneralException($this->formError);
} else {
return null;
}
}
}
return $this->partners['available'] ?? [];
}
/**
* Get the number of displays that are authorised by this API key.
* @return int
*/
public function getAuthorisedDisplayCount(): int
{
return intval($this->partners['displays'] ?? 0);
}
/**
* Get a setting for a partner
* @param string $partnerKey
* @param string $setting
* @param $default
* @return mixed|string|null
*/
public function getPartnerSetting(string $partnerKey, string $setting, $default = null)
{
if (!is_array($this->partners) || !array_key_exists('partners', $this->partners)) {
return $default;
}
foreach ($this->partners['partners'] as $partner) {
if ($partner['name'] === $partnerKey) {
return $partner[$setting] ?? $default;
}
}
return $default;
}
/**
* @throws InvalidArgumentException
* @throws GeneralException
*/
private function setPartners(string $apiKey, array $partners)
{
$this->getLogger()->debug('setPartners: updating');
$this->getLogger()->debug(json_encode($partners));
try {
$this->getClient()->post($this->getServiceUrl() . '/configure', [
'headers' => [
'X-API-KEY' => $apiKey
],
'json' => [
'partners' => $partners
]
]);
} catch (RequestException $e) {
$this->getLogger()->error('setPartners: e = ' . $e->getMessage());
$message = json_decode($e->getResponse()->getBody()->getContents(), true);
throw new GeneralException(empty($message)
? __('Cannot contact SSP service, please try again shortly.')
: $message['message']);
} catch (\Exception $e) {
$this->getLogger()->error('setPartners: e = ' . $e->getMessage());
throw new GeneralException(__('Cannot contact SSP service, please try again shortly.'));
}
}
/**
* @throws \Xibo\Support\Exception\NotFoundException
* @throws \Xibo\Support\Exception\GeneralException
*/
private function setDisplays(string $apiKey, string $cmsUrl, array $partners, array $settings)
{
$displays = [];
foreach ($partners as $partner) {
// If this partner is enabled?
if (!$partner['enabled']) {
continue;
}
// Get displays for this partner
$partnerKey = $partner['name'];
$sspIdField = $settings[$partnerKey . '_sspIdField'] ?? 'displayId';
foreach ($this->displayFactory->query(null, [
'disableUserCheck' => 1,
'displayGroupId' => $settings[$partnerKey . '_displayGroupId'] ?? null,
'authorised' => 1,
]) as $display) {
if (!array_key_exists($display->displayId, $displays)) {
$resolution = explode('x', $display->resolution ?? '');
$displays[$display->displayId] = [
'displayId' => $display->displayId,
'hardwareKey' => $display->license,
'width' => trim($resolution[0] ?? 1920),
'height' => trim($resolution[1] ?? 1080),
'partners' => [],
];
}
switch ($sspIdField) {
case 'customId':
$sspId = $display->customId;
break;
case 'ref1':
$sspId = $display->ref1;
break;
case 'ref2':
$sspId = $display->ref2;
break;
case 'ref3':
$sspId = $display->ref3;
break;
case 'ref4':
$sspId = $display->ref4;
break;
case 'ref5':
$sspId = $display->ref5;
break;
case 'displayId':
default:
$sspId = $display->displayId;
}
$displays[$display->displayId]['partners'][] = [
'name' => $partnerKey,
'sspId' => '' . $sspId,
];
}
}
try {
$this->getClient()->post($this->getServiceUrl() . '/displays', [
'headers' => [
'X-API-KEY' => $apiKey,
],
'json' => [
'cmsUrl' => $cmsUrl,
'displays' => array_values($displays),
],
]);
} catch (RequestException $e) {
$this->getLogger()->error('setDisplays: e = ' . $e->getMessage());
$message = json_decode($e->getResponse()->getBody()->getContents(), true);
throw new GeneralException(empty($message)
? __('Cannot contact SSP service, please try again shortly.')
: $message['message']);
} catch (\Exception $e) {
$this->getLogger()->error('setDisplays: e = ' . $e->getMessage());
throw new GeneralException(__('Cannot contact SSP service, please try again shortly.'));
}
}
/**
* Get the service url, either from settings or a default
* @return string
*/
private function getServiceUrl(): string
{
return $this->getSetting('serviceUrl', 'https://exchange.xibo-adspace.com/api');
}
// <editor-fold desc="Proxy methods">
/**
* Activity data
*/
public function activity(SanitizerInterface $params): array
{
$fromDt = $params->getDate('activityFromDt', [
'default' => Carbon::now()->startOfHour()
]);
$toDt = $params->getDate('activityToDt', [
'default' => $fromDt->addHour()
]);
if ($params->getInt('displayId') == null) {
throw new GeneralException(__('Display ID is required'));
}
// Call the api (override the timeout)
try {
$response = $this->getClient()->get($this->getServiceUrl() . '/activity', [
'timeout' => 120,
'headers' => [
'X-API-KEY' => $this->getSetting('apiKey'),
],
'query' => [
'cmsUrl' => $this->getSetting('cmsUrl'),
'fromDt' => $fromDt->toAtomString(),
'toDt' => $toDt->toAtomString(),
'displayId' => $params->getInt('displayId'),
'campaignId' => $params->getString('partnerId'),
],
]);
$body = json_decode($response->getBody()->getContents(), true);
if (!$body) {
throw new GeneralException(__('No response'));
}
return $body;
} catch (\Exception $e) {
$this->getLogger()->error('activity: e = ' . $e->getMessage());
}
return [
'data' => [],
'recordsTotal' => 0,
];
}
/**
* Available Partners
*/
public function getAvailablePartnersFilter(SanitizerInterface $params): array
{
try {
return $this->getAvailablePartners() ?? [];
} catch (\Exception $e) {
$this->getLogger()->error('activity: e = ' . $e->getMessage());
}
return [
'data' => [],
'recordsTotal' => 0,
];
}
// </editor-fold>
// <editor-fold desc="Listeners">
public function onRegularMaintenance(MaintenanceRegularEvent $event)
{
$this->getLogger()->debug('onRegularMaintenance');
try {
$this->getAvailablePartners();
$partners = $this->partners['partners'] ?? [];
if (count($partners) > 0) {
$this->setDisplays(
$this->getSetting('apiKey'),
$this->getSetting('cmsUrl'),
$partners,
$this->settings
);
}
$event->addMessage('SSP: done');
} catch (\Exception $exception) {
$this->getLogger()->error('SSP connector: ' . $exception->getMessage());
$event->addMessage('Error processing SSP configuration.');
}
}
/**
* Connector is being deleted
* @param \Xibo\Service\ConfigServiceInterface $configService
* @return void
*/
public function delete(ConfigServiceInterface $configService): void
{
$this->getLogger()->debug('delete');
$configService->changeSetting('isAdspaceEnabled', 0);
}
/**
* Connector is being enabled
* @param \Xibo\Service\ConfigServiceInterface $configService
* @return void
*/
public function enable(ConfigServiceInterface $configService): void
{
$this->getLogger()->debug('enable');
$configService->changeSetting('isAdspaceEnabled', 1);
}
/**
* Connector is being disabled
* @param \Xibo\Service\ConfigServiceInterface $configService
* @return void
*/
public function disable(ConfigServiceInterface $configService): void
{
$this->getLogger()->debug('disable');
$configService->changeSetting('isAdspaceEnabled', 0);
}
public function onWidgetEditOption(WidgetEditOptionRequestEvent $event)
{
$this->getLogger()->debug('onWidgetEditOption');
// Pull the widget we're working with.
$widget = $event->getWidget();
if ($widget === null) {
throw new NotFoundException();
}
// We handle the dashboard widget and the property with id="type"
if ($widget->type === 'ssp' && $event->getPropertyId() === 'partnerId') {
// Pull in existing information
$partnerFilter = $event->getPropertyValue();
$options = $event->getOptions();
foreach ($this->getAvailablePartners() as $partnerId => $partner) {
if ((empty($partnerFilter) || $partnerId === $partnerFilter)
&& $this->getPartnerSetting($partnerId, 'enabled') == 1
) {
$options[] = [
'id' => $partnerId,
'type' => $partnerId,
'name' => $partner['name'],
];
}
}
$event->setOptions($options);
}
}
// </editor-fold>
}

607
lib/Controller/Action.php Normal file
View File

@@ -0,0 +1,607 @@
<?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\Controller;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Factory\ActionFactory;
use Xibo\Factory\LayoutFactory;
use Xibo\Factory\ModuleFactory;
use Xibo\Factory\RegionFactory;
use Xibo\Factory\WidgetFactory;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
/**
* Class Action
* @package Xibo\Controller
*/
class Action extends Base
{
/**
* @var ActionFactory
*/
private $actionFactory;
/** @var LayoutFactory */
private $layoutFactory;
/** @var RegionFactory */
private $regionFactory;
/** @var WidgetFactory */
private $widgetFactory;
/** @var ModuleFactory */
private $moduleFactory;
/**
* Set common dependencies.
* @param ActionFactory $actionFactory
* @param LayoutFactory $layoutFactory
* @param RegionFactory $regionFactory
* @param WidgetFactory $widgetFactory
* @param ModuleFactory $moduleFactory
*/
public function __construct($actionFactory, $layoutFactory, $regionFactory, $widgetFactory, $moduleFactory)
{
$this->actionFactory = $actionFactory;
$this->layoutFactory = $layoutFactory;
$this->regionFactory = $regionFactory;
$this->widgetFactory = $widgetFactory;
$this->moduleFactory = $moduleFactory;
}
/**
* Returns a Grid of Actions
*
* @SWG\Get(
* path="/action",
* operationId="actionSearch",
* tags={"action"},
* summary="Search Actions",
* description="Search all Actions this user has access to",
* @SWG\Parameter(
* name="actionId",
* in="query",
* description="Filter by Action Id",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="ownerId",
* in="query",
* description="Filter by Owner Id",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="triggerType",
* in="query",
* description="Filter by Action trigger type",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="triggerCode",
* in="query",
* description="Filter by Action trigger code",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="actionType",
* in="query",
* description="Filter by Action type",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="source",
* in="query",
* description="Filter by Action source",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="sourceId",
* in="query",
* description="Filter by Action source Id",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="target",
* in="query",
* description="Filter by Action target",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="targetId",
* in="query",
* description="Filter by Action target Id",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="layoutId",
* in="query",
* description="Return all actions pertaining to a particular Layout",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="sourceOrTargetId",
* in="query",
* description="Return all actions related to a source or target with the provided ID",
* type="integer",
* required=false
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="#/definitions/Action")
* )
* )
* )
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
*/
public function grid(Request $request, Response $response) : Response
{
$parsedParams = $this->getSanitizer($request->getQueryParams());
$filter = [
'actionId' => $parsedParams->getInt('actionId'),
'ownerId' => $parsedParams->getInt('ownerId'),
'triggerType' => $parsedParams->getString('triggerType'),
'triggerCode' => $parsedParams->getString('triggerCode'),
'actionType' => $parsedParams->getString('actionType'),
'source' => $parsedParams->getString('source'),
'sourceId' => $parsedParams->getInt('sourceId'),
'target' => $parsedParams->getString('target'),
'targetId' => $parsedParams->getInt('targetId'),
'widgetId' => $parsedParams->getInt('widgetId'),
'layoutCode' => $parsedParams->getString('layoutCode'),
'layoutId' => $parsedParams->getInt('layoutId'),
'sourceOrTargetId' => $parsedParams->getInt('sourceOrTargetId'),
];
$actions = $this->actionFactory->query(
$this->gridRenderSort($parsedParams),
$this->gridRenderFilter($filter, $parsedParams)
);
foreach ($actions as $action) {
$action->setUnmatchedProperty('widgetName', null);
$action->setUnmatchedProperty('regionName', null);
if ($action->actionType === 'navWidget' && $action->widgetId != null) {
try {
$widget = $this->widgetFactory->loadByWidgetId($action->widgetId);
$module = $this->moduleFactory->getByType($widget->type);
// dynamic field to display in the grid instead of widgetId
$action->setUnmatchedProperty('widgetName', $widget->getOptionValue('name', $module->name));
} catch (NotFoundException $e) {
// Widget not found, leave widgetName as null
}
}
if ($action->target === 'region' && $action->targetId != null) {
try {
$region = $this->regionFactory->getById($action->targetId);
// dynamic field to display in the grid instead of regionId
$action->setUnmatchedProperty('regionName', $region->name);
} catch (NotFoundException $e) {
// Region not found, leave regionName as null
}
}
}
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = $this->actionFactory->countLast();
$this->getState()->setData($actions);
return $this->render($request, $response);
}
/**
* Add a new Action
*
* @SWG\Post(
* path="/action",
* operationId="actionAdd",
* tags={"action"},
* summary="Add Action",
* description="Add a new Action",
* @SWG\Parameter(
* name="layoutId",
* in="formData",
* description="LayoutId associted with this Action",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="actionType",
* in="formData",
* description="Action type, next, previous, navLayout, navWidget",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="target",
* in="formData",
* description="Target for this action, screen or region",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="targetId",
* in="formData",
* description="The id of the target for this action - regionId if the target is set to region",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="source",
* in="formData",
* description="Source for this action layout, region or widget",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="sourceId",
* in="formData",
* description="The id of the source object, layoutId, regionId or widgetId",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="triggerType",
* in="formData",
* description="Action trigger type, touch or webhook",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="triggerCode",
* in="formData",
* description="Action trigger code",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="widgetId",
* in="formData",
* description="For navWidget actionType, the WidgetId to navigate to",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="layoutCode",
* in="formData",
* description="For navLayout, the Layout Code identifier to navigate to",
* type="string",
* required=false
* ),
* @SWG\Response(
* response=201,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Action"),
* @SWG\Header(
* header="Location",
* description="Location of the new record",
* type="string"
* )
* )
* )
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
*/
public function add(Request $request, Response $response) : Response
{
$sanitizedParams = $this->getSanitizer($request->getParams());
$triggerType = $sanitizedParams->getString('triggerType');
$triggerCode = $sanitizedParams->getString('triggerCode', ['defaultOnEmptyString' => true]);
$actionType = $sanitizedParams->getString('actionType');
$target = $sanitizedParams->getString('target');
$targetId = $sanitizedParams->getInt('targetId');
$widgetId = $sanitizedParams->getInt('widgetId');
$layoutCode = $sanitizedParams->getString('layoutCode');
$layoutId = $sanitizedParams->getInt('layoutId');
$source = $sanitizedParams->getString('source');
$sourceId = $sanitizedParams->getInt('sourceId');
if ($layoutId === null) {
throw new InvalidArgumentException(__('Please provide LayoutId'), 'layoutId');
}
$layout = $this->layoutFactory->getById($layoutId);
// Make sure the Layout is checked out to begin with
if (!$layout->isEditable()) {
throw new InvalidArgumentException(__('Layout is not checked out'), 'statusId');
}
// restrict to one touch Action per source
if (
(!empty($source) && $sourceId !== null && !empty($triggerType))
&& $this->actionFactory->checkIfActionExist($source, $sourceId, $triggerType)
) {
throw new InvalidArgumentException(__('Action with specified Trigger Type already exists'), 'triggerType');
}
$action = $this->actionFactory->create(
$triggerType,
$triggerCode,
$actionType,
$source,
$sourceId,
$target,
$targetId,
$widgetId,
$layoutCode,
$layoutId
);
$action->save(['notifyLayout' => true]);
// Return
$this->getState()->hydrate([
'message' => __('Added Action'),
'httpStatus' => 201,
'id' => $action->actionId,
'data' => $action,
]);
return $this->render($request, $response);
}
/**
* Edit Action
*
* @SWG\PUT(
* path="/action/{actionId}",
* operationId="actionAdd",
* tags={"action"},
* summary="Add Action",
* description="Add a new Action",
* @SWG\Parameter(
* name="actionId",
* in="path",
* description="Action ID to edit",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="layoutId",
* in="formData",
* description="LayoutId associted with this Action",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="actionType",
* in="formData",
* description="Action type, next, previous, navLayout, navWidget",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="target",
* in="formData",
* description="Target for this action, screen or region",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="targetId",
* in="formData",
* description="The id of the target for this action - regionId if the target is set to region",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="source",
* in="formData",
* description="Source for this action layout, region or widget",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="sourceId",
* in="formData",
* description="The id of the source object, layoutId, regionId or widgetId",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="triggerType",
* in="formData",
* description="Action trigger type, touch or webhook",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="triggerCode",
* in="formData",
* description="Action trigger code",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="widgetId",
* in="formData",
* description="For navWidget actionType, the WidgetId to navigate to",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="layoutCode",
* in="formData",
* description="For navLayout, the Layout Code identifier to navigate to",
* type="string",
* required=false
* ),
* @SWG\Response(
* response=201,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Action"),
* @SWG\Header(
* header="Location",
* description="Location of the new record",
* type="string"
* )
* )
* )
*
* @param Request $request
* @param Response $response
* @param int $id
* @return Response
* @throws GeneralException
*/
public function edit(Request $request, Response $response, int $id) : Response
{
$action = $this->actionFactory->getById($id);
$sanitizedParams = $this->getSanitizer($request->getParams());
$layout = $this->layoutFactory->getById($action->layoutId);
// Make sure the Layout is checked out to begin with
if (!$layout->isEditable()) {
throw new InvalidArgumentException(__('Layout is not checked out'), 'statusId');
}
$action->source = $sanitizedParams->getString('source');
$action->sourceId = $sanitizedParams->getInt('sourceId');
$action->triggerType = $sanitizedParams->getString('triggerType');
$action->triggerCode = $sanitizedParams->getString('triggerCode', ['defaultOnEmptyString' => true]);
$action->actionType = $sanitizedParams->getString('actionType');
$action->target = $sanitizedParams->getString('target');
$action->targetId = $sanitizedParams->getInt('targetId');
$action->widgetId = $sanitizedParams->getInt('widgetId');
$action->layoutCode = $sanitizedParams->getString('layoutCode');
$action->validate();
// restrict to one touch Action per source
if ($this->actionFactory->checkIfActionExist($action->source, $action->sourceId, $action->triggerType, $action->actionId)) {
throw new InvalidArgumentException(__('Action with specified Trigger Type already exists'), 'triggerType');
}
$action->save(['notifyLayout' => true, 'validate' => false]);
// Return
$this->getState()->hydrate([
'message' => __('Edited Action'),
'id' => $action->actionId,
'data' => $action
]);
return $this->render($request, $response);
}
/**
* Delete Action
* @param Request $request
* @param Response $response
* @param int $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
*
* @SWG\Delete(
* path="/action/{actionId}",
* operationId="actionDelete",
* tags={"action"},
* summary="Delete Action",
* description="Delete an existing Action",
* @SWG\Parameter(
* name="actionId",
* in="path",
* description="The Action ID to Delete",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*/
public function delete(Request $request, Response $response, int $id) : Response
{
$action = $this->actionFactory->getById($id);
$layout = $this->layoutFactory->getById($action->layoutId);
// Make sure the Layout is checked out to begin with
if (!$layout->isEditable()) {
throw new InvalidArgumentException(__('Layout is not checked out'), 'statusId');
}
$action->notifyLayout($layout->layoutId);
$action->delete();
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Deleted Action'))
]);
return $this->render($request, $response);
}
/**
* @param string $source
* @param int $sourceId
* @return \Xibo\Entity\Layout|\Xibo\Entity\Region|\Xibo\Entity\Widget
* @throws InvalidArgumentException
* @throws NotFoundException
*/
public function checkIfSourceExists(string $source, int $sourceId)
{
if (strtolower($source) === 'layout') {
$object = $this->layoutFactory->getById($sourceId);
} elseif (strtolower($source) === 'region') {
$object = $this->regionFactory->getById($sourceId);
} elseif (strtolower($source) === 'widget') {
$object = $this->widgetFactory->getById($sourceId);
} else {
throw new InvalidArgumentException(__('Provided source is invalid. ') , 'source');
}
return $object;
}
}

View File

@@ -0,0 +1,652 @@
<?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\Controller;
use Carbon\Carbon;
use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Grant\AuthCodeGrant;
use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Stash\Interfaces\PoolInterface;
use Xibo\Entity\ApplicationScope;
use Xibo\Factory\ApplicationFactory;
use Xibo\Factory\ApplicationRedirectUriFactory;
use Xibo\Factory\ApplicationScopeFactory;
use Xibo\Factory\ConnectorFactory;
use Xibo\Factory\UserFactory;
use Xibo\Helper\DateFormatHelper;
use Xibo\Helper\Session;
use Xibo\OAuth\AuthCodeRepository;
use Xibo\OAuth\RefreshTokenRepository;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\ControllerNotImplemented;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
/**
* Class Applications
* @package Xibo\Controller
*/
class Applications extends Base
{
/**
* @var Session
*/
private $session;
/**
* @var ApplicationFactory
*/
private $applicationFactory;
/**
* @var ApplicationRedirectUriFactory
*/
private $applicationRedirectUriFactory;
/** @var ApplicationScopeFactory */
private $applicationScopeFactory;
/** @var UserFactory */
private $userFactory;
/** @var PoolInterface */
private $pool;
/** @var \Xibo\Factory\ConnectorFactory */
private $connectorFactory;
/**
* Set common dependencies.
* @param Session $session
* @param ApplicationFactory $applicationFactory
* @param ApplicationRedirectUriFactory $applicationRedirectUriFactory
* @param $applicationScopeFactory
* @param UserFactory $userFactory
* @param $pool
* @param \Xibo\Factory\ConnectorFactory $connectorFactory
*/
public function __construct(
$session,
$applicationFactory,
$applicationRedirectUriFactory,
$applicationScopeFactory,
$userFactory,
$pool,
ConnectorFactory $connectorFactory
) {
$this->session = $session;
$this->applicationFactory = $applicationFactory;
$this->applicationRedirectUriFactory = $applicationRedirectUriFactory;
$this->applicationScopeFactory = $applicationScopeFactory;
$this->userFactory = $userFactory;
$this->pool = $pool;
$this->connectorFactory = $connectorFactory;
}
/**
* Display Page
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function displayPage(Request $request, Response $response)
{
// Load all connectors and output any javascript.
$connectorJavaScript = [];
foreach ($this->connectorFactory->query(['isVisible' => 1]) as $connector) {
try {
// Create a connector, add in platform settings and register it with the dispatcher.
$connectorObject = $this->connectorFactory->create($connector);
$settingsFormJavaScript = $connectorObject->getSettingsFormJavaScript();
if (!empty($settingsFormJavaScript)) {
$connectorJavaScript[] = $settingsFormJavaScript;
}
} catch (\Exception $exception) {
// Log and ignore.
$this->getLog()->error('Incorrectly configured connector. e=' . $exception->getMessage());
}
}
$this->getState()->template = 'applications-page';
$this->getState()->setData([
'connectorJavaScript' => $connectorJavaScript,
]);
return $this->render($request, $response);
}
/**
* Display page grid
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function grid(Request $request, Response $response)
{
$this->getState()->template = 'grid';
$sanitizedParams = $this->getSanitizer($request->getParams());
$applications = $this->applicationFactory->query(
$this->gridRenderSort($sanitizedParams),
$this->gridRenderFilter(
['name' => $sanitizedParams->getString('name')],
$sanitizedParams
)
);
foreach ($applications as $application) {
if ($this->isApi($request)) {
throw new AccessDeniedException();
}
// Include the buttons property
$application->includeProperty('buttons');
// Add an Edit button (edit form also exposes the secret - not possible to get through the API)
$application->buttons = [];
if ($application->userId == $this->getUser()->userId || $this->getUser()->getUserTypeId() == 1) {
// Edit
$application->buttons[] = [
'id' => 'application_edit_button',
'url' => $this->urlFor($request, 'application.edit.form', ['id' => $application->key]),
'text' => __('Edit')
];
// Delete
$application->buttons[] = [
'id' => 'application_delete_button',
'url' => $this->urlFor($request, 'application.delete.form', ['id' => $application->key]),
'text' => __('Delete')
];
}
}
$this->getState()->setData($applications);
$this->getState()->recordsTotal = $this->applicationFactory->countLast();
return $this->render($request, $response);
}
/**
* Display the Authorize form.
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws InvalidArgumentException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function authorizeRequest(Request $request, Response $response)
{
// Pull authorize params from our session
/** @var AuthorizationRequest $authParams */
$authParams = $this->session->get('authParams');
if (!$authParams) {
throw new InvalidArgumentException(__('Authorisation Parameters missing from session.'), 'authParams');
}
if ($this->applicationFactory->checkAuthorised($authParams->getClient()->getIdentifier(), $this->getUser()->userId)) {
return $this->authorize($request->withParsedBody(['authorization' => 'Approve']), $response);
}
$client = $this->applicationFactory->getClientEntity($authParams->getClient()->getIdentifier())->load();
// Process any scopes.
$scopes = [];
$authScopes = $authParams->getScopes();
// if we have scopes in the request, make sure we only add the valid ones.
// the default scope is all, if it's not set on the Application, $scopes will still be empty here.
if ($authScopes !== null) {
$validScopes = $this->applicationScopeFactory->finalizeScopes(
$authScopes,
$authParams->getGrantTypeId(),
$client
);
// get all the valid scopes by their ID, we need to do this to present more details on the authorize form.
foreach ($validScopes as $scope) {
$scopes[] = $this->applicationScopeFactory->getById($scope->getIdentifier());
}
if (count($scopes) <= 0) {
throw new InvalidArgumentException(
__('This application has not requested access to anything.'),
'authParams'
);
}
// update scopes in auth request in session to scopes we actually present for approval
$authParams->setScopes($validScopes);
}
// Reasert the auth params.
$this->session->set('authParams', $authParams);
// Get, show page
$this->getState()->template = 'applications-authorize-page';
$this->getState()->setData([
'forceHide' => true,
'authParams' => $authParams,
'scopes' => $scopes,
'application' => $client
]);
return $this->render($request, $response);
}
/**
* Authorize an oAuth request
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Exception
*/
public function authorize(Request $request, Response $response)
{
// Pull authorize params from our session
/** @var AuthorizationRequest $authRequest */
$authRequest = $this->session->get('authParams');
if (!$authRequest) {
throw new InvalidArgumentException(__('Authorisation Parameters missing from session.'), 'authParams');
}
$sanitizedQueryParams = $this->getSanitizer($request->getParams());
$apiKeyPaths = $this->getConfig()->getApiKeyDetails();
$privateKey = $apiKeyPaths['privateKeyPath'];
$encryptionKey = $apiKeyPaths['encryptionKey'];
$server = new AuthorizationServer(
$this->applicationFactory,
new \Xibo\OAuth\AccessTokenRepository($this->getLog(), $this->pool, $this->applicationFactory),
$this->applicationScopeFactory,
$privateKey,
$encryptionKey
);
$server->enableGrantType(
new AuthCodeGrant(
new AuthCodeRepository(),
new RefreshTokenRepository($this->getLog(), $this->pool),
new \DateInterval('PT10M')
),
new \DateInterval('PT1H')
);
// get oauth User Entity and set the UserId to the current web userId
$authRequest->setUser($this->getUser());
// We are authorized
if ($sanitizedQueryParams->getString('authorization') === 'Approve') {
$authRequest->setAuthorizationApproved(true);
$this->applicationFactory->setApplicationApproved(
$authRequest->getClient()->getIdentifier(),
$authRequest->getUser()->getIdentifier(),
Carbon::now()->format(DateFormatHelper::getSystemFormat()),
$request->getAttribute('ip_address')
);
$this->getLog()->audit(
'Auth',
0,
'Application access approved',
[
'Application identifier ends with' => substr($authRequest->getClient()->getIdentifier(), -8),
'Application Name' => $authRequest->getClient()->getName()
]
);
} else {
$authRequest->setAuthorizationApproved(false);
}
// Redirect back to the specified redirect url
try {
return $server->completeAuthorizationRequest($authRequest, $response);
} catch (OAuthServerException $exception) {
if ($exception->hasRedirect()) {
return $response->withRedirect($exception->getRedirectUri());
} else {
throw $exception;
}
}
}
/**
* Form to register a new application.
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function addForm(Request $request, Response $response)
{
$this->getState()->template = 'applications-form-add';
return $this->render($request, $response);
}
/**
* Edit Application
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function editForm(Request $request, Response $response, $id)
{
// Get the client
$client = $this->applicationFactory->getById($id);
if ($client->userId != $this->getUser()->userId && $this->getUser()->getUserTypeId() != 1) {
throw new AccessDeniedException();
}
// Load this clients details.
$client->load();
$scopes = $this->applicationScopeFactory->query();
foreach ($scopes as $scope) {
/** @var ApplicationScope $scope */
$found = false;
foreach ($client->scopes as $checked) {
if ($checked->id == $scope->id) {
$found = true;
break;
}
}
$scope->setUnmatchedProperty('selected', $found ? 1 : 0);
}
// Render the view
$this->getState()->template = 'applications-form-edit';
$this->getState()->setData([
'client' => $client,
'scopes' => $scopes,
]);
return $this->render($request, $response);
}
/**
* Delete Application Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function deleteForm(Request $request, Response $response, $id)
{
// Get the client
$client = $this->applicationFactory->getById($id);
if ($client->userId != $this->getUser()->userId && $this->getUser()->getUserTypeId() != 1) {
throw new AccessDeniedException();
}
$this->getState()->template = 'applications-form-delete';
$this->getState()->setData([
'client' => $client,
]);
return $this->render($request, $response);
}
/**
* Register a new application with OAuth
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws InvalidArgumentException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function add(Request $request, Response $response)
{
$sanitizedParams = $this->getSanitizer($request->getParams());
$application = $this->applicationFactory->create();
$application->name = $sanitizedParams->getString('name');
if ($application->name == '') {
throw new InvalidArgumentException(__('Please enter Application name'), 'name');
}
$application->userId = $this->getUser()->userId;
$application->save();
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Added %s'), $application->name),
'data' => $application,
'id' => $application->key
]);
return $this->render($request, $response);
}
/**
* Edit Application
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws InvalidArgumentException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function edit(Request $request, Response $response, $id)
{
$this->getLog()->debug('Editing ' . $id);
// Get the client
$client = $this->applicationFactory->getById($id);
if ($client->userId != $this->getUser()->userId && $this->getUser()->getUserTypeId() != 1) {
throw new AccessDeniedException();
}
$sanitizedParams = $this->getSanitizer($request->getParams());
$client->name = $sanitizedParams->getString('name');
$client->authCode = $sanitizedParams->getCheckbox('authCode');
$client->clientCredentials = $sanitizedParams->getCheckbox('clientCredentials');
$client->isConfidential = $sanitizedParams->getCheckbox('isConfidential');
if ($sanitizedParams->getCheckbox('resetKeys') == 1) {
$client->resetSecret();
$this->pool->getItem('C_' . $client->key)->clear();
}
if ($client->authCode === 1) {
$client->description = $sanitizedParams->getString('description');
$client->logo = $sanitizedParams->getString('logo');
$client->coverImage = $sanitizedParams->getString('coverImage');
$client->companyName = $sanitizedParams->getString('companyName');
$client->termsUrl = $sanitizedParams->getString('termsUrl');
$client->privacyUrl = $sanitizedParams->getString('privacyUrl');
}
// Delete all the redirect urls and add them again
$client->load();
foreach ($client->redirectUris as $uri) {
$uri->delete();
}
$client->redirectUris = [];
// Do we have a redirect?
$redirectUris = $sanitizedParams->getArray('redirectUri');
foreach ($redirectUris as $redirectUri) {
if ($redirectUri == '') {
continue;
}
$redirect = $this->applicationRedirectUriFactory->create();
$redirect->redirectUri = $redirectUri;
$client->assignRedirectUri($redirect);
}
// clear scopes
$client->scopes = [];
// API Scopes
foreach ($this->applicationScopeFactory->query() as $scope) {
/** @var ApplicationScope $scope */
// See if this has been checked this time
$checked = $sanitizedParams->getCheckbox('scope_' . $scope->id);
// Assign scopes
if ($checked) {
$client->assignScope($scope);
}
}
// Change the ownership?
if ($sanitizedParams->getInt('userId') !== null) {
// Check we have permissions to view this user
$user = $this->userFactory->getById($sanitizedParams->getInt('userId'));
$this->getLog()->debug('Attempting to change ownership to ' . $user->userId . ' - ' . $user->userName);
if (!$this->getUser()->checkViewable($user)) {
throw new InvalidArgumentException(__('You do not have permission to assign this user'), 'userId');
}
$client->userId = $user->userId;
}
$client->save();
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Edited %s'), $client->name),
'data' => $client,
'id' => $client->key
]);
return $this->render($request, $response);
}
/**
* Delete application
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function delete(Request $request, Response $response, $id)
{
// Get the client
$client = $this->applicationFactory->getById($id);
if ($client->userId != $this->getUser()->userId && $this->getUser()->getUserTypeId() != 1) {
throw new AccessDeniedException();
}
$client->delete();
$this->pool->getItem('C_' . $client->key)->clear();
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Deleted %s'), $client->name)
]);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @param $id
* @param $userId
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws ControllerNotImplemented
* @throws GeneralException
* @throws InvalidArgumentException
*/
public function revokeAccess(Request $request, Response $response, $id, $userId)
{
if ($userId === null) {
throw new InvalidArgumentException(__('No User ID provided'));
}
if (empty($id)) {
throw new InvalidArgumentException(__('No Client id provided'));
}
$client = $this->applicationFactory->getClientEntity($id);
if ($this->getUser()->userId != $userId) {
throw new InvalidArgumentException(__('Access denied: You do not own this authorization.'));
}
// remove record in lk table
$this->applicationFactory->revokeAuthorised($userId, $client->key);
// clear cache for this clientId/userId pair, this is how we know the application is no longer approved
$this->pool->getItem('C_' . $client->key . '/' . $userId)->clear();
$this->getLog()->audit(
'Auth',
0,
'Application access revoked',
[
'Application identifier ends with' => substr($client->key, -8),
'Application Name' => $client->getName()
]
);
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Access to %s revoked'), $client->name)
]);
return $this->render($request, $response);
}
}

189
lib/Controller/AuditLog.php Normal file
View File

@@ -0,0 +1,189 @@
<?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\Controller;
use Carbon\Carbon;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Factory\AuditLogFactory;
use Xibo\Helper\DateFormatHelper;
use Xibo\Helper\Random;
use Xibo\Helper\SendFile;
use Xibo\Support\Exception\InvalidArgumentException;
/**
* Class AuditLog
* @package Xibo\Controller
*/
class AuditLog extends Base
{
/**
* @var AuditLogFactory
*/
private $auditLogFactory;
/**
* Set common dependencies.
* @param AuditLogFactory $auditLogFactory
*/
public function __construct($auditLogFactory)
{
$this->auditLogFactory = $auditLogFactory;
}
/**
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function displayPage(Request $request, Response $response)
{
$this->getState()->template = 'auditlog-page';
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
function grid(Request $request, Response $response)
{
$sanitizedParams = $this->getSanitizer($request->getQueryParams());
$filterFromDt = $sanitizedParams->getDate('fromDt');
$filterToDt = $sanitizedParams->getDate('toDt');
$filterUser = $sanitizedParams->getString('user');
$filterEntity = $sanitizedParams->getString('entity');
$filterEntityId = $sanitizedParams->getString('entityId');
$filterMessage = $sanitizedParams->getString('message');
$filterIpAddress = $sanitizedParams->getString('ipAddress');
if ($filterFromDt != null && $filterFromDt == $filterToDt) {
$filterToDt->addDay();
}
// Get the dates and times
if ($filterFromDt == null) {
$filterFromDt = Carbon::now()->sub('1 day');
}
if ($filterToDt == null) {
$filterToDt = Carbon::now();
}
$search = [
'fromTimeStamp' => $filterFromDt->format('U'),
'toTimeStamp' => $filterToDt->format('U'),
'userName' => $filterUser,
'entity' => $filterEntity,
'entityId' => $filterEntityId,
'message' => $filterMessage,
'ipAddress' => $filterIpAddress,
'sessionHistoryId' => $sanitizedParams->getInt('sessionHistoryId')
];
$rows = $this->auditLogFactory->query(
$this->gridRenderSort($sanitizedParams),
$this->gridRenderFilter($search, $sanitizedParams)
);
// Do some post processing
foreach ($rows as $row) {
/* @var \Xibo\Entity\AuditLog $row */
$row->objectAfter = json_decode($row->objectAfter);
}
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = $this->auditLogFactory->countLast();
$this->getState()->setData($rows);
return $this->render($request, $response);
}
/**
* Output CSV Form
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function exportForm(Request $request, Response $response)
{
$this->getState()->template = 'auditlog-form-export';
return $this->render($request, $response);
}
/**
* Outputs a CSV of audit trail messages
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws InvalidArgumentException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function export(Request $request, Response $response) : Response
{
$sanitizedParams = $this->getSanitizer($request->getParams());
// We are expecting some parameters
$filterFromDt = $sanitizedParams->getDate('filterFromDt');
$filterToDt = $sanitizedParams->getDate('filterToDt');
$tempFileName = $this->getConfig()->getSetting('LIBRARY_LOCATION') . 'temp/audittrail_' . Random::generateString();
if ($filterFromDt == null || $filterToDt == null) {
throw new InvalidArgumentException(__('Please provide a from/to date.'), 'filterFromDt');
}
$fromTimeStamp = $filterFromDt->setTime(0, 0, 0)->format('U');
$toTimeStamp = $filterToDt->setTime(0, 0, 0)->format('U');
$rows = $this->auditLogFactory->query('logId', ['fromTimeStamp' => $fromTimeStamp, 'toTimeStamp' => $toTimeStamp]);
$out = fopen($tempFileName, 'w');
fputcsv($out, ['ID', 'Date', 'User', 'Entity', 'EntityId', 'Message', 'Object']);
// Do some post processing
foreach ($rows as $row) {
/* @var \Xibo\Entity\AuditLog $row */
fputcsv($out, [$row->logId, Carbon::createFromTimestamp($row->logDate)->format(DateFormatHelper::getSystemFormat()), $row->userName, $row->entity, $row->entityId, $row->message, $row->objectAfter]);
}
fclose($out);
$this->setNoOutput(true);
return $this->render($request, SendFile::decorateResponse(
$response,
$this->getConfig()->getSetting('SENDFILE_MODE'),
$tempFileName,
'audittrail.csv'
)->withHeader('Content-Type', 'text/csv;charset=utf-8'));
}
}

512
lib/Controller/Base.php Normal file
View File

@@ -0,0 +1,512 @@
<?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\Controller;
use Carbon\Carbon;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Slim\Routing\RouteContext;
use Slim\Views\Twig;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;
use Xibo\Entity\User;
use Xibo\Helper\ApplicationState;
use Xibo\Helper\HttpsDetect;
use Xibo\Helper\SanitizerService;
use Xibo\Service\BaseDependenciesService;
use Xibo\Service\ConfigServiceInterface;
use Xibo\Service\HelpServiceInterface;
use Xibo\Service\LogServiceInterface;
use Xibo\Support\Exception\ControllerNotImplemented;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
/**
* Class Base
* @package Xibo\Controller
*
* Base for all Controllers.
*
*/
class Base
{
use DataTablesDotNetTrait;
/**
* @var LogServiceInterface
*/
private $log;
/**
* @Inject
* @var SanitizerService
*/
private $sanitizerService;
/**
* @var ApplicationState
*/
private $state;
/**
* @var HelpServiceInterface
*/
private $helpService;
/**
* @var ConfigServiceInterface
*/
private $configService;
/**
* @var User
*/
private $user;
/**
* Automatically output a full page if non-ajax request arrives
* @var bool
*/
private $fullPage = true;
/**
* Have we already rendered this controller.
* @var bool
*/
private $rendered = false;
/**
* Is this controller expected to output anything?
* @var bool
*/
private $noOutput = false;
/**
* @var Twig
*/
private $view;
/** @var EventDispatcher */
private $dispatcher;
/** @var BaseDependenciesService */
private $baseDependenciesService;
public function useBaseDependenciesService(BaseDependenciesService $baseDependenciesService)
{
$this->baseDependenciesService = $baseDependenciesService;
}
/**
* Get User
* @return User
*/
public function getUser()
{
return $this->baseDependenciesService->getUser();
}
/**
* Get the Application State
* @return ApplicationState
*/
public function getState()
{
return $this->baseDependenciesService->getState();
}
/**
* Get Log
* @return LogServiceInterface
*/
public function getLog()
{
return $this->baseDependenciesService->getLogger();
}
/**
* @param $array
* @return \Xibo\Support\Sanitizer\SanitizerInterface
*/
protected function getSanitizer($array)
{
$sanitizerService = $this->getSanitizerService();
return $sanitizerService->getSanitizer($array);
}
public function getSanitizerService(): SanitizerService
{
return $this->baseDependenciesService->getSanitizer();
}
/**
* Get Config
* @return ConfigServiceInterface
*/
public function getConfig()
{
return $this->baseDependenciesService->getConfig();
}
/**
* @return \Slim\Views\Twig
*/
public function getView()
{
return $this->baseDependenciesService->getView();
}
/**
* @return EventDispatcherInterface
*/
public function getDispatcher(): EventDispatcherInterface
{
return $this->baseDependenciesService->getDispatcher();
}
/**
* Is this the Api?
* @param Request $request
* @return bool
*/
protected function isApi(Request $request)
{
return ($request->getAttribute('_entryPoint') != 'web');
}
/**
* Get Url For Route
* @param Request $request
* @param string $route
* @param array $data
* @param array $params
* @return string
*/
protected function urlFor(Request $request, $route, $data = [], $params = [])
{
$routeParser = RouteContext::fromRequest($request)->getRouteParser();
return $routeParser->urlFor($route, $data, $params);
}
/**
* Set to not output a full page automatically
*/
public function setNotAutomaticFullPage()
{
$this->fullPage = false;
}
/**
* Set No output
* @param bool $bool
*/
public function setNoOutput($bool = true)
{
$this->noOutput = $bool;
}
/**
* End the controller execution, calling render
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws ControllerNotImplemented if the controller is not implemented correctly
* @throws GeneralException
*/
public function render(Request $request, Response $response)
{
if ($this->noOutput) {
return $response;
}
// State will contain the current ApplicationState, including a success flag that can be used to determine
// if we are in error or not.
$state = $this->getState();
$data = $state->getData();
// Grid requests require some extra info appended.
// they can come from any application, hence being dealt with first
$grid = ($state->template === 'grid');
if ($grid) {
$params = $this->getSanitizer($request->getParams());
$recordsTotal = ($state->recordsTotal == null) ? count($data) : $state->recordsTotal;
$recordsFiltered = ($state->recordsFiltered == null) ? $recordsTotal : $state->recordsFiltered;
$data = [
'draw' => $params->getInt('draw'),
'recordsTotal' => $recordsTotal,
'recordsFiltered' => $recordsFiltered,
'data' => $data
];
}
// API Request
if ($this->isApi($request)) {
// Envelope by default - the APIView will un-pack if necessary
$this->getState()->setData([
'grid' => $grid,
'success' => $state->success,
'status' => $state->httpStatus,
'message' => $state->message,
'id' => $state->id,
'data' => $data
]);
return $this->renderApiResponse($request, $response->withStatus($state->httpStatus));
} else if ($request->isXhr()) {
// WEB Ajax
// --------
// Are we a template that should be rendered to HTML
// and then returned?
if ($state->template != '' && $state->template != 'grid') {
return $this->renderTwigAjaxReturn($request, $response);
}
// We always return 200's
if ($grid) {
$json = $data;
} else {
$json = $state->asArray();
}
return $response->withJson($json, 200);
} else {
// WEB Normal
// ----------
if (empty($state->template)) {
$this->getLog()->debug(sprintf('Template Missing. State: %s', json_encode($state)));
throw new ControllerNotImplemented(__('Template Missing'));
}
// Append the sidebar content
$data['clock'] = Carbon::now()->format('H:i T');
$data['currentUser'] = $this->getUser();
try {
$response = $this->getView()->render($response, $state->template . '.twig', $data);
} catch (LoaderError | RuntimeError | SyntaxError $e) {
$this->getLog()->error('Twig Error' . $e->getMessage());
throw new GeneralException(__('Unable to view this page'));
}
}
$this->rendered = true;
return $response;
}
/**
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws ControllerNotImplemented
* @throws GeneralException
*/
public function renderTwigAjaxReturn(Request $request, Response $response)
{
$data = $this->getState()->getData();
$state = $this->getState();
// Supply the current user to the view
$data['currentUser'] = $this->getUser();
// Render the view manually with Twig, parse it and pull out various bits
try {
$view = $this->getView()->render($response, $state->template . '.twig', $data);
} catch (LoaderError | RuntimeError | SyntaxError $e) {
$this->getLog()->error('Twig Error' . $e->getMessage());
throw new GeneralException(__('Unable to view this page'));
}
$view = $view->getBody();
// Log Rendered View
$this->getLog()->debug(sprintf('%s View: %s', $state->template, $view));
if (!$view = json_decode($view, true)) {
$this->getLog()->error(sprintf('Problem with Template: View = %s, Error = %s ', $state->template, json_last_error_msg()));
throw new ControllerNotImplemented(__('Problem with Form Template'));
}
$state->html = $view['html'];
$state->dialogTitle = trim($view['title']);
$state->callBack = $view['callBack'];
$state->extra = $view['extra'];
// Process the buttons
$state->buttons = [];
// Expect each button on a new line
if (trim($view['buttons']) != '') {
// Convert to an array
$view['buttons'] = str_replace("\n\r", "\n", $view['buttons']);
$buttons = explode("\n", $view['buttons']);
foreach ($buttons as $button) {
if ($button == '')
continue;
$this->getLog()->debug('Button is ' . $button);
$button = explode(',', trim($button));
if (count($button) != 2) {
$this->getLog()->error(sprintf('There is a problem with the buttons in the template: %s. Buttons: %s.', $state->template, var_export($view['buttons'], true)));
throw new ControllerNotImplemented(__('Problem with Form Template'));
}
$state->buttons[trim($button[0])] = str_replace('|', ',', trim($button[1]));
}
}
// Process the fieldActions
if (trim($view['fieldActions']) == '') {
$state->fieldActions = [];
} else {
// Convert to an array
$state->fieldActions = json_decode($view['fieldActions']);
}
$json = json_decode($state->asJson());
return $response = $response->withJson($json, 200);
}
/**
* Render a template to string
* @param string $template
* @param array $data
* @return string
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
public function renderTemplateToString($template, $data)
{
return $this->getView()->fetch($template . '.twig', $data);
}
/**
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
*/
public function renderApiResponse(Request $request, Response $response)
{
$data = $this->getState()->getData();
// Don't envelope unless requested
if ($request->getParam('envelope', 0) == 1
|| $request->getAttribute('_entryPoint') === 'test'
) {
// Envelope
// append error bool
if (!$data['success']) {
$data['success'] = false;
}
// append status code
$data['status'] = $response->getStatusCode();
// Enveloped responses always return 200
$response = $response->withStatus(200);
} else {
// Don't envelope
// Set status
$response = $response->withStatus($data['status']);
// Are we successful?
if (!$data['success']) {
// Error condition
$data = [
'error' => [
'message' => $data['message'],
'code' => $data['status'],
'data' => $data['data']
]
];
} else {
// Are we a grid?
if ($data['grid'] == true) {
// Set the response to our data['data'] object
$grid = $data['data'];
$data = $grid['data'];
// Total Number of Rows
$totalRows = $grid['recordsTotal'];
// Set some headers indicating our next/previous pages
$sanitizedParams = $this->getSanitizer($request->getParams());
$start = $sanitizedParams->getInt('start', ['default' => 0]);
$size = $sanitizedParams->getInt('length', ['default' => 10]);
$linkHeader = '';
$url = (new HttpsDetect())->getRootUrl() . $request->getUri()->getPath();
// Is there a next page?
if ($start + $size < $totalRows) {
$linkHeader .= '<' . $url . '?start=' . ($start + $size) . '&length=' . $size . '>; rel="next", ';
}
// Is there a previous page?
if ($start > 0) {
$linkHeader .= '<' . $url . '?start=' . ($start - $size) . '&length=' . $size . '>; rel="prev", ';
}
// The first page
$linkHeader .= '<' . $url . '?start=0&length=' . $size . '>; rel="first"';
$response = $response
->withHeader('X-Total-Count', $totalRows)
->withHeader('Link', $linkHeader);
} else {
// Set the response to our data object
$data = $data['data'];
}
}
}
return $response->withJson($data);
}
/**
* @param string $form The form name
* @return bool
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function getAutoSubmit(string $form)
{
return $this->getUser()->getOptionValue('autoSubmit.' . $form, 'false') === 'true';
}
public function checkRootFolderAllowSave()
{
if ($this->getConfig()->getSetting('FOLDERS_ALLOW_SAVE_IN_ROOT') == 0
&& !$this->getUser()->isSuperAdmin()
) {
throw new InvalidArgumentException(
__('Saving into root folder is disabled, please select a different folder')
);
}
}
}

1538
lib/Controller/Campaign.php Normal file

File diff suppressed because it is too large Load Diff

93
lib/Controller/Clock.php Normal file
View File

@@ -0,0 +1,93 @@
<?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\Controller;
use Carbon\Carbon;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Helper\Session;
/**
* Class Clock
* @package Xibo\Controller
*/
class Clock extends Base
{
/**
* @var Session
*/
private $session;
/**
* Set common dependencies.
* @param Session $session
*/
public function __construct($session)
{
$this->session = $session;
}
/**
* Gets the Time
*
* @SWG\Get(
* path="/clock",
* operationId="clock",
* tags={"misc"},
* description="The Time",
* summary="The current CMS time",
* @SWG\Response(
* response=200,
* description="successful response",
* @SWG\Schema(
* type="object",
* additionalProperties={"title":"time", "type":"string"}
* )
* )
* )
*
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function clock(Request $request, Response $response)
{
$this->session->refreshExpiry = false;
if ($request->isXhr() || $this->isApi($request)) {
$output = Carbon::now()->format('H:i T');
$this->getState()->setData(array('time' => $output));
$this->getState()->html = $output;
$this->getState()->clockUpdate = true;
$this->getState()->success = true;
return $this->render($request, $response);
} else {
// We are returning the response directly, so write the body.
$response->getBody()->write(Carbon::now()->format('c'));
return $response;
}
}
}

585
lib/Controller/Command.php Normal file
View File

@@ -0,0 +1,585 @@
<?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\Controller;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Event\CommandDeleteEvent;
use Xibo\Factory\CommandFactory;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\ControllerNotImplemented;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\NotFoundException;
/**
* Class Command
* Command Controller
* @package Xibo\Controller
*/
class Command extends Base
{
/**
* @var CommandFactory
*/
private $commandFactory;
/**
* Set common dependencies.
* @param CommandFactory $commandFactory
*/
public function __construct($commandFactory)
{
$this->commandFactory = $commandFactory;
}
/**
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws ControllerNotImplemented
* @throws GeneralException
*/
public function displayPage(Request $request, Response $response)
{
$this->getState()->template = 'command-page';
return $this->render($request, $response);
}
/**
* @SWG\Get(
* path="/command",
* operationId="commandSearch",
* tags={"command"},
* summary="Command Search",
* description="Search this users Commands",
* @SWG\Parameter(
* name="commandId",
* in="query",
* description="Filter by Command Id",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="command",
* in="query",
* description="Filter by Command Name",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="code",
* in="query",
* description="Filter by Command Code",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="useRegexForName",
* in="query",
* description="Flag (0,1). When filtering by multiple commands in command filter, should we use regex?",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="useRegexForCode",
* in="query",
* description="Flag (0,1). When filtering by multiple codes in code filter, should we use regex?",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="logicalOperatorName",
* in="query",
* description="When filtering by multiple commands in command filter,
* which logical operator should be used? AND|OR",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="logicalOperatorCode",
* in="query",
* description="When filtering by multiple codes in code filter,
* which logical operator should be used? AND|OR",
* type="string",
* required=false
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="#/definitions/Command")
* )
* )
* )
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws ControllerNotImplemented
* @throws GeneralException
* @throws NotFoundException
*/
public function grid(Request $request, Response $response)
{
$sanitizedParams = $this->getSanitizer($request->getParams());
$filter = [
'commandId' => $sanitizedParams->getInt('commandId'),
'command' => $sanitizedParams->getString('command'),
'code' => $sanitizedParams->getString('code'),
'useRegexForName' => $sanitizedParams->getCheckbox('useRegexForName'),
'useRegexForCode' => $sanitizedParams->getCheckbox('useRegexForCode'),
'logicalOperatorName' => $sanitizedParams->getString('logicalOperatorName'),
'logicalOperatorCode' => $sanitizedParams->getString('logicalOperatorCode'),
];
$commands = $this->commandFactory->query(
$this->gridRenderSort($sanitizedParams),
$this->gridRenderFilter($filter, $sanitizedParams)
);
foreach ($commands as $command) {
/* @var \Xibo\Entity\Command $command */
if ($this->isApi($request)) {
continue;
}
$command->includeProperty('buttons');
if ($this->getUser()->featureEnabled('command.modify')) {
// Command edit
if ($this->getUser()->checkEditable($command)) {
$command->buttons[] = array(
'id' => 'command_button_edit',
'url' => $this->urlFor($request, 'command.edit.form', ['id' => $command->commandId]),
'text' => __('Edit')
);
}
// Command delete
if ($this->getUser()->checkDeleteable($command)) {
$command->buttons[] = [
'id' => 'command_button_delete',
'url' => $this->urlFor($request, 'command.delete.form', ['id' => $command->commandId]),
'text' => __('Delete'),
'multi-select' => true,
'dataAttributes' => [
[
'name' => 'commit-url',
'value' => $this->urlFor($request, 'command.delete', ['id' => $command->commandId])
],
['name' => 'commit-method', 'value' => 'delete'],
['name' => 'id', 'value' => 'command_button_delete'],
['name' => 'text', 'value' => __('Delete')],
['name' => 'sort-group', 'value' => 1],
['name' => 'rowtitle', 'value' => $command->command]
]
];
}
// Command Permissions
if ($this->getUser()->checkPermissionsModifyable($command)) {
// Permissions button
$command->buttons[] = [
'id' => 'command_button_permissions',
'url' => $this->urlFor(
$request,
'user.permissions.form',
['entity' => 'Command', 'id' => $command->commandId]
),
'text' => __('Share'),
'multi-select' => true,
'dataAttributes' => [
[
'name' => 'commit-url',
'value' => $this->urlFor(
$request,
'user.permissions.multi',
['entity' => 'Command', 'id' => $command->commandId]
)
],
['name' => 'commit-method', 'value' => 'post'],
['name' => 'id', 'value' => 'command_button_permissions'],
['name' => 'text', 'value' => __('Share')],
['name' => 'rowtitle', 'value' => $command->command],
['name' => 'sort-group', 'value' => 2],
['name' => 'custom-handler', 'value' => 'XiboMultiSelectPermissionsFormOpen'],
[
'name' => 'custom-handler-url',
'value' => $this->urlFor(
$request,
'user.permissions.multi.form',
['entity' => 'Command']
)
],
['name' => 'content-id-name', 'value' => 'commandId']
]
];
}
}
}
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = $this->commandFactory->countLast();
$this->getState()->setData($commands);
return $this->render($request, $response);
}
/**
* Add Command Form
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws ControllerNotImplemented
* @throws GeneralException
*/
public function addForm(Request $request, Response $response)
{
$this->getState()->template = 'command-form-add';
return $this->render($request, $response);
}
/**
* Edit Command
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws ControllerNotImplemented
* @throws GeneralException
* @throws NotFoundException
*/
public function editForm(Request $request, Response $response, $id)
{
$command = $this->commandFactory->getById($id);
if (!$this->getUser()->checkEditable($command)) {
throw new AccessDeniedException();
}
$this->getState()->template = 'command-form-edit';
$this->getState()->setData([
'command' => $command
]);
return $this->render($request, $response);
}
/**
* Delete Command
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws ControllerNotImplemented
* @throws GeneralException
* @throws NotFoundException
*/
public function deleteForm(Request $request, Response $response, $id)
{
$command = $this->commandFactory->getById($id);
if (!$this->getUser()->checkDeleteable($command)) {
throw new AccessDeniedException();
}
$this->getState()->template = 'command-form-delete';
$this->getState()->setData([
'command' => $command
]);
return $this->render($request, $response);
}
/**
* Add Command
*
* @SWG\Post(
* path="/command",
* operationId="commandAdd",
* tags={"command"},
* summary="Command Add",
* description="Add a Command",
* @SWG\Parameter(
* name="command",
* in="formData",
* description="The Command Name",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="description",
* in="formData",
* description="A description for the command",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="code",
* in="formData",
* description="A unique code for this command",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="commandString",
* in="formData",
* description="The Command String for this Command. Can be overridden on Display Settings.",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="validationString",
* in="formData",
* description="The Validation String for this Command. Can be overridden on Display Settings.",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="availableOn",
* in="formData",
* description="An array of Player types this Command is available on, empty for all.",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="createAlertOn",
* in="formData",
* description="On command execution, when should a Display alert be created?
* success, failure, always or never",
* type="string",
* required=false
* ),
* @SWG\Response(
* response=201,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Command"),
* @SWG\Header(
* header="Location",
* description="Location of the new record",
* type="string"
* )
* )
* )
*
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws ControllerNotImplemented
* @throws GeneralException
*/
public function add(Request $request, Response $response)
{
$sanitizedParams = $this->getSanitizer($request->getParams());
$command = $this->commandFactory->create();
$command->command = $sanitizedParams->getString('command');
$command->description = $sanitizedParams->getString('description');
$command->code = $sanitizedParams->getString('code');
$command->userId = $this->getUser()->userId;
$command->commandString = $sanitizedParams->getString('commandString');
$command->validationString = $sanitizedParams->getString('validationString');
$command->createAlertOn = $sanitizedParams->getString('createAlertOn', ['default' => 'never']);
$availableOn = $sanitizedParams->getArray('availableOn');
if (empty($availableOn)) {
$command->availableOn = null;
} else {
$command->availableOn = implode(',', $availableOn);
}
$command->save();
// Return
$this->getState()->hydrate([
'httpStatus' => 201,
'message' => sprintf(__('Added %s'), $command->command),
'id' => $command->commandId,
'data' => $command
]);
return $this->render($request, $response);
}
/**
* Edit Command
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws ControllerNotImplemented
* @throws GeneralException
* @throws NotFoundException
*
* @SWG\Put(
* path="/command/{commandId}",
* operationId="commandEdit",
* tags={"command"},
* summary="Edit Command",
* description="Edit the provided command",
* @SWG\Parameter(
* name="commandId",
* in="path",
* description="The Command Id to Edit",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="command",
* in="formData",
* description="The Command Name",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="description",
* in="formData",
* description="A description for the command",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="commandString",
* in="formData",
* description="The Command String for this Command. Can be overridden on Display Settings.",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="validationString",
* in="formData",
* description="The Validation String for this Command. Can be overridden on Display Settings.",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="availableOn",
* in="formData",
* description="An array of Player types this Command is available on, empty for all.",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="createAlertOn",
* in="formData",
* description="On command execution, when should a Display alert be created?
* success, failure, always or never",
* type="string",
* required=false
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Command")
* )
* )
*/
public function edit(Request $request, Response $response, $id)
{
$sanitizedParams = $this->getSanitizer($request->getParams());
$command = $this->commandFactory->getById($id);
if (!$this->getUser()->checkEditable($command)) {
throw new AccessDeniedException();
}
$command->command = $sanitizedParams->getString('command');
$command->description = $sanitizedParams->getString('description');
$command->commandString = $sanitizedParams->getString('commandString');
$command->validationString = $sanitizedParams->getString('validationString');
$command->createAlertOn = $sanitizedParams->getString('createAlertOn', ['default' => 'never']);
$availableOn = $sanitizedParams->getArray('availableOn');
if (empty($availableOn)) {
$command->availableOn = null;
} else {
$command->availableOn = implode(',', $availableOn);
}
$command->save();
// Return
$this->getState()->hydrate([
'httpStatus' => 200,
'message' => sprintf(__('Edited %s'), $command->command),
'id' => $command->commandId,
'data' => $command
]);
return $this->render($request, $response);
}
/**
* Delete Command
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws ControllerNotImplemented
* @throws GeneralException
* @throws NotFoundException
* @SWG\Delete(
* path="/command/{commandId}",
* operationId="commandDelete",
* tags={"command"},
* summary="Delete Command",
* description="Delete the provided command",
* @SWG\Parameter(
* name="commandId",
* in="path",
* description="The Command Id to Delete",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*/
public function delete(Request $request, Response $response, $id)
{
$command = $this->commandFactory->getById($id);
if (!$this->getUser()->checkDeleteable($command)) {
throw new AccessDeniedException();
}
$this->getDispatcher()->dispatch(new CommandDeleteEvent($command), CommandDeleteEvent::$NAME);
$command->delete();
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Deleted %s'), $command->command)
]);
return $this->render($request, $response);
}
}

View File

@@ -0,0 +1,261 @@
<?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\Controller;
use Slim\Exception\HttpMethodNotAllowedException;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Event\ConnectorDeletingEvent;
use Xibo\Event\ConnectorEnabledChangeEvent;
use Xibo\Factory\ConnectorFactory;
use Xibo\Factory\WidgetFactory;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\ControllerNotImplemented;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\NotFoundException;
/**
* Connector controller to view, activate and install connectors.
*/
class Connector extends Base
{
/** @var \Xibo\Factory\ConnectorFactory */
private $connectorFactory;
/** @var WidgetFactory */
private $widgetFactory;
public function __construct(ConnectorFactory $connectorFactory, WidgetFactory $widgetFactory)
{
$this->connectorFactory = $connectorFactory;
$this->widgetFactory = $widgetFactory;
}
/**
* @param \Slim\Http\ServerRequest $request
* @param \Slim\Http\Response $response
* @return \Psr\Http\Message\ResponseInterface|\Slim\Http\Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function grid(Request $request, Response $response)
{
$params = $this->getSanitizer($request->getParams());
$connectors = $this->connectorFactory->query($request->getParams());
// Should we show uninstalled connectors?
if ($params->getCheckbox('showUninstalled')) {
$connectors = array_merge($connectors, $this->connectorFactory->getUninstalled());
}
foreach ($connectors as $connector) {
// Instantiate and decorate the entity
try {
$connector->decorate($this->connectorFactory->create($connector));
} catch (NotFoundException) {
$this->getLog()->info('Connector installed which is not found in this CMS. ' . $connector->className);
$connector->setUnmatchedProperty('isHidden', 1);
} catch (\Exception $e) {
$this->getLog()->error('Incorrectly configured connector '
. $connector->className . '. e=' . $e->getMessage());
$connector->setUnmatchedProperty('isHidden', 1);
}
}
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = count($connectors);
$this->getState()->setData($connectors);
return $this->render($request, $response);
}
/**
* Edit Connector Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws ControllerNotImplemented
* @throws GeneralException
* @throws NotFoundException
*/
public function editForm(Request $request, Response $response, $id)
{
// Is this an installed connector, or not.
if (is_numeric($id)) {
$connector = $this->connectorFactory->getById($id);
} else {
$connector = $this->connectorFactory->getUninstalledById($id);
}
$interface = $this->connectorFactory->create($connector);
$this->getState()->template = $interface->getSettingsFormTwig() ?: 'connector-form-edit';
$this->getState()->setData([
'connector' => $connector,
'interface' => $interface
]);
return $this->render($request, $response);
}
/**
* Edit Connector Form Proxy
* this is a magic method used to call a connector method which returns some JSON data
* @param Request $request
* @param Response $response
* @param $id
* @param $method
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Slim\Exception\HttpMethodNotAllowedException
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function editFormProxy(Request $request, Response $response, $id, $method)
{
$connector = $this->connectorFactory->getById($id);
$interface = $this->connectorFactory->create($connector);
if (method_exists($interface, $method)) {
return $response->withJson($interface->{$method}($this->getSanitizer($request->getParams())));
} else {
throw new HttpMethodNotAllowedException($request);
}
}
/**
* Edit Connector
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws ControllerNotImplemented
* @throws GeneralException
* @throws NotFoundException
*/
public function edit(Request $request, Response $response, $id)
{
$params = $this->getSanitizer($request->getParams());
if (is_numeric($id)) {
$connector = $this->connectorFactory->getById($id);
} else {
$connector = $this->connectorFactory->getUninstalledById($id);
// Null the connectorId so that we add this to the database.
$connector->connectorId = null;
}
$interface = $this->connectorFactory->create($connector);
// Is this an uninstallation request
if ($params->getCheckbox('shouldUninstall')) {
// Others
$this->getDispatcher()->dispatch(
new ConnectorDeletingEvent($connector, $this->getConfig()),
ConnectorDeletingEvent::$NAME
);
// Ourselves
if (method_exists($interface, 'delete')) {
$interface->delete($this->getConfig());
}
$connector->delete();
// Successful
$this->getState()->hydrate([
'message' => sprintf(__('Uninstalled %s'), $interface->getTitle())
]);
} else {
// Core properties
$connector->isEnabled = $params->getCheckbox('isEnabled');
// Enabled state change.
// Update ourselves, and any others that might be interested.
if ($connector->hasPropertyChanged('isEnabled')) {
// Others
$this->getDispatcher()->dispatch(
new ConnectorEnabledChangeEvent($connector, $this->getConfig()),
ConnectorEnabledChangeEvent::$NAME
);
// Ourselves
if ($connector->isEnabled && method_exists($interface, 'enable')) {
$interface->enable($this->getConfig());
} else if (!$connector->isEnabled && method_exists($interface, 'disable')) {
$interface->disable($this->getConfig());
}
}
$connector->settings = $interface->processSettingsForm($params, $connector->settings);
$connector->save();
// Successful
$this->getState()->hydrate([
'message' => sprintf(__('Edited %s'), $interface->getTitle()),
'id' => $id,
'data' => $connector
]);
}
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @param $token
* @return \Psr\Http\Message\ResponseInterface
* @throws AccessDeniedException
*/
public function connectorPreview(Request $request, Response $response)
{
$params = $this->getSanitizer($request->getParams());
$token = $params->getString('token');
$isDebug = $params->getCheckbox('isDebug');
if (empty($token)) {
throw new AccessDeniedException();
}
// Dispatch an event to check the token
$tokenEvent = new \Xibo\Event\XmdsConnectorTokenEvent();
$tokenEvent->setToken($token);
$this->getDispatcher()->dispatch($tokenEvent, \Xibo\Event\XmdsConnectorTokenEvent::$NAME);
if (empty($tokenEvent->getWidgetId())) {
throw new AccessDeniedException();
}
// Get the widget
$widget = $this->widgetFactory->getById($tokenEvent->getWidgetId());
// It has been found, so we raise an event here to see if any connector can provide a file for it.
$event = new \Xibo\Event\XmdsConnectorFileEvent($widget, $isDebug);
$this->getDispatcher()->dispatch($event, \Xibo\Event\XmdsConnectorFileEvent::$NAME);
// What now?
return $event->getResponse();
}
}

View File

@@ -0,0 +1,345 @@
<?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\Controller;
use Carbon\Carbon;
use Psr\Http\Message\ResponseInterface;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Entity\Display;
use Xibo\Factory\CampaignFactory;
use Xibo\Factory\CommandFactory;
use Xibo\Factory\DayPartFactory;
use Xibo\Factory\DisplayFactory;
use Xibo\Factory\DisplayGroupFactory;
use Xibo\Factory\FolderFactory;
use Xibo\Factory\LayoutFactory;
use Xibo\Factory\ScheduleFactory;
use Xibo\Helper\Session;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\ControllerNotImplemented;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
/**
* Class CypressTest
* @package Xibo\Controller
*/
class CypressTest extends Base
{
/** @var StorageServiceInterface */
private $store;
/**
* @var Session
*/
private $session;
/**
* @var ScheduleFactory
*/
private $scheduleFactory;
/** @var FolderFactory */
private $folderFactory;
/**
* @var CommandFactory
*/
private $commandFactory;
/**
* @var DisplayGroupFactory
*/
private $displayGroupFactory;
/**
* @var CampaignFactory
*/
private $campaignFactory;
/** @var DisplayFactory */
private $displayFactory;
/** @var LayoutFactory */
private $layoutFactory;
/** @var DayPartFactory */
private $dayPartFactory;
/**
* Set common dependencies.
* @param StorageServiceInterface $store
* @param Session $session
* @param ScheduleFactory $scheduleFactory
* @param DisplayGroupFactory $displayGroupFactory
* @param CampaignFactory $campaignFactory
* @param DisplayFactory $displayFactory
* @param LayoutFactory $layoutFactory
* @param DayPartFactory $dayPartFactory
*/
public function __construct(
$store,
$session,
$scheduleFactory,
$displayGroupFactory,
$campaignFactory,
$displayFactory,
$layoutFactory,
$dayPartFactory,
$folderFactory,
$commandFactory
) {
$this->store = $store;
$this->session = $session;
$this->scheduleFactory = $scheduleFactory;
$this->displayGroupFactory = $displayGroupFactory;
$this->campaignFactory = $campaignFactory;
$this->displayFactory = $displayFactory;
$this->layoutFactory = $layoutFactory;
$this->dayPartFactory = $dayPartFactory;
$this->folderFactory = $folderFactory;
$this->commandFactory = $commandFactory;
}
// <editor-fold desc="Displays">
/**
* @throws InvalidArgumentException
* @throws ControllerNotImplemented
* @throws NotFoundException
* @throws GeneralException
*/
public function scheduleCampaign(Request $request, Response $response): Response|ResponseInterface
{
$this->getLog()->debug('Add Schedule');
$sanitizedParams = $this->getSanitizer($request->getParams());
$schedule = $this->scheduleFactory->createEmpty();
$schedule->userId = $this->getUser()->userId;
$schedule->eventTypeId = 5;
$schedule->campaignId = $sanitizedParams->getInt('campaignId');
$schedule->commandId = $sanitizedParams->getInt('commandId');
$schedule->displayOrder = $sanitizedParams->getInt('displayOrder', ['default' => 0]);
$schedule->isPriority = $sanitizedParams->getInt('isPriority', ['default' => 0]);
$schedule->isGeoAware = $sanitizedParams->getCheckbox('isGeoAware');
$schedule->actionType = $sanitizedParams->getString('actionType');
$schedule->actionTriggerCode = $sanitizedParams->getString('actionTriggerCode');
$schedule->actionLayoutCode = $sanitizedParams->getString('actionLayoutCode');
$schedule->maxPlaysPerHour = $sanitizedParams->getInt('maxPlaysPerHour', ['default' => 0]);
$schedule->syncGroupId = $sanitizedParams->getInt('syncGroupId');
// Set the parentCampaignId for campaign events
if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$CAMPAIGN_EVENT) {
$schedule->parentCampaignId = $schedule->campaignId;
// Make sure we're not directly scheduling an ad campaign
$campaign = $this->campaignFactory->getById($schedule->campaignId);
if ($campaign->type === 'ad') {
throw new InvalidArgumentException(
__('Direct scheduling of an Ad Campaign is not allowed'),
'campaignId'
);
}
}
// Fields only collected for interrupt events
if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$INTERRUPT_EVENT) {
$schedule->shareOfVoice = $sanitizedParams->getInt('shareOfVoice', [
'throw' => function () {
new InvalidArgumentException(
__('Share of Voice must be a whole number between 0 and 3600'),
'shareOfVoice'
);
}
]);
} else {
$schedule->shareOfVoice = null;
}
$schedule->dayPartId = 2;
$schedule->syncTimezone = 0;
$displays = $this->displayFactory->query(null, ['display' => $sanitizedParams->getString('displayName')]);
$display = $displays[0];
$schedule->assignDisplayGroup($this->displayGroupFactory->getById($display->displayGroupId));
// Ready to do the add
$schedule->setDisplayNotifyService($this->displayFactory->getDisplayNotifyService());
if ($schedule->campaignId != null) {
$schedule->setCampaignFactory($this->campaignFactory);
}
$schedule->save();
// Return
$this->getState()->hydrate([
'httpStatus' => 201,
'message' => __('Added Event'),
'id' => $schedule->eventId,
'data' => $schedule
]);
return $this->render($request, $response);
}
/**
* @throws NotFoundException
* @throws ControllerNotImplemented
* @throws GeneralException
*/
public function displaySetStatus(Request $request, Response $response): Response|ResponseInterface
{
$this->getLog()->debug('Set display status');
$sanitizedParams = $this->getSanitizer($request->getParams());
$displays = $this->displayFactory->query(null, ['display' => $sanitizedParams->getString('displayName')]);
$display = $displays[0];
// Get the display
$status = $sanitizedParams->getInt('statusId');
// Set display status
$display->mediaInventoryStatus = $status;
$this->store->update('UPDATE `display` SET MediaInventoryStatus = :status, auditingUntil = :auditingUntil
WHERE displayId = :displayId', [
'displayId' => $display->displayId,
'auditingUntil' => Carbon::now()->addSeconds(86400)->format('U'),
'status' => Display::$STATUS_DONE
]);
$this->store->commitIfNecessary();
$this->store->close();
return $this->render($request, $response);
}
/**
* @throws NotFoundException
* @throws ControllerNotImplemented
* @throws GeneralException
*/
public function displayStatusEquals(Request $request, Response $response): Response|ResponseInterface
{
$this->getLog()->debug('Check display status');
$sanitizedParams = $this->getSanitizer($request->getParams());
// Get the display
$displays = $this->displayFactory->query(null, ['display' => $sanitizedParams->getString('displayName')]);
$display = $displays[0];
$status = $sanitizedParams->getInt('statusId');
// Check display status
$this->getState()->hydrate([
'httpStatus' => 201,
'data' => $display->mediaInventoryStatus === $status
]);
return $this->render($request, $response);
}
// </editor-fold>
public function createCommand(Request $request, Response $response): Response|ResponseInterface
{
$sanitizedParams = $this->getSanitizer($request->getParams());
$command = $this->commandFactory->create();
$command->command = $sanitizedParams->getString('command');
$command->description = $sanitizedParams->getString('description');
$command->code = $sanitizedParams->getString('code');
$command->userId = $this->getUser()->userId;
$command->commandString = $sanitizedParams->getString('commandString');
$command->validationString = $sanitizedParams->getString('validationString');
$availableOn = $sanitizedParams->getArray('availableOn');
if (empty($availableOn)) {
$command->availableOn = null;
} else {
$command->availableOn = implode(',', $availableOn);
}
$command->save();
// Return
$this->getState()->hydrate([
'httpStatus' => 201,
'message' => sprintf(__('Added %s'), $command->command),
'id' => $command->commandId,
'data' => $command
]);
return $this->render($request, $response);
}
/**
* @throws InvalidArgumentException
* @throws ControllerNotImplemented
* @throws NotFoundException
* @throws GeneralException
*/
public function createCampaign(Request $request, Response $response): Response|ResponseInterface
{
$this->getLog()->debug('Creating campaign');
$sanitizedParams = $this->getSanitizer($request->getParams());
$folder = $this->folderFactory->getById($this->getUser()->homeFolderId, 0);
// Create Campaign
$campaign = $this->campaignFactory->create(
'list',
$sanitizedParams->getString('name'),
$this->getUser()->userId,
$folder->getId()
);
// Cycle based playback
if ($campaign->type === 'list') {
$campaign->cyclePlaybackEnabled = $sanitizedParams->getCheckbox('cyclePlaybackEnabled');
$campaign->playCount = ($campaign->cyclePlaybackEnabled) ? $sanitizedParams->getInt('playCount') : null;
// For compatibility with existing API implementations we set a default here.
$campaign->listPlayOrder = ($campaign->cyclePlaybackEnabled)
? 'block'
: $sanitizedParams->getString('listPlayOrder', ['default' => 'round']);
} else if ($campaign->type === 'ad') {
$campaign->targetType = $sanitizedParams->getString('targetType');
$campaign->target = $sanitizedParams->getInt('target');
$campaign->listPlayOrder = 'round';
}
// All done, save.
$campaign->save();
// Return
$this->getState()->hydrate([
'httpStatus' => 201,
'message' => __('Added campaign'),
'id' => $campaign->campaignId,
'data' => $campaign
]);
return $this->render($request, $response);
}
// <editor-fold desc="Schedule">
// </editor-fold>
}

1990
lib/Controller/DataSet.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,690 @@
<?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\Controller;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Stash\Interfaces\PoolInterface;
use Xibo\Factory\DataSetColumnFactory;
use Xibo\Factory\DataSetColumnTypeFactory;
use Xibo\Factory\DataSetFactory;
use Xibo\Factory\DataTypeFactory;
use Xibo\Support\Exception\AccessDeniedException;
/**
* Class DataSetColumn
* @package Xibo\Controller
*/
class DataSetColumn extends Base
{
/** @var DataSetFactory */
private $dataSetFactory;
/** @var DataSetColumnFactory */
private $dataSetColumnFactory;
/** @var DataSetColumnTypeFactory */
private $dataSetColumnTypeFactory;
/** @var DataTypeFactory */
private $dataTypeFactory;
/** @var PoolInterface */
private $pool;
/**
* Set common dependencies.
* @param DataSetFactory $dataSetFactory
* @param DataSetColumnFactory $dataSetColumnFactory
* @param DataSetColumnTypeFactory $dataSetColumnTypeFactory
* @param DataTypeFactory $dataTypeFactory
* @param PoolInterface $pool
*/
public function __construct($dataSetFactory, $dataSetColumnFactory, $dataSetColumnTypeFactory, $dataTypeFactory, $pool)
{
$this->dataSetFactory = $dataSetFactory;
$this->dataSetColumnFactory = $dataSetColumnFactory;
$this->dataSetColumnTypeFactory = $dataSetColumnTypeFactory;
$this->dataTypeFactory = $dataTypeFactory;
$this->pool = $pool;
}
/**
* Column Page
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function displayPage(Request $request, Response $response, $id)
{
$dataSet = $this->dataSetFactory->getById($id);
if (!$this->getUser()->checkEditable($dataSet)) {
throw new AccessDeniedException();
}
$this->getState()->template = 'dataset-column-page';
$this->getState()->setData([
'dataSet' => $dataSet
]);
return $this->render($request, $response);
}
/**
* Column Search
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
* @SWG\Get(
* path="/dataset/{dataSetId}/column",
* operationId="dataSetColumnSearch",
* tags={"dataset"},
* summary="Search Columns",
* description="Search Columns for DataSet",
* @SWG\Parameter(
* name="dataSetId",
* in="path",
* description="The DataSet ID",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="dataSetColumnId",
* in="query",
* description="Filter by DataSet ColumnID",
* type="integer",
* required=false
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="#/definitions/DataSetColumn")
* )
* )
* )
*/
public function grid(Request $request, Response $response, $id)
{
$dataSet = $this->dataSetFactory->getById($id);
$parsedRequestParams = $this->getSanitizer($request->getParams());
if (!$this->getUser()->checkEditable($dataSet)) {
throw new AccessDeniedException();
}
$dataSetColumns = $this->dataSetColumnFactory->query(
$this->gridRenderSort($parsedRequestParams),
$this->gridRenderFilter(
['dataSetId' => $id, 'dataSetColumnId' => $parsedRequestParams->getInt('dataSetColumnId')],
$parsedRequestParams
)
);
foreach ($dataSetColumns as $column) {
/* @var \Xibo\Entity\DataSetColumn $column */
$column->dataType = __($column->dataType);
$column->dataSetColumnType = __($column->dataSetColumnType);
if ($this->isApi($request))
break;
$column->includeProperty('buttons');
if ($this->getUser()->featureEnabled('dataset.modify')) {
// Edit
$column->buttons[] = array(
'id' => 'dataset_button_edit',
'url' => $this->urlFor($request,'dataSet.column.edit.form', ['id' => $id, 'colId' => $column->dataSetColumnId]),
'text' => __('Edit')
);
if ($this->getUser()->checkDeleteable($dataSet)) {
// Delete
$column->buttons[] = array(
'id' => 'dataset_button_delete',
'url' => $this->urlFor($request,'dataSet.column.delete.form', ['id' => $id, 'colId' => $column->dataSetColumnId]),
'text' => __('Delete')
);
}
}
}
$this->getState()->template = 'grid';
$this->getState()->setData($dataSetColumns);
$this->getState()->recordsTotal = $this->dataSetColumnFactory->countLast();
return $this->render($request, $response);
}
/**
* Add form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function addForm(Request $request, Response $response, $id)
{
$dataSet = $this->dataSetFactory->getById($id);
if (!$this->getUser()->checkEditable($dataSet)) {
throw new AccessDeniedException();
}
$this->getState()->template = 'dataset-column-form-add';
$this->getState()->setData([
'dataSet' => $dataSet,
'dataTypes' => $this->dataTypeFactory->query(),
'dataSetColumnTypes' => $this->dataSetColumnTypeFactory->query(),
]);
return $this->render($request, $response);
}
/**
* Add
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\InvalidArgumentException
* @throws \Xibo\Support\Exception\NotFoundException
* @SWG\Post(
* path="/dataset/{dataSetId}/column",
* operationId="dataSetColumnAdd",
* tags={"dataset"},
* summary="Add Column",
* description="Add a Column to a DataSet",
* @SWG\Parameter(
* name="dataSetId",
* in="path",
* description="The DataSet ID",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="heading",
* in="formData",
* description="The heading for the Column",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="listContent",
* in="formData",
* description="A comma separated list of content for drop downs",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="columnOrder",
* in="formData",
* description="The display order for this column",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="dataTypeId",
* in="formData",
* description="The data type ID for this column",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="dataSetColumnTypeId",
* in="formData",
* description="The column type for this column",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="formula",
* in="formData",
* description="MySQL SELECT syntax formula for this Column if the column type is formula",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="remoteField",
* in="formData",
* description="JSON-String to select Data from the Remote DataSet",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="showFilter",
* in="formData",
* description="Flag indicating whether this column should present a filter on DataEntry",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="showSort",
* in="formData",
* description="Flag indicating whether this column should allow sorting on DataEntry",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="tooltip",
* in="formData",
* description="Help text that should be displayed when entering data for this Column.",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="isRequired",
* in="formData",
* description="Flag indicating whether value must be provided for this Column.",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="dateFormat",
* in="formData",
* description="PHP date format for the dates in the source of the remote DataSet",
* type="string",
* required=false
* ),
* @SWG\Response(
* response=201,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/DataSetColumn"),
* @SWG\Header(
* header="Location",
* description="Location of the new record",
* type="string"
* )
* )
* )
*/
public function add(Request $request, Response $response, $id)
{
$dataSet = $this->dataSetFactory->getById($id);
$sanitizedParams = $this->getSanitizer($request->getParams());
if (!$this->getUser()->checkEditable($dataSet)) {
throw new AccessDeniedException();
}
// Create a Column
$column = $this->dataSetColumnFactory->createEmpty();
$column->heading = $sanitizedParams->getString('heading');
$column->listContent = $sanitizedParams->getString('listContent');
$column->columnOrder = $sanitizedParams->getInt('columnOrder');
$column->dataTypeId = $sanitizedParams->getInt('dataTypeId');
$column->dataSetColumnTypeId = $sanitizedParams->getInt('dataSetColumnTypeId');
$column->formula = $request->getParam('formula', null);
$column->remoteField = $request->getParam('remoteField', null);
$column->showFilter = $sanitizedParams->getCheckbox('showFilter');
$column->showSort = $sanitizedParams->getCheckbox('showSort');
$column->tooltip = $sanitizedParams->getString('tooltip');
$column->isRequired = $sanitizedParams->getCheckbox('isRequired', ['default' => 0]);
$column->dateFormat = $sanitizedParams->getString('dateFormat', ['default' => null]);
if ($column->dataSetColumnTypeId == 3) {
$this->pool->deleteItem('/dataset/cache/' . $dataSet->dataSetId);
$this->getLog()->debug('New remote column detected, clear cache for remote dataSet ID ' . $dataSet->dataSetId);
}
// Assign the column to set the column order if necessary
$dataSet->assignColumn($column);
// client side formula disable sort
if (substr($column->formula, 0, 1) === '$') {
$column->showSort = 0;
}
// Save the column
$column->save();
// Notify the change
$dataSet->notify();
// Return
$this->getState()->hydrate([
'httpStatus' => 201,
'message' => sprintf(__('Added %s'), $column->heading),
'id' => $column->dataSetColumnId,
'data' => $column
]);
return $this->render($request, $response);
}
/**
* Edit Form
* @param Request $request
* @param Response $response
* @param $id
* @param $colId
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function editForm(Request $request, Response $response, $id, $colId)
{
$dataSet = $this->dataSetFactory->getById($id);
if (!$this->getUser()->checkEditable($dataSet)) {
throw new AccessDeniedException();
}
$this->getState()->template = 'dataset-column-form-edit';
$this->getState()->setData([
'dataSet' => $dataSet,
'dataSetColumn' => $this->dataSetColumnFactory->getById($colId),
'dataTypes' => $this->dataTypeFactory->query(),
'dataSetColumnTypes' => $this->dataSetColumnTypeFactory->query(),
]);
return $this->render($request, $response);
}
/**
* Edit
* @param Request $request
* @param Response $response
* @param $id
* @param $colId
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\InvalidArgumentException
* @throws \Xibo\Support\Exception\NotFoundException
* @SWG\Put(
* path="/dataset/{dataSetId}/column/{dataSetColumnId}",
* operationId="dataSetColumnEdit",
* tags={"dataset"},
* summary="Edit Column",
* description="Edit a Column to a DataSet",
* @SWG\Parameter(
* name="dataSetId",
* in="path",
* description="The DataSet ID",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="dataSetColumnId",
* in="path",
* description="The Column ID",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="heading",
* in="formData",
* description="The heading for the Column",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="listContent",
* in="formData",
* description="A comma separated list of content for drop downs",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="columnOrder",
* in="formData",
* description="The display order for this column",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="dataTypeId",
* in="formData",
* description="The data type ID for this column",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="dataSetColumnTypeId",
* in="formData",
* description="The column type for this column",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="formula",
* in="formData",
* description="MySQL SELECT syntax formula for this Column if the column type is formula",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="remoteField",
* in="formData",
* description="JSON-String to select Data from the Remote DataSet",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="showFilter",
* in="formData",
* description="Flag indicating whether this column should present a filter on DataEntry",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="showSort",
* in="formData",
* description="Flag indicating whether this column should allow sorting on DataEntry",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="tooltip",
* in="formData",
* description="Help text that should be displayed when entering data for this Column.",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="isRequired",
* in="formData",
* description="Flag indicating whether value must be provided for this Column.",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="dateFormat",
* in="formData",
* description="PHP date format for the dates in the source of the remote DataSet",
* type="string",
* required=false
* ),
* @SWG\Response(
* response=201,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/DataSetColumn"),
* @SWG\Header(
* header="Location",
* description="Location of the new record",
* type="string"
* )
* )
* )
*/
public function edit(Request $request, Response $response, $id, $colId)
{
$dataSet = $this->dataSetFactory->getById($id);
$sanitizedParams = $this->getSanitizer($request->getParams());
if (!$this->getUser()->checkEditable($dataSet)) {
throw new AccessDeniedException();
}
// Column
$column = $this->dataSetColumnFactory->getById($colId);
$column->heading = $sanitizedParams->getString('heading');
$column->listContent = $sanitizedParams->getString('listContent');
$column->columnOrder = $sanitizedParams->getInt('columnOrder');
$column->dataTypeId = $sanitizedParams->getInt('dataTypeId');
$column->dataSetColumnTypeId = $sanitizedParams->getInt('dataSetColumnTypeId');
$column->formula = $request->getParam('formula', null);
$column->remoteField = $request->getParam('remoteField', null);
$column->showFilter = $sanitizedParams->getCheckbox('showFilter');
$column->showSort = $sanitizedParams->getCheckbox('showSort');
$column->tooltip = $sanitizedParams->getString('tooltip');
$column->isRequired = $sanitizedParams->getCheckbox('isRequired');
$column->dateFormat = $sanitizedParams->getString('dateFormat', ['default' => null]);
// client side formula disable sort
if (substr($column->formula, 0, 1) === '$') {
$column->showSort = 0;
}
$column->save();
if ($column->dataSetColumnTypeId == 3 && $column->hasPropertyChanged('remoteField')) {
$this->pool->deleteItem('/dataset/cache/' . $dataSet->dataSetId);
$this->getLog()->debug('Edited remoteField detected, clear cache for remote dataSet ID ' . $dataSet->dataSetId);
}
$dataSet->notify();
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Edited %s'), $column->heading),
'id' => $column->dataSetColumnId,
'data' => $column
]);
return $this->render($request, $response);
}
/**
* Delete Form
* @param Request $request
* @param Response $response
* @param $id
* @param $colId
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function deleteForm(Request $request, Response $response, $id, $colId)
{
$dataSet = $this->dataSetFactory->getById($id);
if (!$this->getUser()->checkDeleteable($dataSet)) {
throw new AccessDeniedException();
}
$this->getState()->template = 'dataset-column-form-delete';
$this->getState()->setData([
'dataSet' => $dataSet,
'dataSetColumn' => $this->dataSetColumnFactory->getById($colId),
]);
return $this->render($request, $response);
}
/**
* Delete
* @param Request $request
* @param Response $response
* @param $id
* @param $colId
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
* @SWG\Delete(
* path="/dataset/{dataSetId}/column/{dataSetColumnId}",
* operationId="dataSetColumnDelete",
* tags={"dataset"},
* summary="Delete Column",
* description="Delete DataSet Column",
* @SWG\Parameter(
* name="dataSetId",
* in="path",
* description="The DataSet ID",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="dataSetColumnId",
* in="path",
* description="The Column ID",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*/
public function delete(Request $request, Response $response, $id, $colId)
{
$dataSet = $this->dataSetFactory->getById($id);
if (!$this->getUser()->checkDeleteable($dataSet)) {
throw new AccessDeniedException();
}
// Get the column
$column = $this->dataSetColumnFactory->getById($colId);
$column->delete();
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Deleted %s'), $column->heading)
]);
return $this->render($request, $response);
}
}

View File

@@ -0,0 +1,593 @@
<?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\Controller;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Factory\DataSetFactory;
use Xibo\Factory\MediaFactory;
use Xibo\Helper\DateFormatHelper;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
/**
* Class DataSetData
* @package Xibo\Controller
*/
class DataSetData extends Base
{
/** @var DataSetFactory */
private $dataSetFactory;
/** @var MediaFactory */
private $mediaFactory;
/**
* Set common dependencies.
* @param DataSetFactory $dataSetFactory
* @param MediaFactory $mediaFactory
*/
public function __construct($dataSetFactory, $mediaFactory)
{
$this->dataSetFactory = $dataSetFactory;
$this->mediaFactory = $mediaFactory;
}
/**
* Display Page
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function displayPage(Request $request, Response $response, $id)
{
$dataSet = $this->dataSetFactory->getById($id);
if (!$this->getUser()->checkEditable($dataSet)) {
throw new AccessDeniedException();
}
// Load data set
$dataSet->load();
$this->getState()->template = 'dataset-dataentry-page';
$this->getState()->setData([
'dataSet' => $dataSet
]);
return $this->render($request, $response);
}
/**
* Grid
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @SWG\Get(
* path="/dataset/data/{dataSetId}",
* operationId="dataSetData",
* tags={"dataset"},
* summary="DataSet Data",
* description="Get Data for DataSet",
* @SWG\Parameter(
* name="dataSetId",
* in="path",
* description="The DataSet ID",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="successful operation"
* )
* )
*/
public function grid(Request $request, Response $response, $id)
{
$dataSet = $this->dataSetFactory->getById($id);
$sanitizedParams = $this->getSanitizer($request->getParams());
if (!$this->getUser()->checkEditable($dataSet)) {
throw new AccessDeniedException();
}
$sorting = $this->gridRenderSort($sanitizedParams);
if ($sorting != null) {
$sorting = implode(',', $sorting);
}
// Filter criteria
$filter = '';
$params = [];
$i = 0;
foreach ($dataSet->getColumn() as $column) {
/* @var \Xibo\Entity\DataSetColumn $column */
if ($column->dataSetColumnTypeId == 1) {
$i++;
if ($sanitizedParams->getString($column->heading) != null) {
$filter .= 'AND `' . $column->heading . '` LIKE :heading_' . $i . ' ';
$params['heading_' . $i] = '%' . $sanitizedParams->getString($column->heading) . '%';
}
}
}
$filter = trim($filter, 'AND');
// Work out the limits
$filter = $this->gridRenderFilter(['filter' => $request->getParam('filter', $filter)], $sanitizedParams);
try {
$data = $dataSet->getData(
[
'order' => $sorting,
'start' => $filter['start'],
'size' => $filter['length'],
'filter' => $filter['filter']
],
[],
$params,
);
} catch (\Exception $e) {
$data = ['exception' => __('Error getting DataSet data, failed with following message: ') . $e->getMessage()];
$this->getLog()->error('Error getting DataSet data, failed with following message: ' . $e->getMessage());
$this->getLog()->debug($e->getTraceAsString());
}
$this->getState()->template = 'grid';
$this->getState()->setData($data);
// Output the count of records for paging purposes
if ($dataSet->countLast() != 0)
$this->getState()->recordsTotal = $dataSet->countLast();
// Set this dataSet as being active
$dataSet->setActive();
return $this->render($request, $response);
}
/**
* Add Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function addForm(Request $request, Response $response, $id)
{
$dataSet = $this->dataSetFactory->getById($id);
if (!$this->getUser()->checkEditable($dataSet)) {
throw new AccessDeniedException();
}
$dataSet->load();
$this->getState()->template = 'dataset-data-form-add';
$this->getState()->setData([
'dataSet' => $dataSet
]);
return $this->render($request, $response);
}
/**
* Add
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\DuplicateEntityException
* @SWG\Post(
* path="/dataset/data/{dataSetId}",
* operationId="dataSetDataAdd",
* tags={"dataset"},
* summary="Add Row",
* description="Add a row of Data to a DataSet",
* @SWG\Parameter(
* name="dataSetId",
* in="path",
* description="The DataSet ID",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="dataSetColumnId_ID",
* in="formData",
* description="Parameter for each dataSetColumnId in the DataSet",
* type="string",
* required=true
* ),
* @SWG\Response(
* response=201,
* description="successful operation",
* @SWG\Header(
* header="Location",
* description="Location of the new record",
* type="string"
* )
* )
* )
*/
public function add(Request $request, Response $response, $id)
{
$dataSet = $this->dataSetFactory->getById($id);
$sanitizedParams = $this->getSanitizer($request->getParams());
if (!$this->getUser()->checkEditable($dataSet)) {
throw new AccessDeniedException();
}
$row = [];
// Expect input for each value-column
foreach ($dataSet->getColumn() as $column) {
/* @var \Xibo\Entity\DataSetColumn $column */
if ($column->dataSetColumnTypeId == 1) {
// Sanitize accordingly
if ($column->dataTypeId == 2) {
// Number
$value = $sanitizedParams->getDouble('dataSetColumnId_' . $column->dataSetColumnId);
} else if ($column->dataTypeId == 3) {
// Date
$date = $sanitizedParams->getDate('dataSetColumnId_' . $column->dataSetColumnId);
// format only if we have the date provided.
$value = $date === null ? $date : $date->format(DateFormatHelper::getSystemFormat());
} else if ($column->dataTypeId == 5) {
// Media Id
$value = $sanitizedParams->getInt('dataSetColumnId_' . $column->dataSetColumnId);
} else if ($column->dataTypeId === 6) {
// HTML
$value = $sanitizedParams->getHtml('dataSetColumnId_' . $column->dataSetColumnId);
} else {
// String
$value = $sanitizedParams->getString('dataSetColumnId_' . $column->dataSetColumnId);
}
$row[$column->heading] = $value;
} elseif ($column->dataSetColumnTypeId == 3) {
throw new InvalidArgumentException(__('Cannot add new rows to remote dataSet'), 'dataSetColumnTypeId');
}
}
// Use the data set object to add a row
$rowId = $dataSet->addRow($row);
// Save the dataSet
$dataSet->save(['validate' => false, 'saveColumns' => false]);
// Return
$this->getState()->hydrate([
'httpStatus' => 201,
'message' => __('Added Row'),
'id' => $rowId,
'data' => [
'id' => $rowId
]
]);
return $this->render($request, $response);
}
/**
* Edit Form
* @param Request $request
* @param Response $response
* @param $id
* @param $rowId
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function editForm(Request $request, Response $response, $id, $rowId)
{
$dataSet = $this->dataSetFactory->getById($id);
if (!$this->getUser()->checkEditable($dataSet)) {
throw new AccessDeniedException();
}
$dataSet->load();
$row = $dataSet->getData(['id' => $rowId])[0];
// Augment my row with any already selected library image
foreach ($dataSet->getColumn() as $dataSetColumn) {
if ($dataSetColumn->dataTypeId === 5) {
// Add this image object to my row
try {
if (isset($row[$dataSetColumn->heading])) {
$row['__images'][$dataSetColumn->dataSetColumnId] = $this->mediaFactory->getById($row[$dataSetColumn->heading]);
}
} catch (NotFoundException $notFoundException) {
$this->getLog()->debug('DataSet ' . $id . ' references an image that no longer exists. ID is ' . $row[$dataSetColumn->heading]);
}
}
}
$this->getState()->template = 'dataset-data-form-edit';
$this->getState()->setData([
'dataSet' => $dataSet,
'row' => $row
]);
return $this->render($request, $response);
}
/**
* Edit Row
* @param Request $request
* @param Response $response
* @param $id
* @param int $rowId
*
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\DuplicateEntityException
* @SWG\Put(
* path="/dataset/data/{dataSetId}/{rowId}",
* operationId="dataSetDataEdit",
* tags={"dataset"},
* summary="Edit Row",
* description="Edit a row of Data to a DataSet",
* @SWG\Parameter(
* name="dataSetId",
* in="path",
* description="The DataSet ID",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="rowId",
* in="path",
* description="The Row ID of the Data to Edit",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="dataSetColumnId_ID",
* in="formData",
* description="Parameter for each dataSetColumnId in the DataSet",
* type="string",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="successful operation"
* )
* )
*/
public function edit(Request $request, Response $response, $id, $rowId)
{
$dataSet = $this->dataSetFactory->getById($id);
$sanitizedParams = $this->getSanitizer($request->getParams());
if (!$this->getUser()->checkEditable($dataSet)) {
throw new AccessDeniedException();
}
$existingRow = $dataSet->getData(['id' => $rowId])[0];
$row = [];
// Expect input for each value-column
foreach ($dataSet->getColumn() as $column) {
$existingValue = $existingRow[$column->heading];
/* @var \Xibo\Entity\DataSetColumn $column */
if ($column->dataSetColumnTypeId == 1) {
// Pull out the value
$value = $request->getParam('dataSetColumnId_' . $column->dataSetColumnId, null);
$this->getLog()->debug('Value is: ' . var_export($value, true)
. ', existing value is ' . var_export($existingValue, true));
// Sanitize accordingly
if ($column->dataTypeId == 2) {
// Number
if (isset($value)) {
$value = $sanitizedParams->getDouble('dataSetColumnId_' . $column->dataSetColumnId);
} else {
$value = $existingValue;
}
} else if ($column->dataTypeId == 3) {
// Date
if (isset($value)) {
$value = $sanitizedParams->getDate('dataSetColumnId_' . $column->dataSetColumnId);
} else {
$value = $existingValue;
}
} else if ($column->dataTypeId == 5) {
// Media Id
if (isset($value)) {
$value = $sanitizedParams->getInt('dataSetColumnId_' . $column->dataSetColumnId);
} else {
$value = null;
}
} else if ($column->dataTypeId === 6) {
// HTML
if (isset($value)) {
$value = $sanitizedParams->getHtml('dataSetColumnId_' . $column->dataSetColumnId);
} else {
$value = null;
}
} else {
// String
if (isset($value)) {
$value = $sanitizedParams->getString('dataSetColumnId_' . $column->dataSetColumnId);
} else {
$value = $existingValue;
}
}
$row[$column->heading] = $value;
}
}
// Use the data set object to edit a row
if ($row != []) {
$dataSet->editRow($rowId, $row);
} else {
throw new InvalidArgumentException(__('Cannot edit data of remote columns'), 'dataSetColumnTypeId');
}
// Save the dataSet
$dataSet->save(['validate' => false, 'saveColumns' => false]);
// Return
$this->getState()->hydrate([
'message' => __('Edited Row'),
'id' => $rowId,
'data' => [
'id' => $rowId
]
]);
return $this->render($request, $response);
}
/**
* Delete Form
* @param Request $request
* @param Response $response
* @param $id
* @param int $rowId
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function deleteForm(Request $request, Response $response, $id, $rowId)
{
$dataSet = $this->dataSetFactory->getById($id);
if (!$this->getUser()->checkEditable($dataSet)) {
throw new AccessDeniedException();
}
$dataSet->load();
$this->getState()->template = 'dataset-data-form-delete';
$this->getState()->setData([
'dataSet' => $dataSet,
'row' => $dataSet->getData(['id' => $rowId])[0]
]);
return $this->render($request, $response);
}
/**
* Delete Row
* @param Request $request
* @param Response $response
* @param $id
* @param $rowId
*
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\DuplicateEntityException
* @SWG\Delete(
* path="/dataset/data/{dataSetId}/{rowId}",
* operationId="dataSetDataDelete",
* tags={"dataset"},
* summary="Delete Row",
* description="Delete a row of Data to a DataSet",
* @SWG\Parameter(
* name="dataSetId",
* in="path",
* description="The DataSet ID",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="rowId",
* in="path",
* description="The Row ID of the Data to Delete",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*/
public function delete(Request $request, Response $response, $id, $rowId)
{
$dataSet = $this->dataSetFactory->getById($id);
if (!$this->getUser()->checkEditable($dataSet)) {
throw new AccessDeniedException();
}
if (empty($dataSet->getData(['id' => $rowId])[0])) {
throw new NotFoundException(__('row not found'), 'dataset');
}
// Delete the row
$dataSet->deleteRow($rowId);
// Save the dataSet
$dataSet->save(['validate' => false, 'saveColumns' => false]);
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => __('Deleted Row'),
'id' => $rowId
]);
return $this->render($request, $response);
}
}

View File

@@ -0,0 +1,913 @@
<?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\Controller;
use Carbon\Carbon;
use Carbon\Exceptions\InvalidDateException;
use PicoFeed\Syndication\Rss20FeedBuilder;
use PicoFeed\Syndication\Rss20ItemBuilder;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Stash\Interfaces\PoolInterface;
use Xibo\Factory\DataSetColumnFactory;
use Xibo\Factory\DataSetFactory;
use Xibo\Factory\DataSetRssFactory;
use Xibo\Helper\DateFormatHelper;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
class DataSetRss extends Base
{
/** @var DataSetRssFactory */
private $dataSetRssFactory;
/** @var DataSetFactory */
private $dataSetFactory;
/** @var DataSetColumnFactory */
private $dataSetColumnFactory;
/** @var PoolInterface */
private $pool;
/** @var StorageServiceInterface */
private $store;
/**
* Set common dependencies.
* @param DataSetRssFactory $dataSetRssFactory
* @param DataSetFactory $dataSetFactory
* @param DataSetColumnFactory $dataSetColumnFactory
* @param PoolInterface $pool
* @param StorageServiceInterface $store
*/
public function __construct($dataSetRssFactory, $dataSetFactory, $dataSetColumnFactory, $pool, $store)
{
$this->dataSetRssFactory = $dataSetRssFactory;
$this->dataSetFactory = $dataSetFactory;
$this->dataSetColumnFactory = $dataSetColumnFactory;
$this->pool = $pool;
$this->store = $store;
}
/**
* Display Page
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function displayPage(Request $request, Response $response, $id)
{
$dataSet = $this->dataSetFactory->getById($id);
if (!$this->getUser()->checkEditable($dataSet)) {
throw new AccessDeniedException();
}
$this->getState()->template = 'dataset-rss-page';
$this->getState()->setData([
'dataSet' => $dataSet
]);
return $this->render($request, $response);
}
/**
* Search
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @SWG\Get(
* path="/dataset/{dataSetId}/rss",
* operationId="dataSetRSSSearch",
* tags={"dataset"},
* summary="Search RSSs",
* description="Search RSSs for DataSet",
* @SWG\Parameter(
* name="dataSetId",
* in="path",
* description="The DataSet ID",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="#/definitions/DataSetRss")
* )
* )
* )
*/
public function grid(Request $request, Response $response, $id)
{
$dataSet = $this->dataSetFactory->getById($id);
$sanitizedParams = $this->getSanitizer($request->getQueryParams());
if (!$this->getUser()->checkEditable($dataSet)) {
throw new AccessDeniedException();
}
$feeds = $this->dataSetRssFactory->query($this->gridRenderSort($sanitizedParams), $this->gridRenderFilter([
'dataSetId' => $id,
'useRegexForName' => $sanitizedParams->getCheckbox('useRegexForName')
], $sanitizedParams));
foreach ($feeds as $feed) {
if ($this->isApi($request))
continue;
$feed->includeProperty('buttons');
if ($this->getUser()->featureEnabled('dataset.data')) {
// Edit
$feed->buttons[] = array(
'id' => 'datasetrss_button_edit',
'url' => $this->urlFor($request,'dataSet.rss.edit.form', ['id' => $id, 'rssId' => $feed->id]),
'text' => __('Edit')
);
if ($this->getUser()->checkDeleteable($dataSet)) {
// Delete
$feed->buttons[] = array(
'id' => 'datasetrss_button_delete',
'url' => $this->urlFor($request,'dataSet.rss.delete.form', ['id' => $id, 'rssId' => $feed->id]),
'text' => __('Delete')
);
}
}
}
$this->getState()->template = 'grid';
$this->getState()->setData($feeds);
return $this->render($request, $response);
}
/**
* Add form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function addForm(Request $request, Response $response, $id)
{
$dataSet = $this->dataSetFactory->getById($id);
if (!$this->getUser()->checkEditable($dataSet)) {
throw new AccessDeniedException();
}
$columns = $dataSet->getColumn();
$dateColumns = [];
foreach ($columns as $column) {
if ($column->dataTypeId === 3)
$dateColumns[] = $column;
}
$this->getState()->template = 'dataset-rss-form-add';
$this->getState()->setData([
'dataSet' => $dataSet,
'extra' => [
'orderClauses' => [],
'filterClauses' => [],
'columns' => $columns,
'dateColumns' => $dateColumns
]
]);
return $this->render($request, $response);
}
/**
* Add
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @SWG\Post(
* path="/dataset/{dataSetId}/rss",
* operationId="dataSetRssAdd",
* tags={"dataset"},
* summary="Add RSS",
* description="Add a RSS to a DataSet",
* @SWG\Parameter(
* name="dataSetId",
* in="path",
* description="The DataSet ID",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="title",
* in="formData",
* description="The title for the RSS",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="title",
* in="formData",
* description="The author for the RSS",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="summaryColumnId",
* in="formData",
* description="The columnId to be used as each item summary",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="contentColumnId",
* in="formData",
* description="The columnId to be used as each item content",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="publishedDateColumnId",
* in="formData",
* description="The columnId to be used as each item published date",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=201,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/DataSetRss"),
* @SWG\Header(
* header="Location",
* description="Location of the new record",
* type="string"
* )
* )
* )
*/
public function add(Request $request, Response $response, $id)
{
$dataSet = $this->dataSetFactory->getById($id);
$sanitizedParams = $this->getSanitizer($request->getParams());
if (!$this->getUser()->checkEditable($dataSet)) {
throw new AccessDeniedException();
}
if ($sanitizedParams->getString('title') == '') {
throw new InvalidArgumentException(__('Please enter title'), 'title');
}
if ($sanitizedParams->getString('author') == '') {
throw new InvalidArgumentException(__('Please enter author name'), 'author');
}
// Create RSS
$feed = $this->dataSetRssFactory->createEmpty();
$feed->dataSetId = $id;
$feed->title = $sanitizedParams->getString('title');
$feed->author = $sanitizedParams->getString('author');
$feed->titleColumnId = $sanitizedParams->getInt('titleColumnId');
$feed->summaryColumnId = $sanitizedParams->getInt('summaryColumnId');
$feed->contentColumnId = $sanitizedParams->getInt('contentColumnId');
$feed->publishedDateColumnId = $sanitizedParams->getInt('publishedDateColumnId');
$this->handleFormFilterAndOrder($request, $response, $feed);
// New feed needs a PSK
$feed->setNewPsk();
// Save
$feed->save();
// Return
$this->getState()->hydrate([
'httpStatus' => 201,
'message' => sprintf(__('Added %s'), $feed->title),
'id' => $feed->id,
'data' => $feed
]);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @param \Xibo\Entity\DataSetRss $feed
*/
private function handleFormFilterAndOrder(Request $request, Response $response, $feed)
{
$sanitizedParams = $this->getSanitizer($request->getParams());
// Order criteria
$orderClauses = $sanitizedParams->getArray('orderClause');
$orderClauseDirections = $sanitizedParams->getArray('orderClauseDirection');
$orderClauseMapping = [];
$i = -1;
foreach ($orderClauses as $orderClause) {
$i++;
if ($orderClause == '')
continue;
// Map the stop code received to the stop ref (if there is one)
$orderClauseMapping[] = [
'orderClause' => $orderClause,
'orderClauseDirection' => isset($orderClauseDirections[$i]) ? $orderClauseDirections[$i] : '',
];
}
$feed->sort = json_encode([
'sort' => $sanitizedParams->getString('sort'),
'useOrderingClause' => $sanitizedParams->getCheckbox('useOrderingClause'),
'orderClauses' => $orderClauseMapping
]);
// Filter criteria
$filterClauses = $sanitizedParams->getArray('filterClause');
$filterClauseOperator = $sanitizedParams->getArray('filterClauseOperator');
$filterClauseCriteria = $sanitizedParams->getArray('filterClauseCriteria');
$filterClauseValue = $sanitizedParams->getArray('filterClauseValue');
$filterClauseMapping = [];
$i = -1;
foreach ($filterClauses as $filterClause) {
$i++;
if ($filterClause == '')
continue;
// Map the stop code received to the stop ref (if there is one)
$filterClauseMapping[] = [
'filterClause' => $filterClause,
'filterClauseOperator' => isset($filterClauseOperator[$i]) ? $filterClauseOperator[$i] : '',
'filterClauseCriteria' => isset($filterClauseCriteria[$i]) ? $filterClauseCriteria[$i] : '',
'filterClauseValue' => isset($filterClauseValue[$i]) ? $filterClauseValue[$i] : '',
];
}
$feed->filter = json_encode([
'filter' => $sanitizedParams->getString('filter'),
'useFilteringClause' => $sanitizedParams->getCheckbox('useFilteringClause'),
'filterClauses' => $filterClauseMapping
]);
}
/**
* Edit Form
* @param Request $request
* @param Response $response
* @param $id
* @param $rssId
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function editForm(Request $request, Response $response, $id, $rssId)
{
$dataSet = $this->dataSetFactory->getById($id);
if (!$this->getUser()->checkEditable($dataSet)) {
throw new AccessDeniedException();
}
$feed = $this->dataSetRssFactory->getById($rssId);
$columns = $dataSet->getColumn();
$dateColumns = [];
foreach ($columns as $column) {
if ($column->dataTypeId === 3)
$dateColumns[] = $column;
}
$this->getState()->template = 'dataset-rss-form-edit';
$this->getState()->setData([
'dataSet' => $dataSet,
'feed' => $feed,
'extra' => array_merge($feed->getSort(), $feed->getFilter(), ['columns' => $columns, 'dateColumns' => $dateColumns])
]);
return $this->render($request, $response);
}
/**
* Edit
* @param Request $request
* @param Response $response
* @param $id
* @param $rssId
*
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @SWG\Put(
* path="/dataset/{dataSetId}/rss/{rssId}",
* operationId="dataSetRssEdit",
* tags={"dataset"},
* summary="Edit Rss",
* description="Edit DataSet Rss Feed",
* @SWG\Parameter(
* name="dataSetId",
* in="path",
* description="The DataSet ID",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="rssId",
* in="path",
* description="The RSS ID",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="title",
* in="formData",
* description="The title for the RSS",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="title",
* in="formData",
* description="The author for the RSS",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="summaryColumnId",
* in="formData",
* description="The rssId to be used as each item summary",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="contentColumnId",
* in="formData",
* description="The columnId to be used as each item content",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="publishedDateColumnId",
* in="formData",
* description="The columnId to be used as each item published date",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="regeneratePsk",
* in="formData",
* description="Regenerate the PSK?",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*/
public function edit(Request $request, Response $response, $id, $rssId)
{
$dataSet = $this->dataSetFactory->getById($id);
$sanitizedParams = $this->getSanitizer($request->getParams());
if (!$this->getUser()->checkEditable($dataSet)) {
throw new AccessDeniedException();
}
if ($sanitizedParams->getString('title') == '') {
throw new InvalidArgumentException(__('Please enter title'), 'title');
}
if ($sanitizedParams->getString('author') == '') {
throw new InvalidArgumentException(__('Please enter author name'), 'author');
}
$feed = $this->dataSetRssFactory->getById($rssId);
$feed->title = $sanitizedParams->getString('title');
$feed->author = $sanitizedParams->getString('author');
$feed->titleColumnId = $sanitizedParams->getInt('titleColumnId');
$feed->summaryColumnId = $sanitizedParams->getInt('summaryColumnId');
$feed->contentColumnId = $sanitizedParams->getInt('contentColumnId');
$feed->publishedDateColumnId = $sanitizedParams->getInt('publishedDateColumnId');
$this->handleFormFilterAndOrder($request, $response, $feed);
if ($sanitizedParams->getCheckbox('regeneratePsk')) {
$feed->setNewPsk();
}
$feed->save();
// Delete from the cache
$this->pool->deleteItem('/dataset/rss/' . $feed->id);
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Edited %s'), $feed->title),
'id' => $feed->id,
'data' => $feed
]);
return $this->render($request, $response);
}
/**
* Delete Form
* @param Request $request
* @param Response $response
* @param $id
* @param $rssId
*
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function deleteForm(Request $request, Response $response, $id, $rssId)
{
$dataSet = $this->dataSetFactory->getById($id);
if (!$this->getUser()->checkDeleteable($dataSet)) {
throw new AccessDeniedException();
}
$feed = $this->dataSetRssFactory->getById($rssId);
$this->getState()->template = 'dataset-rss-form-delete';
$this->getState()->setData([
'dataSet' => $dataSet,
'feed' => $feed
]);
return $this->render($request, $response);
}
/**
* Delete
* @param Request $request
* @param Response $response
* @param $id
* @param $rssId
*
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @SWG\Delete(
* path="/dataset/{dataSetId}/rss/{rssId}",
* operationId="dataSetRSSDelete",
* tags={"dataset"},
* summary="Delete RSS",
* description="Delete DataSet RSS",
* @SWG\Parameter(
* name="dataSetId",
* in="path",
* description="The DataSet ID",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="rssId",
* in="path",
* description="The RSS ID",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*/
public function delete(Request $request, Response $response, $id, $rssId)
{
$dataSet = $this->dataSetFactory->getById($id);
if (!$this->getUser()->checkDeleteable($dataSet)) {
throw new AccessDeniedException();
}
$feed = $this->dataSetRssFactory->getById($rssId);
$feed->delete();
// Delete from the cache
$this->pool->deleteItem('/dataset/rss/' . $feed->id);
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Deleted %s'), $feed->title)
]);
return $this->render($request, $response);
}
/**
* Output feed
* this is a public route (no authentication requried)
* @param Request $request
* @param Response $response
* @param $psk
* @throws \Exception
*/
public function feed(Request $request, Response $response, $psk)
{
$this->setNoOutput();
$this->getLog()->debug('RSS Feed Request with PSK ' . $psk);
// Try and get the feed using the PSK
try {
$feed = $this->dataSetRssFactory->getByPsk($psk);
// Get the DataSet out
$dataSet = $this->dataSetFactory->getById($feed->dataSetId);
// What is the edit date of this data set
$dataSetEditDate = ($dataSet->lastDataEdit == 0)
? Carbon::now()->subMonths(2)
: Carbon::createFromTimestamp($dataSet->lastDataEdit);
// Do we have this feed in the cache?
$cache = $this->pool->getItem('/dataset/rss/' . $feed->id);
$output = $cache->get();
if ($cache->isMiss() || $cache->getCreation() < $dataSetEditDate) {
// We need to recache
$this->getLog()->debug('Generating RSS feed and saving to cache. Created on '
. ($cache->getCreation()
? $cache->getCreation()->format(DateFormatHelper::getSystemFormat())
: 'never'));
$output = $this->generateFeed($feed, $dataSetEditDate, $dataSet);
$cache->set($output);
$cache->expiresAfter(new \DateInterval('PT5M'));
$this->pool->saveDeferred($cache);
} else {
$this->getLog()->debug('Serving from Cache');
}
$response->withHeader('Content-Type', 'application/rss+xml');
echo $output;
} catch (NotFoundException) {
$this->getState()->httpStatus = 404;
}
return $response;
}
/**
* @param \Xibo\Entity\DataSetRss $feed
* @param Carbon $dataSetEditDate
* @param \Xibo\Entity\DataSet $dataSet
* @return string
* @throws \Xibo\Support\Exception\NotFoundException
*/
private function generateFeed($feed, $dataSetEditDate, $dataSet): string
{
// Create the start of our feed, its description, etc.
$builder = Rss20FeedBuilder::create()
->withTitle($feed->title)
->withAuthor($feed->author)
->withFeedUrl('')
->withSiteUrl('')
->withDate($dataSetEditDate);
$sort = $feed->getSort();
$filter = $feed->getFilter();
// Get results, using the filter criteria
// Ordering
$ordering = '';
if ($sort['useOrderingClause'] == 1) {
$ordering = $sort['sort'];
} else {
// Build an order string
foreach ($sort['orderClauses'] as $clause) {
$ordering .= $clause['orderClause'] . ' ' . $clause['orderClauseDirection'] . ',';
}
$ordering = rtrim($ordering, ',');
}
// Filtering
$filtering = '';
if ($filter['useFilteringClause'] == 1) {
$filtering = $filter['filter'];
} else {
// Build
$i = 0;
foreach ($filter['filterClauses'] as $clause) {
$i++;
$criteria = '';
switch ($clause['filterClauseCriteria']) {
case 'starts-with':
$criteria = 'LIKE \'' . $clause['filterClauseValue'] . '%\'';
break;
case 'ends-with':
$criteria = 'LIKE \'%' . $clause['filterClauseValue'] . '\'';
break;
case 'contains':
$criteria = 'LIKE \'%' . $clause['filterClauseValue'] . '%\'';
break;
case 'equals':
$criteria = '= \'' . $clause['filterClauseValue'] . '\'';
break;
case 'not-contains':
$criteria = 'NOT LIKE \'%' . $clause['filterClauseValue'] . '%\'';
break;
case 'not-starts-with':
$criteria = 'NOT LIKE \'' . $clause['filterClauseValue'] . '%\'';
break;
case 'not-ends-with':
$criteria = 'NOT LIKE \'%' . $clause['filterClauseValue'] . '\'';
break;
case 'not-equals':
$criteria = '<> \'' . $clause['filterClauseValue'] . '\'';
break;
case 'greater-than':
$criteria = '> \'' . $clause['filterClauseValue'] . '\'';
break;
case 'less-than':
$criteria = '< \'' . $clause['filterClauseValue'] . '\'';
break;
default:
// Continue out of the switch and the loop (this takes us back to our foreach)
continue 2;
}
if ($i > 1)
$filtering .= ' ' . $clause['filterClauseOperator'] . ' ';
// Ability to filter by not-empty and empty
if ($clause['filterClauseCriteria'] == 'is-empty') {
$filtering .= 'IFNULL(`' . $clause['filterClause'] . '`, \'\') = \'\'';
} else if ($clause['filterClauseCriteria'] == 'is-not-empty') {
$filtering .= 'IFNULL(`' . $clause['filterClause'] . '`, \'\') <> \'\'';
} else {
$filtering .= $clause['filterClause'] . ' ' . $criteria;
}
}
}
// Get an array representing the id->heading mappings
$mappings = [];
$columns = [];
if ($feed->titleColumnId != 0)
$columns[] = $feed->titleColumnId;
if ($feed->summaryColumnId != 0)
$columns[] = $feed->summaryColumnId;
if ($feed->contentColumnId != 0)
$columns[] = $feed->contentColumnId;
if ($feed->publishedDateColumnId != 0)
$columns[] = $feed->publishedDateColumnId;
foreach ($columns as $dataSetColumnId) {
// Get the column definition this represents
$column = $dataSet->getColumn($dataSetColumnId);
/* @var \Xibo\Entity\DataSetColumn $column */
$mappings[$column->heading] = [
'dataSetColumnId' => $dataSetColumnId,
'heading' => $column->heading,
'dataTypeId' => $column->dataTypeId
];
}
$filter = [
'filter' => $filtering,
'order' => $ordering
];
// Set the timezone for SQL
$dateNow = Carbon::now();
$this->store->setTimeZone($dateNow->format('P'));
// Get the data (complete table, filtered)
$dataSetResults = $dataSet->getData($filter);
foreach ($dataSetResults as $row) {
$item = Rss20ItemBuilder::create($builder);
$item->withUrl('');
$hasContent = false;
$hasDate = false;
// Go through the columns of each row
foreach ($row as $key => $value) {
// Is this one of the columns we're interested in?
if (isset($mappings[$key])) {
// Yes it is - which one?
$hasContent = true;
if ($mappings[$key]['dataSetColumnId'] === $feed->titleColumnId) {
$item->withTitle($value);
} else if ($mappings[$key]['dataSetColumnId'] === $feed->summaryColumnId) {
$item->withSummary($value);
} else if ($mappings[$key]['dataSetColumnId'] === $feed->contentColumnId) {
$item->withContent($value);
} else if ($mappings[$key]['dataSetColumnId'] === $feed->publishedDateColumnId) {
try {
$date = Carbon::createFromTimestamp($value);
} catch (InvalidDateException) {
$date = $dataSetEditDate;
}
if ($date !== null) {
$item->withPublishedDate($date);
$hasDate = true;
}
}
}
}
if (!$hasDate) {
$item->withPublishedDate($dataSetEditDate);
}
if ($hasContent) {
$builder->withItem($item);
}
}
// Found, do things
return $builder->build();
}
}

View File

@@ -0,0 +1,100 @@
<?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\Controller;
use Xibo\Support\Sanitizer\SanitizerInterface;
/**
* Trait DataTablesDotNetTrait
* @package Xibo\Controller
*
* Methods which implement the particular sorting/filtering requirements of DataTables.Net
*/
trait DataTablesDotNetTrait
{
/**
* Set the filter
* @param array $extraFilter
* @param SanitizerInterface|null $sanitizedRequestParams
* @return array
*/
protected function gridRenderFilter(array $extraFilter, $sanitizedRequestParams = null)
{
if ($sanitizedRequestParams === null) {
return $extraFilter;
}
// Handle filtering
$filter = [];
if ($sanitizedRequestParams->getInt('disablePaging') != 1) {
$filter['start'] = $sanitizedRequestParams->getInt('start', ['default' => 0]);
$filter['length'] = $sanitizedRequestParams->getInt('length', ['default' => 10]);
}
$search = $sanitizedRequestParams->getArray('search', ['default' => []]);
if (is_array($search) && isset($search['value'])) {
$filter['search'] = $search['value'];
} else if ($search != '') {
$filter['search'] = $search;
}
// Merge with any extra filter items that have been provided
$filter = array_merge($extraFilter, $filter);
return $filter;
}
/**
* Set the sort order
* @param SanitizerInterface|array $sanitizedRequestParams
* @return array
*/
protected function gridRenderSort($sanitizedRequestParams)
{
if ($sanitizedRequestParams instanceof SanitizerInterface) {
$columns = $sanitizedRequestParams->getArray('columns');
$order = $sanitizedRequestParams->getArray('order');
} else {
$columns = $sanitizedRequestParams['columns'] ?? null;
$order = $sanitizedRequestParams['order'] ?? null;
}
if ($columns === null
|| !is_array($columns)
|| count($columns) <= 0
|| $order === null
|| !is_array($order)
|| count($order) <= 0
) {
return null;
}
return array_map(function ($element) use ($columns) {
$val = (isset($columns[$element['column']]['name']) && $columns[$element['column']]['name'] != '')
? $columns[$element['column']]['name']
: $columns[$element['column']]['data'];
$val = preg_replace('/[^A-Za-z0-9_]/', '', $val);
return '`' . $val . '`' . (($element['dir'] == 'desc') ? ' DESC' : '');
}, $order);
}
}

621
lib/Controller/DayPart.php Normal file
View File

@@ -0,0 +1,621 @@
<?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\Controller;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Factory\DayPartFactory;
use Xibo\Factory\ScheduleFactory;
use Xibo\Service\DisplayNotifyServiceInterface;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\InvalidArgumentException;
/**
* Class DayPart
* @package Xibo\Controller
*/
class DayPart extends Base
{
/** @var DayPartFactory */
private $dayPartFactory;
/** @var ScheduleFactory */
private $scheduleFactory;
/** @var DisplayNotifyServiceInterface */
private $displayNotifyService;
/**
* Set common dependencies.
* @param DayPartFactory $dayPartFactory
* @param ScheduleFactory $scheduleFactory
* @param \Xibo\Service\DisplayNotifyServiceInterface $displayNotifyService
*/
public function __construct($dayPartFactory, $scheduleFactory, DisplayNotifyServiceInterface $displayNotifyService)
{
$this->dayPartFactory = $dayPartFactory;
$this->scheduleFactory = $scheduleFactory;
$this->displayNotifyService = $displayNotifyService;
}
/**
* View Route
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function displayPage(Request $request, Response $response)
{
$this->getState()->template = 'daypart-page';
return $this->render($request, $response);
}
/**
* Search
*
* @SWG\Get(
* path="/daypart",
* operationId="dayPartSearch",
* tags={"dayPart"},
* summary="Daypart Search",
* description="Search dayparts",
* @SWG\Parameter(
* name="dayPartId",
* in="query",
* description="The dayPart ID to Search",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="name",
* in="query",
* description="The name of the dayPart to Search",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="embed",
* in="query",
* description="Embed related data such as exceptions",
* type="string",
* required=false
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="#/definitions/DayPart")
* )
* )
* )
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function grid(Request $request, Response $response)
{
$sanitizedParams = $this->getSanitizer($request->getQueryParams());
$filter = [
'dayPartId' => $sanitizedParams->getInt('dayPartId'),
'name' => $sanitizedParams->getString('name'),
'useRegexForName' => $sanitizedParams->getCheckbox('useRegexForName'),
'isAlways' => $sanitizedParams->getInt('isAlways'),
'isCustom' => $sanitizedParams->getInt('isCustom'),
'isRetired' => $sanitizedParams->getInt('isRetired')
];
$dayParts = $this->dayPartFactory->query($this->gridRenderSort($sanitizedParams), $this->gridRenderFilter($filter, $sanitizedParams));
$embed = ($sanitizedParams->getString('embed') != null) ? explode(',', $sanitizedParams->getString('embed')) : [];
foreach ($dayParts as $dayPart) {
/* @var \Xibo\Entity\DayPart $dayPart */
if (!in_array('exceptions', $embed)){
$dayPart->excludeProperty('exceptions');
}
if ($this->isApi($request))
continue;
$dayPart->includeProperty('buttons');
if ($dayPart->isCustom !== 1
&& $dayPart->isAlways !== 1
&& $this->getUser()->featureEnabled('daypart.modify')
) {
// CRUD
$dayPart->buttons[] = array(
'id' => 'daypart_button_edit',
'url' => $this->urlFor($request,'daypart.edit.form', ['id' => $dayPart->dayPartId]),
'text' => __('Edit')
);
if ($this->getUser()->checkDeleteable($dayPart)) {
$dayPart->buttons[] = [
'id' => 'daypart_button_delete',
'url' => $this->urlFor($request,'daypart.delete.form', ['id' => $dayPart->dayPartId]),
'text' => __('Delete'),
'multi-select' => true,
'dataAttributes' => [
['name' => 'commit-url', 'value' => $this->urlFor($request,'daypart.delete', ['id' => $dayPart->dayPartId])],
['name' => 'commit-method', 'value' => 'delete'],
['name' => 'id', 'value' => 'daypart_button_delete'],
['name' => 'text', 'value' => __('Delete')],
['name' => 'sort-group', 'value' => 1],
['name' => 'rowtitle', 'value' => $dayPart->name]
]
];
}
}
if ($this->getUser()->checkPermissionsModifyable($dayPart)
&& $this->getUser()->featureEnabled('daypart.modify')
) {
if (count($dayPart->buttons) > 0)
$dayPart->buttons[] = ['divider' => true];
// Edit Permissions
$dayPart->buttons[] = [
'id' => 'daypart_button_permissions',
'url' => $this->urlFor($request,'user.permissions.form', ['entity' => 'DayPart', 'id' => $dayPart->dayPartId]),
'text' => __('Share'),
'multi-select' => true,
'dataAttributes' => [
['name' => 'commit-url', 'value' => $this->urlFor($request,'user.permissions.multi', ['entity' => 'DayPart', 'id' => $dayPart->dayPartId])],
['name' => 'commit-method', 'value' => 'post'],
['name' => 'id', 'value' => 'daypart_button_permissions'],
['name' => 'text', 'value' => __('Share')],
['name' => 'rowtitle', 'value' => $dayPart->name],
['name' => 'sort-group', 'value' => 2],
['name' => 'custom-handler', 'value' => 'XiboMultiSelectPermissionsFormOpen'],
['name' => 'custom-handler-url', 'value' => $this->urlFor($request,'user.permissions.multi.form', ['entity' => 'DayPart'])],
['name' => 'content-id-name', 'value' => 'dayPartId']
]
];
}
}
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = $this->dayPartFactory->countLast();
$this->getState()->setData($dayParts);
return $this->render($request, $response);
}
/**
* Add Daypart Form
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function addForm(Request $request, Response $response)
{
$this->getState()->template = 'daypart-form-add';
$this->getState()->setData([
'extra' => [
'exceptions' => []
]
]);
return $this->render($request, $response);
}
/**
* Edit Daypart
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function editForm(Request $request, Response $response, $id)
{
$dayPart = $this->dayPartFactory->getById($id);
if (!$this->getUser()->checkEditable($dayPart)) {
throw new AccessDeniedException();
}
if ($dayPart->isAlways === 1 || $dayPart->isCustom === 1) {
throw new AccessDeniedException();
}
$this->getState()->template = 'daypart-form-edit';
$this->getState()->setData([
'dayPart' => $dayPart,
'extra' => [
'exceptions' => $dayPart->exceptions
]
]);
return $this->render($request, $response);
}
/**
* Delete Daypart
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function deleteForm(Request $request, Response $response, $id)
{
$dayPart = $this->dayPartFactory->getById($id);
if (!$this->getUser()->checkDeleteable($dayPart)) {
throw new AccessDeniedException();
}
if ($dayPart->isAlways === 1 || $dayPart->isCustom === 1) {
throw new AccessDeniedException();
}
// Get a count of schedules for this day part
$schedules = $this->scheduleFactory->getByDayPartId($id);
$this->getState()->template = 'daypart-form-delete';
$this->getState()->setData([
'countSchedules' => count($schedules),
'dayPart' => $dayPart
]);
return $this->render($request, $response);
}
/**
* Add
* @SWG\Post(
* path="/daypart",
* operationId="dayPartAdd",
* tags={"dayPart"},
* summary="Daypart Add",
* description="Add a Daypart",
* @SWG\Parameter(
* name="name",
* in="formData",
* description="The Daypart Name",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="description",
* in="formData",
* description="A description for the dayPart",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="startTime",
* in="formData",
* description="The start time for this day part",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="endTime",
* in="formData",
* description="The end time for this day part",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="exceptionDays",
* in="formData",
* description="String array of exception days",
* type="array",
* required=false,
* @SWG\Items(type="string")
* ),
* @SWG\Parameter(
* name="exceptionStartTimes",
* in="formData",
* description="String array of exception start times to match the exception days",
* type="array",
* required=false,
* @SWG\Items(type="string")
* ),
* @SWG\Parameter(
* name="exceptionEndTimes",
* in="formData",
* description="String array of exception end times to match the exception days",
* type="array",
* required=false,
* @SWG\Items(type="string")
* ),
* @SWG\Response(
* response=201,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/DayPart"),
* @SWG\Header(
* header="Location",
* description="Location of the new record",
* type="string"
* )
* )
* )
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\InvalidArgumentException
*/
public function add(Request $request, Response $response)
{
$dayPart = $this->dayPartFactory->createEmpty();
$this->handleCommonInputs($dayPart, $request);
$dayPart
->setScheduleFactory($this->scheduleFactory, $this->displayNotifyService)
->save();
// Return
$this->getState()->hydrate([
'httpStatus' => 201,
'message' => sprintf(__('Added %s'), $dayPart->name),
'id' => $dayPart->dayPartId,
'data' => $dayPart
]);
return $this->render($request, $response);
}
/**
* Edit
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\InvalidArgumentException
* @throws \Xibo\Support\Exception\NotFoundException
* @SWG\Put(
* path="/daypart/{dayPartId}",
* operationId="dayPartEdit",
* tags={"dayPart"},
* summary="Daypart Edit",
* description="Edit a Daypart",
* @SWG\Parameter(
* name="dayPartId",
* in="path",
* description="The Daypart Id",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="name",
* in="formData",
* description="The Daypart Name",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="description",
* in="formData",
* description="A description for the dayPart",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="startTime",
* in="formData",
* description="The start time for this day part",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="endTime",
* in="formData",
* description="The end time for this day part",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="exceptionDays",
* in="formData",
* description="String array of exception days",
* type="array",
* required=false,
* @SWG\Items(type="string")
* ),
* @SWG\Parameter(
* name="exceptionStartTimes",
* in="formData",
* description="String array of exception start times to match the exception days",
* type="array",
* required=false,
* @SWG\Items(type="string")
* ),
* @SWG\Parameter(
* name="exceptionEndTimes",
* in="formData",
* description="String array of exception end times to match the exception days",
* type="array",
* required=false,
* @SWG\Items(type="string")
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/DayPart")
* )
* )
*/
public function edit(Request $request, Response $response, $id)
{
$dayPart = $this->dayPartFactory->getById($id)
->load();
if (!$this->getUser()->checkEditable($dayPart)) {
throw new AccessDeniedException();
}
if ($dayPart->isAlways === 1 || $dayPart->isCustom === 1) {
throw new AccessDeniedException();
}
$this->handleCommonInputs($dayPart, $request);
$dayPart
->setScheduleFactory($this->scheduleFactory, $this->displayNotifyService)
->save();
// Return
$this->getState()->hydrate([
'httpStatus' => 200,
'message' => sprintf(__('Edited %s'), $dayPart->name),
'id' => $dayPart->dayPartId,
'data' => $dayPart
]);
return $this->render($request, $response);
}
/**
* Handle common inputs
* @param \Xibo\Entity\DayPart $dayPart
* @param Request $request
*/
private function handleCommonInputs($dayPart, Request $request)
{
$dayPart->userId = $this->getUser()->userId;
$sanitizedParams = $this->getSanitizer($request->getParams());
$dayPart->name = $sanitizedParams->getString('name');
$dayPart->description = $sanitizedParams->getString('description');
$dayPart->isRetired = $sanitizedParams->getCheckbox('isRetired');
$dayPart->startTime = $sanitizedParams->getString('startTime');
$dayPart->endTime = $sanitizedParams->getString('endTime');
// Exceptions
$exceptionDays = $sanitizedParams->getArray('exceptionDays', ['default' => []]);
$exceptionStartTimes = $sanitizedParams->getArray('exceptionStartTimes', ['default' => []]);
$exceptionEndTimes = $sanitizedParams->getArray('exceptionEndTimes', ['default' => []]);
// Clear down existing exceptions
$dayPart->exceptions = [];
$i = -1;
foreach ($exceptionDays as $exceptionDay) {
// Pull the corrisponding start/end time out of the same position in the array
$i++;
$exceptionDayStartTime = isset($exceptionStartTimes[$i]) ? $exceptionStartTimes[$i] : '';
$exceptionDayEndTime = isset($exceptionEndTimes[$i]) ? $exceptionEndTimes[$i] : '';
if ($exceptionDay == '' || $exceptionDayStartTime == '' || $exceptionDayEndTime == '')
continue;
// Is this already set?
$found = false;
foreach ($dayPart->exceptions as $exception) {
if ($exception['day'] == $exceptionDay) {
$exception['start'] = $exceptionDayStartTime;
$exception['end'] = $exceptionDayEndTime;
$found = true;
break;
}
}
// Otherwise add it
if (!$found) {
$dayPart->exceptions[] = [
'day' => $exceptionDay,
'start' => $exceptionDayStartTime,
'end' => $exceptionDayEndTime
];
}
}
}
/**
* Delete
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
* @SWG\Delete(
* path="/daypart/{dayPartId}",
* operationId="dayPartDelete",
* tags={"dayPart"},
* summary="Delete DayPart",
* description="Delete the provided dayPart",
* @SWG\Parameter(
* name="dayPartId",
* in="path",
* description="The Daypart Id to Delete",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*/
public function delete(Request $request, Response $response, $id)
{
$dayPart = $this->dayPartFactory->getById($id);
if (!$this->getUser()->checkDeleteable($dayPart)) {
throw new AccessDeniedException();
}
if ($dayPart->isSystemDayPart()) {
throw new InvalidArgumentException(__('Cannot Delete system specific DayParts'));
}
$dayPart
->setScheduleFactory($this->scheduleFactory, $this->displayNotifyService)
->delete();
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Deleted %s'), $dayPart->name)
]);
return $this->render($request, $response);
}
}

View File

@@ -0,0 +1,698 @@
<?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\Controller;
use Psr\Http\Message\ResponseInterface;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Factory\ModuleFactory;
use Xibo\Factory\ModuleTemplateFactory;
use Xibo\Helper\SendFile;
use Xibo\Service\MediaService;
use Xibo\Service\UploadService;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\ConfigurationException;
use Xibo\Support\Exception\ControllerNotImplemented;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
/**
* Class Module
* @package Xibo\Controller
*/
class Developer extends Base
{
public function __construct(
private readonly ModuleFactory $moduleFactory,
private readonly ModuleTemplateFactory $moduleTemplateFactory
) {
}
/**
* Display the module templates page
* @param Request $request
* @param Response $response
* @return \Slim\Http\Response
* @throws \Xibo\Support\Exception\GeneralException
*/
public function displayTemplatePage(Request $request, Response $response): Response
{
$this->getState()->template = 'developer-template-page';
return $this->render($request, $response);
}
/**
* Show Module templates in a grid
* @param \Slim\Http\ServerRequest $request
* @param \Slim\Http\Response $response
* @return \Slim\Http\Response
* @throws \Xibo\Support\Exception\GeneralException
*/
public function templateGrid(Request $request, Response $response): Response
{
$params = $this->getSanitizer($request->getParams());
$templates = $this->moduleTemplateFactory->loadUserTemplates(
$this->gridRenderSort($params),
$this->gridRenderFilter(
[
'id' => $params->getInt('id'),
'templateId' => $params->getString('templateId'),
'dataType' => $params->getString('dataType'),
],
$params
)
);
foreach ($templates as $template) {
if ($this->isApi($request)) {
break;
}
$template->includeProperty('buttons');
if ($this->getUser()->checkEditable($template) &&
$this->getUser()->featureEnabled('developer.edit')
) {
// Edit button
$template->buttons[] = [
'id' => 'template_button_edit',
'url' => $this->urlFor($request, 'developer.templates.view.edit', ['id' => $template->id]),
'text' => __('Edit'),
'class' => 'XiboRedirectButton',
];
$template->buttons[] = [
'id' => 'template_button_export',
'linkType' => '_self', 'external' => true,
'url' => $this->urlFor($request, 'developer.templates.export', ['id' => $template->id]),
'text' => __('Export XML'),
];
$template->buttons[] = [
'id' => 'template_button_copy',
'url' => $this->urlFor($request, 'developer.templates.form.copy', ['id' => $template->id]),
'text' => __('Copy'),
];
}
if ($this->getUser()->featureEnabled('developer.edit') &&
$this->getUser()->checkPermissionsModifyable($template)
) {
$template->buttons[] = ['divider' => true];
// Permissions for Module Template
$template->buttons[] = [
'id' => 'template_button_permissions',
'url' => $this->urlFor(
$request,
'user.permissions.form',
['entity' => 'ModuleTemplate', 'id' => $template->id]
),
'text' => __('Share'),
'multi-select' => true,
'dataAttributes' => [
[
'name' => 'commit-url',
'value' => $this->urlFor(
$request,
'user.permissions.multi',
['entity' => 'ModuleTemplate', 'id' => $template->id]
)
],
['name' => 'commit-method', 'value' => 'post'],
['name' => 'id', 'value' => 'template_button_permissions'],
['name' => 'text', 'value' => __('Share')],
['name' => 'rowtitle', 'value' => $template->templateId],
['name' => 'sort-group', 'value' => 2],
['name' => 'custom-handler', 'value' => 'XiboMultiSelectPermissionsFormOpen'],
[
'name' => 'custom-handler-url',
'value' => $this->urlFor(
$request,
'user.permissions.multi.form',
['entity' => 'ModuleTemplate']
)
],
['name' => 'content-id-name', 'value' => 'id']
]
];
}
if ($this->getUser()->checkDeleteable($template) &&
$this->getUser()->featureEnabled('developer.delete')
) {
$template->buttons[] = ['divider' => true];
// Delete button
$template->buttons[] = [
'id' => 'template_button_delete',
'url' => $this->urlFor($request, 'developer.templates.form.delete', ['id' => $template->id]),
'text' => __('Delete'),
'multi-select' => true,
'dataAttributes' => [
[
'name' => 'commit-url',
'value' => $this->urlFor(
$request,
'developer.templates.delete',
['id' => $template->id]
)
],
['name' => 'commit-method', 'value' => 'delete'],
['name' => 'id', 'value' => 'template_button_delete'],
['name' => 'text', 'value' => __('Delete')],
['name' => 'sort-group', 'value' => 1],
['name' => 'rowtitle', 'value' => $template->templateId]
]
];
}
}
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = $this->moduleTemplateFactory->countLast();
$this->getState()->setData($templates);
return $this->render($request, $response);
}
/**
* Shows an add form for a module template
* @param Request $request
* @param Response $response
* @return \Slim\Http\Response
* @throws \Xibo\Support\Exception\GeneralException
*/
public function templateAddForm(Request $request, Response $response): Response
{
$this->getState()->template = 'developer-template-form-add';
return $this->render($request, $response);
}
/**
* Display the module template page
* @param Request $request
* @param Response $response
* @param mixed $id The template ID to edit.
* @return \Slim\Http\Response
* @throws \Xibo\Support\Exception\GeneralException
*/
public function displayTemplateEditPage(Request $request, Response $response, $id): Response
{
$template = $this->moduleTemplateFactory->getUserTemplateById($id);
if ($template->ownership !== 'user') {
throw new AccessDeniedException();
}
$this->getState()->template = 'developer-template-edit-page';
$this->getState()->setData([
'template' => $template,
'propertiesJSON' => json_encode($template->properties),
]);
return $this->render($request, $response);
}
/**
* Add a module template
* @param Request $request
* @param Response $response
* @return \Slim\Http\Response
* @throws \Xibo\Support\Exception\GeneralException
*/
public function templateAdd(Request $request, Response $response): Response
{
// When adding a template we just save the XML
$params = $this->getSanitizer($request->getParams());
$templateId = $params->getString('templateId', ['throw' => function () {
throw new InvalidArgumentException(__('Please supply a unique template ID'), 'templateId');
}]);
$title = $params->getString('title', ['throw' => function () {
throw new InvalidArgumentException(__('Please supply a title'), 'title');
}]);
$dataType = $params->getString('dataType', ['throw' => function () {
throw new InvalidArgumentException(__('Please supply a data type'), 'dataType');
}]);
$showIn = $params->getString('showIn', ['throw' => function () {
throw new InvalidArgumentException(
__('Please select relevant editor which should show this Template'),
'showIn'
);
}]);
// do we have a template selected?
if (!empty($params->getString('copyTemplateId'))) {
// get the selected template
$copyTemplate = $this->moduleTemplateFactory->getByDataTypeAndId(
$dataType,
$params->getString('copyTemplateId')
);
// get the template xml and load to document.
$xml = new \DOMDocument();
$xml->loadXML($copyTemplate->getXml());
// get template node, make adjustments from the form
$templateNode = $xml->getElementsByTagName('template')[0];
$this->setNode($xml, 'id', $templateId, false, $templateNode);
$this->setNode($xml, 'title', $title, false, $templateNode);
$this->setNode($xml, 'showIn', $showIn, false, $templateNode);
// create template with updated xml.
$template = $this->moduleTemplateFactory->createUserTemplate($xml->saveXML());
} else {
// The most basic template possible.
$template = $this->moduleTemplateFactory->createUserTemplate('<?xml version="1.0"?>
<template>
<id>' . $templateId . '</id>
<title>' . $title . '</title>
<type>static</type>
<dataType>' . $dataType . '</dataType>
<showIn>'. $showIn . '</showIn>
<properties></properties>
</template>');
}
$template->ownerId = $this->getUser()->userId;
$template->save();
$this->getState()->hydrate([
'httpState' => 201,
'message' => __('Added'),
'id' => $template->id,
]);
return $this->render($request, $response);
}
/**
* Edit a module template
* @param Request $request
* @param Response $response
* @param $id
* @return Response
* @throws \Xibo\Support\Exception\GeneralException
*/
public function templateEdit(Request $request, Response $response, $id): Response
{
$template = $this->moduleTemplateFactory->getUserTemplateById($id);
$params = $this->getSanitizer($request->getParams());
$templateId = $params->getString('templateId', ['throw' => function () {
throw new InvalidArgumentException(__('Please supply a unique template ID'), 'templateId');
}]);
$title = $params->getString('title', ['throw' => function () {
throw new InvalidArgumentException(__('Please supply a title'), 'title');
}]);
$dataType = $params->getString('dataType', ['throw' => function () {
throw new InvalidArgumentException(__('Please supply a data type'), 'dataType');
}]);
$showIn = $params->getString('showIn', ['throw' => function () {
throw new InvalidArgumentException(
__('Please select relevant editor which should show this Template'),
'showIn'
);
}]);
$template->dataType = $dataType;
$template->isEnabled = $params->getCheckbox('enabled');
// TODO: validate?
$twig = $params->getParam('twig');
$hbs = $params->getParam('hbs');
$style = $params->getParam('style');
$head = $params->getParam('head');
$properties = $params->getParam('properties');
$onTemplateRender = $params->getParam('onTemplateRender');
$onTemplateVisible = $params->getParam('onTemplateVisible');
// We need to edit the XML we have for this template.
$document = $template->getDocument();
// Root nodes
$template->templateId = $templateId;
$this->setNode($document, 'id', $templateId, false);
$this->setNode($document, 'title', $title, false);
$this->setNode($document, 'showIn', $showIn, false);
$this->setNode($document, 'dataType', $dataType, false);
$this->setNode($document, 'onTemplateRender', $onTemplateRender);
$this->setNode($document, 'onTemplateVisible', $onTemplateVisible);
// Stencil nodes.
$stencilNodes = $document->getElementsByTagName('stencil');
if ($stencilNodes->count() <= 0) {
$stencilNode = $document->createElement('stencil');
$document->documentElement->appendChild($stencilNode);
} else {
$stencilNode = $stencilNodes[0];
}
$this->setNode($document, 'twig', $twig, true, $stencilNode);
$this->setNode($document, 'hbs', $hbs, true, $stencilNode);
$this->setNode($document, 'style', $style, true, $stencilNode);
$this->setNode($document, 'head', $head, true, $stencilNode);
// Properties.
// this is different because we want to replace the properties node with a new one.
if (!empty($properties)) {
// parse json and create a new properties node
$newPropertiesXml = $this->moduleTemplateFactory->parseJsonPropertiesToXml($properties);
$propertiesNodes = $document->getElementsByTagName('properties');
if ($propertiesNodes->count() <= 0) {
$document->documentElement->appendChild(
$document->importNode($newPropertiesXml->documentElement, true)
);
} else {
$document->documentElement->replaceChild(
$document->importNode($newPropertiesXml->documentElement, true),
$propertiesNodes[0]
);
}
}
// All done.
$template->setXml($document->saveXML());
$template->save();
if ($params->getCheckbox('isInvalidateWidget')) {
$template->invalidate();
}
$this->getState()->hydrate([
'message' => sprintf(__('Edited %s'), $template->title),
'id' => $template->id,
]);
return $this->render($request, $response);
}
/**
* Helper function to set a node.
* @param \DOMDocument $document
* @param string $node
* @param string $value
* @param bool $cdata
* @param \DOMElement|null $childNode
* @return void
* @throws \DOMException
*/
private function setNode(
\DOMDocument $document,
string $node,
string $value,
bool $cdata = true,
?\DOMElement $childNode = null
): void {
$addTo = $childNode ?? $document->documentElement;
$nodes = $addTo->getElementsByTagName($node);
if ($nodes->count() <= 0) {
if ($cdata) {
$element = $document->createElement($node);
$cdata = $document->createCDATASection($value);
$element->appendChild($cdata);
} else {
$element = $document->createElement($node, $value);
}
$addTo->appendChild($element);
} else {
/** @var \DOMElement $element */
$element = $nodes[0];
if ($cdata) {
$cdata = $document->createCDATASection($value);
$element->textContent = $value;
if ($element->firstChild !== null) {
$element->replaceChild($cdata, $element->firstChild);
} else {
//$element->textContent = '';
$element->appendChild($cdata);
}
} else {
$element->textContent = $value;
}
}
}
public function getAvailableDataTypes(Request $request, Response $response)
{
$params = $this->getSanitizer($request->getParams());
$dataTypes = $this->moduleFactory->getAllDataTypes();
if ($params->getString('dataType') !== null) {
foreach ($dataTypes as $dataType) {
if ($dataType->id === $params->getString('dataType')) {
$dataTypes = [$dataType];
}
}
}
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = 0;
$this->getState()->setData($dataTypes);
return $this->render($request, $response);
}
/**
* Export module template
* @param Request $request
* @param Response $response
* @param $id
* @return ResponseInterface|Response
* @throws AccessDeniedException
* @throws ControllerNotImplemented
* @throws GeneralException
* @throws NotFoundException
*/
public function templateExport(Request $request, Response $response, $id): Response|ResponseInterface
{
$template = $this->moduleTemplateFactory->getUserTemplateById($id);
if ($template->ownership !== 'user') {
throw new AccessDeniedException();
}
$tempFileName = $this->getConfig()->getSetting('LIBRARY_LOCATION') . 'temp/' . $template->templateId . '.xml';
$template->getDocument()->save($tempFileName);
$this->setNoOutput(true);
return $this->render($request, SendFile::decorateResponse(
$response,
$this->getConfig()->getSetting('SENDFILE_MODE'),
$tempFileName,
$template->templateId . '.xml'
)->withHeader('Content-Type', 'text/xml;charset=utf-8'));
}
/**
* Import xml file and create module template
* @param Request $request
* @param Response $response
* @return ResponseInterface|Response
* @throws ControllerNotImplemented
* @throws GeneralException
* @throws ConfigurationException
*/
public function templateImport(Request $request, Response $response): Response|ResponseInterface
{
$this->getLog()->debug('Import Module Template');
$libraryFolder = $this->getConfig()->getSetting('LIBRARY_LOCATION');
// Make sure the library exists
MediaService::ensureLibraryExists($libraryFolder);
$options = [
'upload_dir' => $libraryFolder . 'temp/',
'accept_file_types' => '/\.xml/i',
'libraryQuotaFull' => false,
];
$this->getLog()->debug('Hand off to Upload Handler with options: ' . json_encode($options));
// Hand off to the Upload Handler provided by jquery-file-upload
$uploadService = new UploadService($libraryFolder . 'temp/', $options, $this->getLog(), $this->getState());
$uploadHandler = $uploadService->createUploadHandler();
$uploadHandler->setPostProcessor(function ($file, $uploadHandler) use ($libraryFolder) {
// Return right away if the file already has an error.
if (!empty($file->error)) {
return $file;
}
$this->getUser()->isQuotaFullByUser(true);
$filePath = $libraryFolder . 'temp/' . $file->fileName;
// load the xml from uploaded file
$xml = new \DOMDocument();
$xml->load($filePath);
// Add the Template
$moduleTemplate = $this->moduleTemplateFactory->createUserTemplate($xml->saveXML());
$moduleTemplate->ownerId = $this->getUser()->userId;
$moduleTemplate->save();
// Tidy up the temporary file
@unlink($filePath);
return $file;
});
$uploadHandler->post();
$this->setNoOutput(true);
return $this->render($request, $response);
}
/**
* Show module template copy form
* @param Request $request
* @param Response $response
* @param $id
* @return Response|ResponseInterface
* @throws AccessDeniedException
* @throws ControllerNotImplemented
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
*/
public function templateCopyForm(Request $request, Response $response, $id): Response|ResponseInterface
{
$moduleTemplate = $this->moduleTemplateFactory->getUserTemplateById($id);
if (!$this->getUser()->checkViewable($moduleTemplate)) {
throw new AccessDeniedException();
}
$this->getState()->template = 'developer-template-form-copy';
$this->getState()->setData([
'template' => $moduleTemplate,
]);
return $this->render($request, $response);
}
/**
* Copy module template
* @param Request $request
* @param Response $response
* @param $id
* @return Response|ResponseInterface
* @throws AccessDeniedException
* @throws ControllerNotImplemented
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
*/
public function templateCopy(Request $request, Response $response, $id): Response|ResponseInterface
{
$moduleTemplate = $this->moduleTemplateFactory->getUserTemplateById($id);
if (!$this->getUser()->checkViewable($moduleTemplate)) {
throw new AccessDeniedException();
}
$params = $this->getSanitizer($request->getParams());
$newTemplate = clone $moduleTemplate;
$newTemplate->templateId = $params->getString('templateId');
$newTemplate->save();
// Return
$this->getState()->hydrate([
'httpStatus' => 201,
'message' => sprintf(__('Copied as %s'), $newTemplate->templateId),
'id' => $newTemplate->id,
'data' => $newTemplate
]);
return $this->render($request, $response);
}
/**
* Show module template delete form
* @param Request $request
* @param Response $response
* @param $id
* @return Response|ResponseInterface
* @throws AccessDeniedException
* @throws ControllerNotImplemented
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
*/
public function templateDeleteForm(Request $request, Response $response, $id): Response|ResponseInterface
{
$moduleTemplate = $this->moduleTemplateFactory->getUserTemplateById($id);
if (!$this->getUser()->checkDeleteable($moduleTemplate)) {
throw new AccessDeniedException();
}
$this->getState()->template = 'developer-template-form-delete';
$this->getState()->setData([
'template' => $moduleTemplate,
]);
return $this->render($request, $response);
}
/**
* Delete module template
* @param Request $request
* @param Response $response
* @param $id
* @return Response|ResponseInterface
* @throws AccessDeniedException
* @throws ControllerNotImplemented
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
*/
public function templateDelete(Request $request, Response $response, $id): Response|ResponseInterface
{
$moduleTemplate = $this->moduleTemplateFactory->getUserTemplateById($id);
if (!$this->getUser()->checkDeleteable($moduleTemplate)) {
throw new AccessDeniedException();
}
$moduleTemplate->delete();
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Deleted %s'), $moduleTemplate->templateId)
]);
return $this->render($request, $response);
}
}

3196
lib/Controller/Display.php Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,698 @@
<?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\Controller;
use Carbon\Carbon;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Stash\Interfaces\PoolInterface;
use Xibo\Factory\CommandFactory;
use Xibo\Factory\DayPartFactory;
use Xibo\Factory\DisplayProfileFactory;
use Xibo\Factory\PlayerVersionFactory;
use Xibo\Helper\DateFormatHelper;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\NotFoundException;
/**
* Class DisplayProfile
* @package Xibo\Controller
*/
class DisplayProfile extends Base
{
use DisplayProfileConfigFields;
/** @var PoolInterface */
private $pool;
/**
* @var DayPartFactory
*/
private $dayPartFactory;
/**
* @var DisplayProfileFactory
*/
private $displayProfileFactory;
/**
* @var CommandFactory
*/
private $commandFactory;
/** @var PlayerVersionFactory */
private $playerVersionFactory;
/**
* Set common dependencies.
* @param PoolInterface $pool
* @param DisplayProfileFactory $displayProfileFactory
* @param CommandFactory $commandFactory
* @param PlayerVersionFactory $playerVersionFactory
* @param DayPartFactory $dayPartFactory
*/
public function __construct($pool, $displayProfileFactory, $commandFactory, $playerVersionFactory, $dayPartFactory)
{
$this->pool = $pool;
$this->displayProfileFactory = $displayProfileFactory;
$this->commandFactory = $commandFactory;
$this->playerVersionFactory = $playerVersionFactory;
$this->dayPartFactory = $dayPartFactory;
}
/**
* Include display page template page based on sub page selected
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
function displayPage(Request $request, Response $response)
{
$this->getState()->template = 'displayprofile-page';
$this->getState()->setData([
'types' => $this->displayProfileFactory->getAvailableTypes()
]);
return $this->render($request, $response);
}
/**
* @SWG\Get(
* path="/displayprofile",
* operationId="displayProfileSearch",
* tags={"displayprofile"},
* summary="Display Profile Search",
* description="Search this users Display Profiles",
* @SWG\Parameter(
* name="displayProfileId",
* in="query",
* description="Filter by DisplayProfile Id",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="displayProfile",
* in="query",
* description="Filter by DisplayProfile Name",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="type",
* in="query",
* description="Filter by DisplayProfile Type (windows|android|lg)",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="embed",
* in="query",
* description="Embed related data such as config,commands,configWithDefault",
* type="string",
* required=false
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="#/definitions/DisplayProfile")
* )
* )
* )
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
function grid(Request $request, Response $response)
{
$parsedQueryParams = $this->getSanitizer($request->getQueryParams());
$filter = [
'displayProfileId' => $parsedQueryParams->getInt('displayProfileId'),
'displayProfile' => $parsedQueryParams->getString('displayProfile'),
'useRegexForName' => $parsedQueryParams->getCheckbox('useRegexForName'),
'type' => $parsedQueryParams->getString('type'),
'logicalOperatorName' => $parsedQueryParams->getString('logicalOperatorName'),
];
$embed = ($parsedQueryParams->getString('embed') != null)
? explode(',', $parsedQueryParams->getString('embed'))
: [];
$profiles = $this->displayProfileFactory->query(
$this->gridRenderSort($parsedQueryParams),
$this->gridRenderFilter($filter, $parsedQueryParams)
);
foreach ($profiles as $profile) {
// Load the config
$profile->load([
'loadConfig' => in_array('config', $embed),
'loadCommands' => in_array('commands', $embed)
]);
if (in_array('configWithDefault', $embed)) {
$profile->includeProperty('configDefault');
}
if (!in_array('config', $embed)) {
$profile->excludeProperty('config');
}
if ($this->isApi($request)) {
continue;
}
$profile->includeProperty('buttons');
if ($this->getUser()->featureEnabled('displayprofile.modify')) {
// Default Layout
$profile->buttons[] = array(
'id' => 'displayprofile_button_edit',
'url' => $this->urlFor(
$request,
'displayProfile.edit.form',
['id' => $profile->displayProfileId]
),
'text' => __('Edit')
);
$profile->buttons[] = array(
'id' => 'displayprofile_button_copy',
'url' => $this->urlFor(
$request,
'displayProfile.copy.form',
['id' => $profile->displayProfileId]
),
'text' => __('Copy')
);
if ($this->getUser()->checkDeleteable($profile)) {
$profile->buttons[] = array(
'id' => 'displayprofile_button_delete',
'url' => $this->urlFor(
$request,
'displayProfile.delete.form',
['id' => $profile->displayProfileId]
),
'text' => __('Delete')
);
}
}
}
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = $this->displayProfileFactory->countLast();
$this->getState()->setData($profiles);
return $this->render($request, $response);
}
/**
* Display Profile Add Form
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
function addForm(Request $request, Response $response)
{
$this->getState()->template = 'displayprofile-form-add';
$this->getState()->setData([
'types' => $this->displayProfileFactory->getAvailableTypes()
]);
return $this->render($request, $response);
}
/**
* Display Profile Add
*
* @SWG\Post(
* path="/displayprofile",
* operationId="displayProfileAdd",
* tags={"displayprofile"},
* summary="Add Display Profile",
* description="Add a Display Profile",
* @SWG\Parameter(
* name="name",
* in="formData",
* description="The Name of the Display Profile",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="type",
* in="formData",
* description="The Client Type this Profile will apply to",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="isDefault",
* in="formData",
* description="Flag indicating if this is the default profile for the client type",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=201,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/DisplayProfile"),
* @SWG\Header(
* header="Location",
* description="Location of the new record",
* type="string"
* )
* )
* )
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\InvalidArgumentException
*/
public function add(Request $request, Response $response)
{
$sanitizedParams = $this->getSanitizer($request->getParams());
$displayProfile = $this->displayProfileFactory->createEmpty();
$displayProfile->name = $sanitizedParams->getString('name');
$displayProfile->type = $sanitizedParams->getString('type');
$displayProfile->isDefault = $sanitizedParams->getCheckbox('isDefault');
$displayProfile->userId = $this->getUser()->userId;
$displayProfile->isCustom = $this->displayProfileFactory->isCustomType($displayProfile->type);
// We do not set any config at this point, so that unless the user chooses to edit the display profile
// our defaults in the Display Profile Factory take effect
$displayProfile->save();
// Return
$this->getState()->hydrate([
'httpStatus' => 201,
'message' => sprintf(__('Added %s'), $displayProfile->name),
'id' => $displayProfile->displayProfileId,
'data' => $displayProfile
]);
return $this->render($request, $response);
}
/**
* Edit Profile Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function editForm(Request $request, Response $response, $id)
{
// Create a form out of the config object.
$displayProfile = $this->displayProfileFactory->getById($id);
// Check permissions
if ($this->getUser()->userTypeId != 1 && $this->getUser()->userId != $displayProfile->userId) {
throw new AccessDeniedException(__('You do not have permission to edit this profile'));
}
// Player Version Setting
$versionId = $displayProfile->type === 'chromeOS'
? $displayProfile->getSetting('playerVersionId')
: $displayProfile->getSetting('versionMediaId');
$playerVersions = [];
// Daypart - Operating Hours
$dayPartId = $displayProfile->getSetting('dayPartId');
$dayparts = [];
// Get the Player Version for this display profile type
if ($versionId !== null) {
try {
$playerVersions[] = $this->playerVersionFactory->getById($versionId);
} catch (NotFoundException) {
$this->getLog()->debug('Unknown versionId set on Display Profile. '
. $displayProfile->displayProfileId);
}
}
if ($dayPartId !== null) {
try {
$dayparts[] = $this->dayPartFactory->getById($dayPartId);
} catch (NotFoundException $e) {
$this->getLog()->debug('Unknown dayPartId set on Display Profile. ' . $displayProfile->displayProfileId);
}
}
// elevated logs
$elevateLogsUntil = $displayProfile->getSetting('elevateLogsUntil');
$elevateLogsUntilIso = !empty($elevateLogsUntil)
? Carbon::createFromTimestamp($elevateLogsUntil)->format(DateFormatHelper::getSystemFormat())
: null;
$displayProfile->setUnmatchedProperty('elevateLogsUntilIso', $elevateLogsUntilIso);
$this->getState()->template = 'displayprofile-form-edit';
$this->getState()->setData([
'displayProfile' => $displayProfile,
'commands' => $displayProfile->commands,
'versions' => $playerVersions,
'lockOptions' => json_decode($displayProfile->getSetting('lockOptions', '[]'), true),
'dayParts' => $dayparts
]);
return $this->render($request, $response);
}
/**
* Edit
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\InvalidArgumentException
* @SWG\Put(
* path="/displayprofile/{displayProfileId}",
* operationId="displayProfileEdit",
* tags={"displayprofile"},
* summary="Edit Display Profile",
* description="Edit a Display Profile",
* @SWG\Parameter(
* name="displayProfileId",
* in="path",
* description="The Display Profile ID",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="name",
* in="formData",
* description="The Name of the Display Profile",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="type",
* in="formData",
* description="The Client Type this Profile will apply to",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="isDefault",
* in="formData",
* description="Flag indicating if this is the default profile for the client type",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*/
public function edit(Request $request, Response $response, $id)
{
// Create a form out of the config object.
$displayProfile = $this->displayProfileFactory->getById($id);
$parsedParams = $this->getSanitizer($request->getParams());
if ($this->getUser()->userTypeId != 1 && $this->getUser()->userId != $displayProfile->userId) {
throw new AccessDeniedException(__('You do not have permission to edit this profile'));
}
$displayProfile->name = $parsedParams->getString('name');
$displayProfile->isDefault = $parsedParams->getCheckbox('isDefault');
// Track changes to versionMediaId
$originalPlayerVersionId = $displayProfile->getSetting('playerVersionId');
// Different fields for each client type
$this->editConfigFields($displayProfile, $parsedParams);
// Capture and update commands
foreach ($this->commandFactory->query() as $command) {
if ($parsedParams->getString('commandString_' . $command->commandId) != null) {
// Set and assign the command
$command->commandString = $parsedParams->getString('commandString_' . $command->commandId);
$command->validationString = $parsedParams->getString('validationString_' . $command->commandId);
$command->createAlertOn = $parsedParams->getString('createAlertOn_' . $command->commandId);
$displayProfile->assignCommand($command);
} else {
$displayProfile->unassignCommand($command);
}
}
// If we are chromeOS and the default profile, has the player version changed?
if ($displayProfile->type === 'chromeOS'
&& ($displayProfile->isDefault || $displayProfile->hasPropertyChanged('isDefault'))
&& ($originalPlayerVersionId !== $displayProfile->getSetting('playerVersionId'))
) {
$this->getLog()->debug('edit: updating symlink to the latest chromeOS version');
// Update a symlink to the new player version.
try {
$version = $this->playerVersionFactory->getById($displayProfile->getSetting('playerVersionId'));
$version->setActive();
} catch (NotFoundException) {
$this->getLog()->error('edit: Player version does not exist');
}
}
// Save the changes
$displayProfile->save();
// Clear the display cached
$this->pool->deleteItem('display/');
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Edited %s'), $displayProfile->name),
'id' => $displayProfile->displayProfileId,
'data' => $displayProfile
]);
return $this->render($request, $response);
}
/**
* Delete Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
function deleteForm(Request $request, Response $response, $id)
{
// Create a form out of the config object.
$displayProfile = $this->displayProfileFactory->getById($id);
if ($this->getUser()->userTypeId != 1 && $this->getUser()->userId != $displayProfile->userId)
throw new AccessDeniedException(__('You do not have permission to edit this profile'));
$this->getState()->template = 'displayprofile-form-delete';
$this->getState()->setData([
'displayProfile' => $displayProfile,
]);
return $this->render($request, $response);
}
/**
* Delete Display Profile
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\InvalidArgumentException
* @SWG\Delete(
* path="/displayprofile/{displayProfileId}",
* operationId="displayProfileDelete",
* tags={"displayprofile"},
* summary="Delete Display Profile",
* description="Delete an existing Display Profile",
* @SWG\Parameter(
* name="displayProfileId",
* in="path",
* description="The Display Profile ID",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*/
function delete(Request $request, Response $response, $id)
{
// Create a form out of the config object.
$displayProfile = $this->displayProfileFactory->getById($id);
if ($this->getUser()->userTypeId != 1 && $this->getUser()->userId != $displayProfile->userId) {
throw new AccessDeniedException(__('You do not have permission to delete this profile'));
}
$displayProfile->delete();
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Deleted %s'), $displayProfile->name)
]);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function copyForm(Request $request, Response $response, $id)
{
// Create a form out of the config object.
$displayProfile = $this->displayProfileFactory->getById($id);
if ($this->getUser()->userTypeId != 1 && $this->getUser()->userId != $displayProfile->userId)
throw new AccessDeniedException(__('You do not have permission to delete this profile'));
$this->getState()->template = 'displayprofile-form-copy';
$this->getState()->setData([
'displayProfile' => $displayProfile
]);
return $this->render($request, $response);
}
/**
* Copy Display Profile
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\InvalidArgumentException
* @SWG\Post(
* path="/displayprofile/{displayProfileId}/copy",
* operationId="displayProfileCopy",
* tags={"displayprofile"},
* summary="Copy Display Profile",
* description="Copy an existing Display Profile",
* @SWG\Parameter(
* name="displayProfileId",
* in="path",
* description="The Display Profile ID",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="name",
* in="path",
* description="The name for the copy",
* type="string",
* required=true
* ),
* @SWG\Response(
* response=201,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/DisplayProfile"),
* @SWG\Header(
* header="Location",
* description="Location of the new record",
* type="string"
* )
* )
* )
*/
public function copy(Request $request, Response $response, $id)
{
// Create a form out of the config object.
$displayProfile = $this->displayProfileFactory->getById($id);
if ($this->getUser()->userTypeId != 1 && $this->getUser()->userId != $displayProfile->userId) {
throw new AccessDeniedException(__('You do not have permission to delete this profile'));
}
// clear DisplayProfileId, commands and set isDefault to 0
$new = clone $displayProfile;
$new->name = $this->getSanitizer($request->getParams())->getString('name');
foreach ($displayProfile->commands as $command) {
/* @var \Xibo\Entity\Command $command */
if (!empty($command->commandStringDisplayProfile)) {
// if the original Display Profile has a commandString
// assign this command with the same commandString to new Display Profile
// commands with only default commandString are not directly assigned to Display profile
$command->commandString = $command->commandStringDisplayProfile;
$command->validationString = $command->validationStringDisplayProfile;
$new->assignCommand($command);
}
}
$new->save();
// Return
$this->getState()->hydrate([
'httpStatus' => 201,
'message' => sprintf(__('Added %s'), $new->name),
'id' => $new->displayProfileId,
'data' => $new
]);
return $this->render($request, $response);
}
}

File diff suppressed because it is too large Load Diff

270
lib/Controller/Fault.php Normal file
View File

@@ -0,0 +1,270 @@
<?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\Controller;
use Carbon\Carbon;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Factory\DisplayFactory;
use Xibo\Factory\LogFactory;
use Xibo\Helper\Environment;
use Xibo\Helper\Random;
use Xibo\Helper\SendFile;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\InvalidArgumentException;
/**
* Class Fault
* @package Xibo\Controller
*/
class Fault extends Base
{
/** @var StorageServiceInterface */
private $store;
/**
* @var LogFactory
*/
private $logFactory;
/** @var DisplayFactory */
private $displayFactory;
/**
* Set common dependencies.
* @param StorageServiceInterface $store
* @param LogFactory $logFactory
* @param DisplayFactory $displayFactory
*/
public function __construct($store, $logFactory, $displayFactory)
{
$this->store = $store;
$this->logFactory = $logFactory;
$this->displayFactory = $displayFactory;
}
/**
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function displayPage(Request $request, Response $response)
{
$url = $request->getUri() . $request->getUri()->getPath();
$config = $this->getConfig();
$data = [
'environmentCheck' => $config->checkEnvironment(),
'environmentFault' => $config->envFault,
'environmentWarning' => $config->envWarning,
'binLogError' => ($config->checkBinLogEnabled() && !$config->checkBinLogFormat()),
'urlError' => !Environment::checkUrl($url)
];
$this->getState()->template = 'fault-page';
$this->getState()->setData($data);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function collect(Request $request, Response $response)
{
$this->setNoOutput(true);
$sanitizedParams = $this->getSanitizer($request->getParams());
// Create a ZIP file
$tempFileName = $this->getConfig()->getSetting('LIBRARY_LOCATION') . 'temp/' . Random::generateString();
$zip = new \ZipArchive();
$result = $zip->open($tempFileName, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
if ($result !== true) {
throw new InvalidArgumentException(__('Can\'t create ZIP. Error Code: ' . $result));
}
// Decide what we output based on the options selected.
$outputVersion = $sanitizedParams->getCheckbox('outputVersion') == 1;
$outputLog = $sanitizedParams->getCheckbox('outputLog') == 1;
$outputEnvCheck = $sanitizedParams->getCheckbox('outputEnvCheck') == 1;
$outputSettings = $sanitizedParams->getCheckbox('outputSettings') == 1;
$outputDisplays = $sanitizedParams->getCheckbox('outputDisplays') == 1;
$outputDisplayProfile = $sanitizedParams->getCheckbox('outputDisplayProfile') == 1;
if (!$outputVersion &&
!$outputLog &&
!$outputEnvCheck &&
!$outputSettings &&
!$outputDisplays &&
!$outputDisplayProfile
) {
throw new InvalidArgumentException(__('Please select at least one option'));
}
$environmentVariables = [
'app_ver' => Environment::$WEBSITE_VERSION_NAME,
'XmdsVersion' => Environment::$XMDS_VERSION,
'XlfVersion' => Environment::$XLF_VERSION
];
// Should we output the version?
if ($outputVersion) {
$zip->addFromString('version.json', json_encode($environmentVariables, JSON_PRETTY_PRINT));
}
// Should we output a log?
if ($outputLog) {
$tempLogFile = $this->getConfig()->getSetting('LIBRARY_LOCATION') . 'temp/log_' . Random::generateString();
$out = fopen($tempLogFile, 'w');
fputcsv(
$out,
[
'logId',
'runNo',
'logDate',
'channel',
'page',
'function',
'message',
'display.display',
'type',
'sessionHistoryId'
]
);
$fromDt = Carbon::now()->subSeconds(60 * 10)->format('U');
// Do some post processing
foreach ($this->logFactory->query(['logId'], ['fromDt' => $fromDt]) as $row) {
/* @var \Xibo\Entity\LogEntry $row */
fputcsv(
$out,
[
$row->logId,
$row->runNo,
$row->logDate,
$row->channel,
$row->page,
$row->function,
$row->message,
$row->display,
$row->type,
$row->sessionHistoryId
]
);
}
fclose($out);
$zip->addFile($tempLogFile, 'log.csv');
}
// Output ENV Check
if ($outputEnvCheck) {
$zip->addFromString('environment.json', json_encode(array_map(function ($element) {
unset($element['advice']);
return $element;
}, $this->getConfig()->checkEnvironment()), JSON_PRETTY_PRINT));
}
// Output Settings
if ($outputSettings) {
$zip->addFromString('settings.json', json_encode(array_map(function ($element) {
return [$element['setting'] => $element['value']];
}, $this->store->select('SELECT setting, `value` FROM `setting`', [])), JSON_PRETTY_PRINT));
}
// Output Displays
if ($outputDisplays) {
$displays = $this->displayFactory->query(['display']);
// Output Profiles
if ($outputDisplayProfile) {
foreach ($displays as $display) {
/** @var \Xibo\Entity\Display $display */
$display->setUnmatchedProperty('settingProfile', array_map(function ($element) {
unset($element['helpText']);
return $element;
}, $display->getSettings()));
}
}
$zip->addFromString('displays.json', json_encode($displays, JSON_PRETTY_PRINT));
}
// Close the ZIP file
$zip->close();
return $this->render($request, SendFile::decorateResponse(
$response,
$this->getConfig()->getSetting('SENDFILE_MODE'),
$tempFileName,
'troubleshoot.zip'
));
}
/**
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function debugOn(Request $request, Response $response)
{
$this->getConfig()->changeSetting('audit', 'debug');
$this->getConfig()->changeSetting('ELEVATE_LOG_UNTIL', Carbon::now()->addMinutes(30)->format('U'));
// Return
$this->getState()->hydrate([
'message' => __('Switched to Debug Mode')
]);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function debugOff(Request $request, Response $response)
{
$this->getConfig()->changeSetting('audit', $this->getConfig()->getSetting('RESTING_LOG_LEVEL'));
$this->getConfig()->changeSetting('ELEVATE_LOG_UNTIL', '');
// Return
$this->getState()->hydrate([
'message' => __('Switched to Normal Mode')
]);
return $this->render($request, $response);
}
}

564
lib/Controller/Folder.php Normal file
View File

@@ -0,0 +1,564 @@
<?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\Controller;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Factory\FolderFactory;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
class Folder extends Base
{
/**
* @var FolderFactory
*/
private $folderFactory;
/**
* Set common dependencies.
* @param FolderFactory $folderFactory
*/
public function __construct(FolderFactory $folderFactory)
{
$this->folderFactory = $folderFactory;
}
/**
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function displayPage(Request $request, Response $response)
{
$this->getState()->template = 'folders-page';
$this->getState()->setData([]);
return $this->render($request, $response);
}
/**
* Returns JSON representation of the Folder tree
*
* @SWG\Get(
* path="/folders",
* operationId="folderSearch",
* tags={"folder"},
* summary="Search Folders",
* description="Returns JSON representation of the Folder tree",
* @SWG\Parameter(
* name="folderId",
* in="path",
* description="Show usage details for the specified Folder Id",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="gridView",
* in="query",
* description="Flag (0, 1), Show Folders in a standard grid response",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="folderId",
* in="query",
* description="Use with gridView, Filter by Folder Id",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="folderName",
* in="query",
* description="Use with gridView, Filter by Folder name",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="exactFolderName",
* in="query",
* description="Use with gridView, Filter by exact Folder name match",
* type="integer",
* required=false
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="#/definitions/Folder")
* )
* )
* )
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\GeneralException
*/
public function grid(Request $request, Response $response, $folderId = null)
{
$params = $this->getSanitizer($request->getParams());
if ($params->getInt('gridView') === 1) {
$folders = $this->folderFactory->query($this->gridRenderSort($params), $this->gridRenderFilter([
'folderName' => $params->getString('folderName'),
'folderId' => $params->getInt('folderId'),
'exactFolderName' => $params->getInt('exactFolderName'),
], $params));
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = $this->folderFactory->countLast();
$this->getState()->setData($folders);
return $this->render($request, $response);
} else if ($params->getString('folderName') !== null) {
// Search all folders by name
$folders = $this->folderFactory->query($this->gridRenderSort($params), $this->gridRenderFilter([
'folderName' => $params->getString('folderName'),
'exactFolderName' => $params->getInt('exactFolderName'),
], $params));
return $response->withJson($folders);
} else if ($folderId !== null) {
// Should we return information for a specific folder?
$folder = $this->folderFactory->getById($folderId);
$this->decorateWithButtons($folder);
$this->folderFactory->decorateWithHomeFolderCount($folder);
$this->folderFactory->decorateWithSharing($folder);
$this->folderFactory->decorateWithUsage($folder);
return $response->withJson($folder);
} else {
// Show a tree view of all folders.
$rootFolder = $this->folderFactory->getById(1);
// homeFolderId,
// do we show tree for current user
// or a specified user?
$homeFolderId = ($params->getInt('homeFolderId') !== null)
? $params->getInt('homeFolderId')
: $this->getUser()->homeFolderId;
$this->buildTreeView($rootFolder, $homeFolderId);
return $response->withJson([$rootFolder]);
}
}
/**
* @param \Xibo\Entity\Folder $folder
* @param int $homeFolderId
* @throws InvalidArgumentException
*/
private function buildTreeView(\Xibo\Entity\Folder $folder, int $homeFolderId)
{
// Set the folder type
$folder->type = '';
if ($folder->isRoot === 1) {
$folder->type = 'root';
}
if ($homeFolderId === $folder->id) {
$folder->type = 'home';
}
if (!empty($folder->children)) {
$children = array_filter(explode(',', $folder->children));
} else {
$children = [];
}
$childrenDetails = [];
foreach ($children as $childId) {
try {
$child = $this->folderFactory->getById($childId);
if ($child->children != null) {
$this->buildTreeView($child, $homeFolderId);
}
if (!$this->getUser()->checkViewable($child)) {
$child->text = __('Private Folder');
$child->type = 'disabled';
}
if ($homeFolderId === $child->id) {
$child->type = 'home';
}
$childrenDetails[] = $child;
} catch (NotFoundException $exception) {
// this should be fine, just log debug message about it.
$this->getLog()->debug('User does not have permissions to Folder ID ' . $childId);
}
}
$folder->children = $childrenDetails;
}
/**
* Add a new Folder
*
* @SWG\Post(
* path="/folders",
* operationId="folderAdd",
* tags={"folder"},
* summary="Add Folder",
* description="Add a new Folder to the specified parent Folder",
* @SWG\Parameter(
* name="text",
* in="formData",
* description="Folder Name",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="parentId",
* in="formData",
* description="The ID of the parent Folder, if not provided, Folder will be added under Root Folder",
* type="string",
* required=false
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(
* @SWG\Items(ref="#/definitions/Folder")
* )
* )
* )
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\InvalidArgumentException
*/
public function add(Request $request, Response $response)
{
$sanitizedParams = $this->getSanitizer($request->getParams());
$folder = $this->folderFactory->createEmpty();
$folder->text = $sanitizedParams->getString('text');
$folder->parentId = $sanitizedParams->getString('parentId', ['default' => 1]);
$folder->save();
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Added %s'), $folder->text),
'id' => $folder->id,
'data' => $folder
]);
return $this->render($request, $response);
}
/**
* Edit existing Folder
*
* @SWG\Put(
* path="/folders/{folderId}",
* operationId="folderEdit",
* tags={"folder"},
* summary="Edit Folder",
* description="Edit existing Folder",
* @SWG\Parameter(
* name="folderId",
* in="path",
* description="Folder ID to edit",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="text",
* in="formData",
* description="Folder Name",
* type="string",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(
* @SWG\Items(ref="#/definitions/Folder")
* )
* )
* )
* @param Request $request
* @param Response $response
* @param $folderId
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\InvalidArgumentException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function edit(Request $request, Response $response, $folderId)
{
$sanitizedParams = $this->getSanitizer($request->getParams());
$folder = $this->folderFactory->getById($folderId);
if ($folder->isRoot === 1) {
throw new InvalidArgumentException(__('Cannot edit root Folder'), 'isRoot');
}
if (!$this->getUser()->checkEditable($folder)) {
throw new AccessDeniedException();
}
$folder->text = $sanitizedParams->getString('text');
$folder->save();
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Edited %s'), $folder->text),
'id' => $folder->id,
'data' => $folder
]);
return $this->render($request, $response);
}
/**
* Delete existing Folder
*
* @SWG\Delete(
* path="/folders/{folderId}",
* operationId="folderDelete",
* tags={"folder"},
* summary="Delete Folder",
* description="Delete existing Folder",
* @SWG\Parameter(
* name="folderId",
* in="path",
* description="Folder ID to edit",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation",
* @SWG\Schema(
* @SWG\Items(ref="#/definitions/Folder")
* )
* )
* )
* @param Request $request
* @param Response $response
* @param $folderId
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function delete(Request $request, Response $response, $folderId)
{
$folder = $this->folderFactory->getById($folderId);
$folder->load();
if ($folder->isRoot === 1) {
throw new InvalidArgumentException(__('Cannot remove root Folder'), 'isRoot');
}
if (!$this->getUser()->checkDeleteable($folder)) {
throw new AccessDeniedException();
}
if ($folder->isHome()) {
throw new InvalidArgumentException(
__('Cannot remove Folder set as home Folder for a user'),
'folderId',
__('Change home Folder for Users using this Folder before deleting')
);
}
if ($folder->id == $this->getConfig()->getSetting('DISPLAY_DEFAULT_FOLDER')) {
throw new InvalidArgumentException(
__('Cannot remove Folder set as default Folder for new Displays'),
'folderId',
__('Change Default Folder for new Displays before deleting')
);
}
// Check if the folder is in use
$this->folderFactory->decorateWithUsage($folder);
$usage = $folder->getUnmatchedProperty('usage');
// Prevent deletion if the folder has any usage
if (!empty($usage)) {
$usageDetails = [];
// Loop through usage data and construct the formatted message
foreach ($usage as $item) {
$usageDetails[] = $item['type'] . ' (' . $item['count'] . ')';
}
throw new InvalidArgumentException(
__('Cannot remove Folder with content: ' . implode(', ', $usageDetails)),
'folderId',
__('Reassign objects from this Folder before deleting.')
);
}
try {
$folder->delete();
} catch (\Exception $exception) {
$this->getLog()->debug('Folder delete failed with message: ' . $exception->getMessage());
throw new InvalidArgumentException(
__('Cannot remove Folder with content'),
'folderId',
__('Reassign objects from this Folder before deleting.')
);
}
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Deleted %s'), $folder->text)
]);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @param $folderId
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws InvalidArgumentException
* @throws NotFoundException
*/
public function getContextMenuButtons(Request $request, Response $response, $folderId)
{
$folder = $this->folderFactory->getById($folderId);
$this->decorateWithButtons($folder);
return $response->withJson($folder->buttons);
}
private function decorateWithButtons(\Xibo\Entity\Folder $folder)
{
$user = $this->getUser();
if ($user->featureEnabled('folder.add')
&& $user->checkViewable($folder)
&& (!$folder->isRoot() || $user->isSuperAdmin())
) {
$folder->buttons['create'] = true;
}
$featureModify = $user->featureEnabled('folder.modify');
if ($featureModify
&& $user->checkEditable($folder)
&& !$folder->isRoot()
&& ($this->getUser()->isSuperAdmin() || $folder->getId() !== $this->getUser()->homeFolderId)
) {
$folder->buttons['modify'] = true;
}
if ($featureModify
&& $user->checkDeleteable($folder)
&& !$folder->isRoot()
&& ($this->getUser()->isSuperAdmin() || $folder->getId() !== $this->getUser()->homeFolderId)
) {
$folder->buttons['delete'] = true;
}
if ($user->isSuperAdmin() && !$folder->isRoot()) {
$folder->buttons['share'] = true;
}
if (!$folder->isRoot() && $user->checkViewable($folder) && $user->featureEnabled('folder.modify')) {
$folder->buttons['move'] = true;
}
}
public function moveForm(Request $request, Response $response, $folderId)
{
$folder = $this->folderFactory->getById($folderId, 0);
if (!$this->getUser()->checkEditable($folder)) {
throw new AccessDeniedException();
}
$this->getState()->template = 'folder-form-move';
$this->getState()->setData([
'folder' => $folder,
'deletable' => $this->getUser()->checkDeleteable($folder)
]);
return $this->render($request, $response);
}
public function move(Request $request, Response $response, $folderId)
{
$params = $this->getSanitizer($request->getParams());
$folder = $this->folderFactory->getById($folderId);
$newParentFolder = $this->folderFactory->getById($params->getInt('folderId'), 0);
if (!$this->getUser()->checkEditable($folder)
|| $folder->isRoot()
|| !$this->getUser()->checkViewable($newParentFolder)
) {
throw new AccessDeniedException();
}
if ($folder->id === $params->getInt('folderId')) {
throw new InvalidArgumentException(
__('Please select different folder, cannot move Folder to the same Folder')
);
}
if ($folder->isTheSameBranch($newParentFolder->getId())) {
throw new InvalidArgumentException(
__('Please select different folder, cannot move Folder inside of one of its sub-folders')
);
}
if ($folder->parentId === $newParentFolder->getId() && $params->getCheckbox('merge') !== 1) {
throw new InvalidArgumentException(__('This Folder is already a sub-folder of the selected Folder, if you wish to move its content to the parent Folder, please check the merge checkbox.'));//phpcs:ignore
}
// if we need to merge contents of the folder, dispatch an event that will move every object inside the folder
// to the new folder, any sub-folders will be moved to the new parent folder keeping the tree structure.
if ($params->getCheckbox('merge') === 1) {
$event = new \Xibo\Event\FolderMovingEvent($folder, $newParentFolder, true);
$this->getDispatcher()->dispatch($event, $event::$NAME);
// after moving event is done, we should be able to safely delete the original folder
$folder = $this->folderFactory->getById($folderId, 0);
$folder->load();
$folder->delete();
} else {
// if we just want to move the Folder to new parent, we move folder and its sub-folders to the new parent
// changing the permissionsFolderId as well if needed.
$folder->updateFoldersAfterMove($folder->parentId, $newParentFolder->getId());
}
return $this->render($request, $response);
}
}

593
lib/Controller/Font.php Normal file
View File

@@ -0,0 +1,593 @@
<?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\Controller;
use GuzzleHttp\Psr7\Stream;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Stash\Invalidation;
use Xibo\Factory\FontFactory;
use Xibo\Helper\ByteFormatter;
use Xibo\Helper\HttpCacheProvider;
use Xibo\Service\DownloadService;
use Xibo\Service\MediaService;
use Xibo\Service\MediaServiceInterface;
use Xibo\Service\UploadService;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\ConfigurationException;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
class Font extends Base
{
/**
* @var FontFactory
*/
private $fontFactory;
/**
* @var MediaServiceInterface
*/
private $mediaService;
public function __construct(FontFactory $fontFactory)
{
$this->fontFactory = $fontFactory;
}
public function useMediaService(MediaServiceInterface $mediaService)
{
$this->mediaService = $mediaService;
}
public function getMediaService(): MediaServiceInterface
{
return $this->mediaService->setUser($this->getUser());
}
public function getFontFactory() : FontFactory
{
return $this->fontFactory;
}
/**
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function displayPage(Request $request, Response $response)
{
if (!$this->getUser()->featureEnabled('font.view')) {
throw new AccessDeniedException();
}
$this->getState()->template = 'fonts-page';
$this->getState()->setData([
'validExt' => implode('|', $this->getValidExtensions())
]);
return $this->render($request, $response);
}
/**
* Prints out a Table of all Font items
*
* @SWG\Get(
* path="/fonts",
* operationId="fontSearch",
* tags={"font"},
* summary="Font Search",
* description="Search the available Fonts",
* @SWG\Parameter(
* name="id",
* in="query",
* description="Filter by Font Id",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="name",
* in="query",
* description="Filter by Font Name",
* type="string",
* required=false
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="#/definitions/Font")
* )
* )
* )
*
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function grid(Request $request, Response $response)
{
$parsedQueryParams = $this->getSanitizer($request->getQueryParams());
// Construct the SQL
$fonts = $this->fontFactory->query($this->gridRenderSort($parsedQueryParams), $this->gridRenderFilter([
'id' => $parsedQueryParams->getInt('id'),
'name' => $parsedQueryParams->getString('name'),
], $parsedQueryParams));
foreach ($fonts as $font) {
$font->setUnmatchedProperty('fileSizeFormatted', ByteFormatter::format($font->size));
$font->buttons = [];
if ($this->isApi($request)) {
break;
}
// download the font file
$font->buttons[] = [
'id' => 'content_button_download',
'linkType' => '_self', 'external' => true,
'url' => $this->urlFor($request, 'font.download', ['id' => $font->id]),
'text' => __('Download')
];
// font details from fontLib and preview text
$font->buttons[] = [
'id' => 'font_button_details',
'url' => $this->urlFor($request, 'font.details', ['id' => $font->id]),
'text' => __('Details')
];
$font->buttons[] = ['divider' => true];
if ($this->getUser()->featureEnabled('font.delete')) {
// Delete Button
$font->buttons[] = [
'id' => 'content_button_delete',
'url' => $this->urlFor($request, 'font.form.delete', ['id' => $font->id]),
'text' => __('Delete'),
'multi-select' => true,
'dataAttributes' => [
[
'name' => 'commit-url',
'value' => $this->urlFor($request, 'font.delete', ['id' => $font->id])
],
['name' => 'commit-method', 'value' => 'delete'],
['name' => 'id', 'value' => 'content_button_delete'],
['name' => 'text', 'value' => __('Delete')],
['name' => 'sort-group', 'value' => 1],
['name' => 'rowtitle', 'value' => $font->name]
]
];
}
}
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = $this->fontFactory->countLast();
$this->getState()->setData($fonts);
return $this->render($request, $response);
}
/**
* Font details provided by FontLib
*
* @SWG\Get(
* path="/fonts/details/{id}",
* operationId="fontDetails",
* tags={"font"},
* summary="Font Details",
* description="Get the Font details",
* @SWG\Parameter(
* name="id",
* in="path",
* description="The Font ID",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(
* type="object",
* additionalProperties={
* "title"="details",
* "type"="array"
* }
* )
* )
* )
*
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws NotFoundException
* @throws \FontLib\Exception\FontNotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function getFontLibDetails(Request $request, Response $response, $id)
{
$font = $this->fontFactory->getById($id);
$fontLib = \FontLib\Font::load($font->getFilePath());
$fontLib->parse();
$fontDetails = [
'Name' => $fontLib->getFontName(),
'SubFamily Name' => $fontLib->getFontSubfamily(),
'Subfamily ID' => $fontLib->getFontSubfamilyID(),
'Full Name' => $fontLib->getFontFullName(),
'Version' => $fontLib->getFontVersion(),
'Font Weight' => $fontLib->getFontWeight(),
'Font Postscript Name' => $fontLib->getFontPostscriptName(),
'Font Copyright' => $fontLib->getFontCopyright(),
];
$this->getState()->template = 'fonts-fontlib-details';
$this->getState()->setData([
'details' => $fontDetails,
'fontId' => $font->id
]);
return $this->render($request, $response);
}
/**
* @SWG\Get(
* path="/fonts/download/{id}",
* operationId="fontDownload",
* tags={"font"},
* summary="Download Font",
* description="Download a Font file from the Library",
* produces={"application/octet-stream"},
* @SWG\Parameter(
* name="id",
* in="path",
* description="The Font ID to Download",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(type="file"),
* @SWG\Header(
* header="X-Sendfile",
* description="Apache Send file header - if enabled.",
* type="string"
* ),
* @SWG\Header(
* header="X-Accel-Redirect",
* description="nginx send file header - if enabled.",
* type="string"
* )
* )
* )
*
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\GeneralException
*/
public function download(Request $request, Response $response, $id)
{
if (is_numeric($id)) {
$font = $this->fontFactory->getById($id);
} else {
$font = $this->fontFactory->getByName($id)[0];
}
$this->getLog()->debug('Download request for fontId ' . $id);
$library = $this->getConfig()->getSetting('LIBRARY_LOCATION');
$sendFileMode = $this->getConfig()->getSetting('SENDFILE_MODE');
$attachmentName = urlencode($font->fileName);
$libraryPath = $library . 'fonts' . DIRECTORY_SEPARATOR . $font->fileName;
$downLoadService = new DownloadService($libraryPath, $sendFileMode);
$downLoadService->useLogger($this->getLog()->getLoggerInterface());
return $downLoadService->returnFile($response, $attachmentName, '/download/fonts/' . $font->fileName);
}
/**
* @return string[]
*/
private function getValidExtensions()
{
return ['otf', 'ttf', 'eot', 'svg', 'woff'];
}
/**
* Font Upload
*
* @SWG\Post(
* path="/fonts",
* operationId="fontUpload",
* tags={"font"},
* summary="Font Upload",
* description="Upload a new Font file",
* @SWG\Parameter(
* name="files",
* in="formData",
* description="The Uploaded File",
* type="file",
* required=true
* ),
* @SWG\Parameter(
* name="name",
* in="formData",
* description="Optional Font Name",
* type="string",
* required=false
* ),
* @SWG\Response(
* response=200,
* description="successful operation"
* )
* )
*
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws ConfigurationException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\DuplicateEntityException
*/
public function add(Request $request, Response $response)
{
if (!$this->getUser()->featureEnabled('font.add')) {
throw new AccessDeniedException();
}
$libraryFolder = $this->getConfig()->getSetting('LIBRARY_LOCATION');
// Make sure the library exists
MediaService::ensureLibraryExists($libraryFolder);
$validExt = $this->getValidExtensions();
// Make sure there is room in the library
$libraryLimit = $this->getConfig()->getSetting('LIBRARY_SIZE_LIMIT_KB') * 1024;
$options = [
'accept_file_types' => '/\.' . implode('|', $validExt) . '$/i',
'libraryLimit' => $libraryLimit,
'libraryQuotaFull' => ($libraryLimit > 0 && $this->getMediaService()->libraryUsage() > $libraryLimit),
];
// Output handled by UploadHandler
$this->setNoOutput(true);
$this->getLog()->debug('Hand off to Upload Handler with options: ' . json_encode($options));
// Hand off to the Upload Handler provided by jquery-file-upload
$uploadService = new UploadService($libraryFolder . 'temp/', $options, $this->getLog(), $this->getState());
$uploadHandler = $uploadService->createUploadHandler();
$uploadHandler->setPostProcessor(function ($file, $uploadHandler) use ($libraryFolder) {
// Return right away if the file already has an error.
if (!empty($file->error)) {
return $file;
}
$this->getUser()->isQuotaFullByUser(true);
// Get the uploaded file and move it to the right place
$filePath = $libraryFolder . 'temp/' . $file->fileName;
// Add the Font
$font = $this->getFontFactory()
->createFontFromUpload($filePath, $file->name, $file->fileName, $this->getUser()->userName);
$font->save();
// Test to ensure the final file size is the same as the file size we're expecting
if ($file->size != $font->size) {
throw new InvalidArgumentException(
__('Sorry this is a corrupted upload, the file size doesn\'t match what we\'re expecting.'),
'size'
);
}
// everything is fine, move the file from temp folder.
rename($filePath, $libraryFolder . 'fonts/' . $font->fileName);
// return
$file->id = $font->id;
$file->md5 = $font->md5;
$file->name = $font->name;
return $file;
});
// Handle the post request
$uploadHandler->post();
// all done, refresh fonts.css
$this->getMediaService()->updateFontsCss();
// Explicitly set the Content-Type header to application/json
$response = $response->withHeader('Content-Type', 'application/json');
return $this->render($request, $response);
}
/**
* Font Delete Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function deleteForm(Request $request, Response $response, $id)
{
if (!$this->getUser()->featureEnabled('font.delete')) {
throw new AccessDeniedException();
}
if (is_numeric($id)) {
$font = $this->fontFactory->getById($id);
} else {
$font = $this->fontFactory->getByName($id)[0];
}
$this->getState()->template = 'font-form-delete';
$this->getState()->setData([
'font' => $font
]);
return $this->render($request, $response);
}
/**
* Font Delete
*
* @SWG\Delete(
* path="/fonts/{id}/delete",
* operationId="fontDelete",
* tags={"font"},
* summary="Font Delete",
* description="Delete existing Font file",
* @SWG\Parameter(
* name="id",
* in="path",
* description="The Font ID to delete",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws ConfigurationException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\DuplicateEntityException
*/
public function delete(Request $request, Response $response, $id)
{
if (!$this->getUser()->featureEnabled('font.delete')) {
throw new AccessDeniedException();
}
if (is_numeric($id)) {
$font = $this->fontFactory->getById($id);
} else {
$font = $this->fontFactory->getByName($id)[0];
}
// delete record and file
$font->delete();
// refresh fonts.css
$this->getMediaService()->updateFontsCss();
return $this->render($request, $response);
}
/**
* Return the CMS flavored font css
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface
* @throws ConfigurationException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\DuplicateEntityException
*/
public function fontCss(Request $request, Response $response)
{
$tempFileName = $this->getConfig()->getSetting('LIBRARY_LOCATION') . 'fonts/local_fontcss';
$cacheItem = $this->getMediaService()->getPool()->getItem('localFontCss');
$cacheItem->setInvalidationMethod(Invalidation::SLEEP, 5000, 15);
if ($cacheItem->isMiss()) {
$this->getLog()->debug('local font css cache has expired, regenerating');
$cacheItem->lock(60);
$localCss = '';
// Regenerate the CSS for fonts
foreach ($this->fontFactory->query() as $font) {
// Go through all installed fonts each time and regenerate.
$fontTemplate = '@font-face {
font-family: \'[family]\';
src: url(\'[url]\');
}';
// Css for the local CMS contains the full download path to the font
$url = $this->urlFor($request, 'font.download', ['id' => $font->id]);
$localCss .= str_replace('[url]', $url, str_replace('[family]', $font->familyName, $fontTemplate));
}
// cache
$cacheItem->set($localCss);
$cacheItem->expiresAfter(new \DateInterval('P30D'));
$this->getMediaService()->getPool()->saveDeferred($cacheItem);
} else {
$this->getLog()->debug('local font css file served from cache ');
$localCss = $cacheItem->get();
}
// Return the CSS to the browser as a file
$out = fopen($tempFileName, 'w');
if (!$out) {
throw new ConfigurationException(__('Unable to write to the library'));
}
fputs($out, $localCss);
fclose($out);
// Work out the etag
$response = HttpCacheProvider::withEtag($response, md5($localCss));
$this->setNoOutput(true);
$response = $response->withHeader('Content-Type', 'text/css')
->withBody(new Stream(fopen($tempFileName, 'r')));
return $this->render($request, $response);
}
}

View File

@@ -0,0 +1,52 @@
<?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\Controller;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Slim\Views\Twig;
use Xibo\Helper\SanitizerService;
use Xibo\Service\ConfigServiceInterface;
use Xibo\Service\LogServiceInterface;
/**
* Class IconDashboard
* @package Xibo\Controller
*/
class IconDashboard extends Base
{
/**
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function displayPage(Request $request, Response $response)
{
$this->getState()->template = 'dashboard-icon-page';
return $this->render($request, $response);
}
}

3540
lib/Controller/Layout.php Normal file

File diff suppressed because it is too large Load Diff

3007
lib/Controller/Library.php Normal file

File diff suppressed because it is too large Load Diff

166
lib/Controller/Logging.php Normal file
View File

@@ -0,0 +1,166 @@
<?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\Controller;
use Carbon\Carbon;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Factory\LogFactory;
use Xibo\Factory\UserFactory;
use Xibo\Helper\DateFormatHelper;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\AccessDeniedException;
/**
* Class Logging
* @package Xibo\Controller
*/
class Logging extends Base
{
/**
* @var LogFactory
*/
private $logFactory;
/** @var StorageServiceInterface */
private $store;
/** @var UserFactory */
private $userFactory;
/**
* Set common dependencies.
* @param StorageServiceInterface $store
* @param LogFactory $logFactory
* @param UserFactory $userFactory
*/
public function __construct($store, $logFactory, $userFactory)
{
$this->store = $store;
$this->logFactory = $logFactory;
$this->userFactory = $userFactory;
}
/**
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function displayPage(Request $request, Response $response)
{
$this->getState()->template = 'log-page';
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
function grid(Request $request, Response $response)
{
$parsedQueryParams = $this->getSanitizer($request->getQueryParams());
// Date time criteria
$seconds = $parsedQueryParams->getInt('seconds', ['default' => 120]);
$intervalType = $parsedQueryParams->getInt('intervalType', ['default' => 1]);
$fromDt = $parsedQueryParams->getDate('fromDt', ['default' => Carbon::now()]);
$logs = $this->logFactory->query($this->gridRenderSort($parsedQueryParams), $this->gridRenderFilter([
'fromDt' => $fromDt->clone()->subSeconds($seconds * $intervalType)->format('U'),
'toDt' => $fromDt->format('U'),
'type' => $parsedQueryParams->getString('level'),
'page' => $parsedQueryParams->getString('page'),
'channel' => $parsedQueryParams->getString('channel'),
'function' => $parsedQueryParams->getString('function'),
'displayId' => $parsedQueryParams->getInt('displayId'),
'userId' => $parsedQueryParams->getInt('userId'),
'excludeLog' => $parsedQueryParams->getCheckbox('excludeLog'),
'runNo' => $parsedQueryParams->getString('runNo'),
'message' => $parsedQueryParams->getString('message'),
'display' => $parsedQueryParams->getString('display'),
'useRegexForName' => $parsedQueryParams->getCheckbox('useRegexForName'),
'displayGroupId' => $parsedQueryParams->getInt('displayGroupId'),
], $parsedQueryParams));
foreach ($logs as $log) {
// Normalise the date
$log->logDate = Carbon::createFromTimeString($log->logDate)->format(DateFormatHelper::getSystemFormat());
}
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = $this->logFactory->countLast();
$this->getState()->setData($logs);
return $this->render($request, $response);
}
/**
* Truncate Log Form
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function truncateForm(Request $request, Response $response)
{
if ($this->getUser()->userTypeId != 1) {
throw new AccessDeniedException(__('Only Administrator Users can truncate the log'));
}
$this->getState()->template = 'log-form-truncate';
$this->getState()->autoSubmit = $this->getAutoSubmit('truncateForm');
return $this->render($request, $response);
}
/**
* Truncate the Log
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function truncate(Request $request, Response $response)
{
if ($this->getUser()->userTypeId != 1) {
throw new AccessDeniedException(__('Only Administrator Users can truncate the log'));
}
$this->store->update('TRUNCATE TABLE log', array());
// Return
$this->getState()->hydrate([
'message' => __('Log Truncated')
]);
return $this->render($request, $response);
}
}

707
lib/Controller/Login.php Normal file
View File

@@ -0,0 +1,707 @@
<?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\Controller;
use RobThree\Auth\TwoFactorAuth;
use Slim\Flash\Messages;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Slim\Routing\RouteContext;
use Xibo\Entity\User;
use Xibo\Factory\UserFactory;
use Xibo\Helper\Environment;
use Xibo\Helper\HttpsDetect;
use Xibo\Helper\LogoutTrait;
use Xibo\Helper\Random;
use Xibo\Helper\Session;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\ConfigurationException;
use Xibo\Support\Exception\ExpiredException;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
/**
* Class Login
* @package Xibo\Controller
*/
class Login extends Base
{
use LogoutTrait;
/** @var Session */
private $session;
/** @var UserFactory */
private $userFactory;
/** @var \Stash\Interfaces\PoolInterface */
private $pool;
/**
* @var Messages
*/
private $flash;
/**
* Set common dependencies.
* @param Session $session
* @param UserFactory $userFactory
* @param \Stash\Interfaces\PoolInterface $pool
*/
public function __construct($session, $userFactory, $pool)
{
$this->session = $session;
$this->userFactory = $userFactory;
$this->pool = $pool;
}
/**
* Get Flash Message
*
* @return Messages
*/
protected function getFlash()
{
return $this->flash;
}
public function setFlash(Messages $messages)
{
$this->flash = $messages;
}
/**
* Output a login form
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface
* @throws GeneralException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function loginForm(Request $request, Response $response)
{
// Sanitize the body
$sanitizedRequestBody = $this->getSanitizer($request->getParams());
// Check to see if the user has provided a special token
$nonce = $sanitizedRequestBody->getString('nonce');
if ($nonce != '') {
// We have a nonce provided, so validate that in preference to showing the form.
$nonce = explode('::', $nonce);
$this->getLog()->debug('Nonce is ' . var_export($nonce, true));
$cache = $this->pool->getItem('/nonce/' . $nonce[0]);
$validated = $cache->get();
if ($cache->isMiss()) {
$this->getLog()->error('Expired nonce used.');
$this->getFlash()->addMessageNow('login_message', __('This link has expired.'));
} else if (!password_verify($nonce[1], $validated['hash'])) {
$this->getLog()->error('Invalid nonce used.');
$this->getFlash()->addMessageNow('login_message', __('This link has expired.'));
} else {
// We're valid.
$this->pool->deleteItem('/nonce/' . $nonce[0]);
try {
$user = $this->userFactory->getById($validated['userId']);
// Log in this user
$user->touch(true);
$this->getLog()->info($user->userName . ' user logged in via token.');
// Set the userId on the log object
$this->getLog()->setUserId($user->userId);
$this->getLog()->setIpAddress($request->getAttribute('ip_address'));
// Expire all sessions
$session = $this->session;
// this is a security measure in case the user is logged in somewhere else.
// (not this one though, otherwise we will deadlock
$session->expireAllSessionsForUser($user->userId);
// Switch Session ID's
$session->setIsExpired(0);
$session->regenerateSessionId();
$session->setUser($user->userId);
$this->getLog()->setSessionHistoryId($session->get('sessionHistoryId'));
// Audit Log
$this->getLog()->audit('User', $user->userId, 'Login Granted via token', [
'UserAgent' => $request->getHeader('User-Agent')
]);
return $response->withRedirect($this->urlFor($request, 'home'));
} catch (NotFoundException $notFoundException) {
$this->getLog()->error('Valid nonce for non-existing user');
$this->getFlash()->addMessageNow('login_message', __('This link has expired.'));
}
}
}
// Check to see if the password reminder functionality is enabled.
$passwordReminderEnabled = $this->getConfig()->getSetting('PASSWORD_REMINDER_ENABLED');
$mailFrom = $this->getConfig()->getSetting('mail_from');
$authCASEnabled = isset($this->getConfig()->casSettings);
// Template
$this->getState()->template = 'login';
$this->getState()->setData([
'passwordReminderEnabled' => (($passwordReminderEnabled === 'On' || $passwordReminderEnabled === 'On except Admin') && $mailFrom != ''),
'authCASEnabled' => $authCASEnabled,
'version' => Environment::$WEBSITE_VERSION_NAME
]);
return $this->render($request, $response);
}
/**
* Login
* @param Request $request
* @param Response $response
* @return \Slim\Http\Response
* @throws \Xibo\Support\Exception\DuplicateEntityException
* @throws \Xibo\Support\Exception\InvalidArgumentException
*/
public function login(Request $request, Response $response): Response
{
$parsedRequest = $this->getSanitizer($request->getParsedBody());
$routeParser = RouteContext::fromRequest($request)->getRouteParser();
// Capture the prior route (if there is one)
$redirect = $this->urlFor($request, 'login');
$priorRoute = $parsedRequest->getString('priorRoute');
try {
// Get our username and password
$username = $parsedRequest->getString('username');
$password = $parsedRequest->getString('password');
$this->getLog()->debug('Login with username ' . $username);
// Get our user
try {
$user = $this->userFactory->getByName($username);
// Retired user
if ($user->retired === 1) {
throw new AccessDeniedException(
__('Sorry this account does not exist or does not have permission to access the web portal.')
);
}
// Check password
$user->checkPassword($password);
// check if 2FA is enabled
if ($user->twoFactorTypeId != 0) {
$_SESSION['tfaUsername'] = $user->userName;
$this->getFlash()->addMessage('priorRoute', $priorRoute);
return $response->withRedirect($routeParser->urlFor('tfa'));
}
// We are logged in, so complete the login flow
$this->completeLoginFlow($user, $request);
} catch (NotFoundException) {
throw new AccessDeniedException(__('User not found'));
}
$redirect = $this->getRedirect($request, $priorRoute);
} catch (AccessDeniedException $e) {
$this->getLog()->warning($e->getMessage());
$this->getFlash()->addMessage('login_message', __('Username or Password incorrect'));
$this->getFlash()->addMessage('priorRoute', $priorRoute);
} catch (ExpiredException $e) {
$this->getFlash()->addMessage('priorRoute', $priorRoute);
}
$this->setNoOutput(true);
$this->getLog()->debug('Redirect to ' . $redirect);
return $response->withRedirect($redirect);
}
/**
* Forgotten password link requested
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws ConfigurationException
* @throws \PHPMailer\PHPMailer\Exception
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
public function forgottenPassword(Request $request, Response $response)
{
// Is this functionality enabled?
$passwordReminderEnabled = $this->getConfig()->getSetting('PASSWORD_REMINDER_ENABLED');
$mailFrom = $this->getConfig()->getSetting('mail_from');
$parsedRequest = $this->getSanitizer($request->getParsedBody());
$routeParser = RouteContext::fromRequest($request)->getRouteParser();
if (!(($passwordReminderEnabled === 'On' || $passwordReminderEnabled === 'On except Admin') && $mailFrom != '')) {
throw new ConfigurationException(__('This feature has been disabled by your administrator'));
}
// Get our username
$username = $parsedRequest->getString('username');
// Log
$this->getLog()->info('Forgotten Password Request for ' . $username);
// Check to see if the provided username is valid, and if so, record a nonce and send them a link
try {
// Get our user
/* @var User $user */
$user = $this->userFactory->getByName($username);
// Does this user have an email address associated to their user record?
if ($user->email == '') {
throw new NotFoundException(__('No email'));
}
// Nonce parts (nonce isn't ever stored, only the hash of it is stored, it only exists in the email)
$action = 'user-reset-password-' . Random::generateString(10);
$nonce = Random::generateString(20);
// Create a nonce for this user and store it somewhere
$cache = $this->pool->getItem('/nonce/' . $action);
$cache->set([
'action' => $action,
'hash' => password_hash($nonce, PASSWORD_DEFAULT),
'userId' => $user->userId
]);
$cache->expiresAfter(1800); // 30 minutes?
// Save cache
$this->pool->save($cache);
// Make a link
$link = ((new HttpsDetect())->getRootUrl()) . $routeParser->urlFor('login') . '?nonce=' . $action . '::' . $nonce;
// Uncomment this to get a debug message showing the link.
//$this->getLog()->debug('Link is:' . $link);
// Send the mail
$mail = new \PHPMailer\PHPMailer\PHPMailer();
$mail->CharSet = 'UTF-8';
$mail->Encoding = 'base64';
$mail->From = $mailFrom;
$msgFromName = $this->getConfig()->getSetting('mail_from_name');
if ($msgFromName != null) {
$mail->FromName = $msgFromName;
}
$mail->Subject = __('Password Reset');
$mail->addAddress($user->email);
// Body
$mail->isHTML(true);
// We need to specify the style for the pw reset button since mailers usually ignore bootstrap classes
$linkButton = '<a href="' . $link . '"
style="
display: inline-block;
padding: 8px 15px;
font-size: 15px;
color: #FFFFFF;
background-color: #428BCA;
text-decoration: none;
border-radius: 5px;
">
' . __('Reset Password') . '
</a>';
$mail->Body = $this->generateEmailBody(
$mail->Subject,
'<p>' . __('You are receiving this email because a password reminder was requested for your account.
If you did not make this request, please report this email to your administrator immediately.') . '</p>'
. $linkButton
. '<p style="margin-top:10px; font-size:14px; color:#555555;">'
. __('If the button does not work, copy and paste the following URL into your browser:')
. '<br><a href="' . $link . '">' . $link . '</a></p>'
);
if (!$mail->send()) {
throw new ConfigurationException('Unable to send password reminder to ' . $user->email);
} else {
$this->getFlash()->addMessage(
'login_message',
__('A reminder email will been sent to this user if they exist'),
);
}
// Audit Log
$this->getLog()->audit('User', $user->userId, 'Password Reset Link Granted', [
'UserAgent' => $request->getHeader('User-Agent')
]);
} catch (GeneralException) {
$this->getFlash()->addMessage(
'login_message',
__('A reminder email will been sent to this user if they exist'),
);
}
$this->setNoOutput(true);
return $response->withRedirect($routeParser->urlFor('login'));
}
/**
* Log out
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
*/
public function logout(Request $request, Response $response)
{
$redirect = true;
if ($request->getQueryParam('redirect') != null) {
$redirect = $request->getQueryParam('redirect');
}
$this->completeLogoutFlow($this->getUser(), $this->session, $this->getLog(), $request);
if ($redirect) {
return $response->withRedirect($this->urlFor($request, 'home'));
}
return $response->withStatus(200);
}
/**
* Ping Pong
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function PingPong(Request $request, Response $response)
{
$parseRequest = $this->getSanitizer($request->getQueryParams());
$this->session->refreshExpiry = ($parseRequest->getCheckbox('refreshSession') == 1);
$this->getState()->success = true;
return $this->render($request, $response);
}
/**
* Shows information about Xibo
*
* @SWG\Get(
* path="/about",
* operationId="about",
* tags={"misc"},
* summary="About",
* description="Information about this API, such as Version code, etc",
* @SWG\Response(
* response=200,
* description="successful response",
* @SWG\Schema(
* type="object",
* additionalProperties={
* "title"="version",
* "type"="string"
* }
* )
* )
* )
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function about(Request $request, Response $response)
{
$state = $this->getState();
if ($request->isXhr()) {
$state->template = 'about-text';
} else {
$state->template = 'about-page';
}
$state->setData(['version' => Environment::$WEBSITE_VERSION_NAME, 'sourceUrl' => $this->getConfig()->getThemeConfig('cms_source_url')]);
return $this->render($request, $response);
}
/**
* Generate an email body
* @param $subject
* @param $body
* @return string
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
private function generateEmailBody($subject, $body)
{
return $this->renderTemplateToString('email-template', [
'config' => $this->getConfig(),
'subject' => $subject, 'body' => $body
]);
}
/**
* 2FA Auth required
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \PHPMailer\PHPMailer\Exception
* @throws \RobThree\Auth\TwoFactorAuthException
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function twoFactorAuthForm(Request $request, Response $response)
{
if (!isset($_SESSION['tfaUsername'])) {
$this->getFlash()->addMessage('login_message', __('Session has expired, please log in again'));
return $response->withRedirect($this->urlFor($request, 'login'));
}
$user = $this->userFactory->getByName($_SESSION['tfaUsername']);
$message = '';
// if our user has email two factor enabled, we need to send the email with code now
if ($user->twoFactorTypeId === 1) {
if ($user->email == '') {
throw new NotFoundException(__('No email'));
}
$mailFrom = $this->getConfig()->getSetting('mail_from');
$issuerSettings = $this->getConfig()->getSetting('TWOFACTOR_ISSUER');
$appName = $this->getConfig()->getThemeConfig('app_name');
if ($issuerSettings !== '') {
$issuer = $issuerSettings;
} else {
$issuer = $appName;
}
if ($mailFrom == '') {
throw new InvalidArgumentException(__('Sending email address in CMS Settings is not configured'), 'mail_from');
}
$tfa = new TwoFactorAuth($issuer);
// Nonce parts (nonce isn't ever stored, only the hash of it is stored, it only exists in the email)
$action = 'user-tfa-email-auth' . Random::generateString(10);
$nonce = Random::generateString(20);
// Create a nonce for this user and store it somewhere
$cache = $this->pool->getItem('/nonce/' . $action);
$cache->set([
'action' => $action,
'hash' => password_hash($nonce, PASSWORD_DEFAULT),
'userId' => $user->userId
]);
$cache->expiresAfter(1800); // 30 minutes?
// Save cache
$this->pool->save($cache);
// Make a link
$code = $tfa->getCode($user->twoFactorSecret);
// Send the mail
$mail = new \PHPMailer\PHPMailer\PHPMailer();
$mail->CharSet = 'UTF-8';
$mail->Encoding = 'base64';
$mail->From = $mailFrom;
$msgFromName = $this->getConfig()->getSetting('mail_from_name');
if ($msgFromName != null) {
$mail->FromName = $msgFromName;
}
$mail->Subject = __('Two Factor Authentication');
$mail->addAddress($user->email);
// Body
$mail->isHTML(true);
$mail->Body = $this->generateEmailBody($mail->Subject,
'<p>' . __('You are receiving this email because two factor email authorisation is enabled in your CMS user account. If you did not make this request, please report this email to your administrator immediately.') . '</p>' . '<p>' . $code . '</p>');
if (!$mail->send()) {
$message = __('Unable to send two factor code to email address associated with this user');
} else {
$message = __('Two factor code email has been sent to your email address');
// Audit Log
$this->getLog()->audit('User', $user->userId, 'Two Factor Code email sent', [
'UserAgent' => $request->getHeader('User-Agent')
]);
}
}
// Template
$this->getState()->template = 'tfa';
// the flash message do not work well here - need to reload the page to see the message, hence the below
$this->getState()->setData(['message' => $message]);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @return \Slim\Http\Response
* @throws \RobThree\Auth\TwoFactorAuthException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function twoFactorAuthValidate(Request $request, Response $response): Response
{
$user = $this->userFactory->getByName($_SESSION['tfaUsername']);
$result = false;
$updatedCodes = [];
$sanitizedParams = $this->getSanitizer($request->getParams());
if (isset($_POST['code'])) {
$issuerSettings = $this->getConfig()->getSetting('TWOFACTOR_ISSUER');
$appName = $this->getConfig()->getThemeConfig('app_name');
if ($issuerSettings !== '') {
$issuer = $issuerSettings;
} else {
$issuer = $appName;
}
$tfa = new TwoFactorAuth($issuer);
if ($user->twoFactorTypeId === 1 && $user->email !== '') {
$result = $tfa->verifyCode($user->twoFactorSecret, $sanitizedParams->getString('code'), 9);
} else {
$result = $tfa->verifyCode($user->twoFactorSecret, $sanitizedParams->getString('code'), 3);
}
} elseif (isset($_POST['recoveryCode'])) {
// get the array of recovery codes, go through them and try to match provided code
$codes = $user->twoFactorRecoveryCodes;
foreach (json_decode($codes) as $code) {
// if the provided recovery code matches one stored in the database, we want to log in the user
if ($code === $sanitizedParams->getString('recoveryCode')) {
$result = true;
}
if ($code !== $sanitizedParams->getString('recoveryCode')) {
$updatedCodes[] = $code;
}
}
// recovery codes are one time use, as such we want to update user recovery codes and remove the one that
// was just used.
$user->updateRecoveryCodes(json_encode($updatedCodes));
}
if ($result) {
// We are logged in at this point
$this->completeLoginFlow($user, $request);
$this->setNoOutput(true);
//unset the session tfaUsername
unset($_SESSION['tfaUsername']);
return $response->withRedirect($this->getRedirect($request, $sanitizedParams->getString('priorRoute')));
} else {
$this->getLog()->error('Authentication code incorrect, redirecting to login page');
$this->getFlash()->addMessage('login_message', __('Authentication code incorrect'));
return $response->withRedirect($this->urlFor($request, 'login'));
}
}
/**
* @param \Xibo\Entity\User $user
* @param Request $request
*/
private function completeLoginFlow(User $user, Request $request): void
{
$user->touch();
$this->getLog()->info($user->userName . ' user logged in.');
// Set the userId on the log object
$this->getLog()->setUserId($user->userId);
$this->getLog()->setIpAddress($request->getAttribute('ip_address'));
// Switch Session ID's
$session = $this->session;
$session->setIsExpired(0);
$session->regenerateSessionId();
$session->setUser($user->userId);
$this->getLog()->setSessionHistoryId($session->get('sessionHistoryId'));
// Audit Log
$this->getLog()->audit('User', $user->userId, 'Login Granted', [
'UserAgent' => $request->getHeader('User-Agent')
]);
}
/**
* Get a redirect link from the given request and prior route
* validate the prior route by only taking its path
* @param \Slim\Http\ServerRequest $request
* @param string|null $priorRoute
* @return string
*/
private function getRedirect(Request $request, ?string $priorRoute): string
{
$home = $this->urlFor($request, 'home');
// Parse the prior route
$parsedPriorRoute = parse_url($priorRoute);
if (!$parsedPriorRoute) {
$priorRoute = $home;
} else {
$priorRoute = $parsedPriorRoute['path'];
}
// Certain routes always lead home
if ($priorRoute == ''
|| $priorRoute == '/'
|| str_contains($priorRoute, $this->urlFor($request, 'login'))
) {
$redirectTo = $home;
} else {
$redirectTo = $priorRoute;
}
return $redirectTo;
}
}

View File

@@ -0,0 +1,288 @@
<?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\Controller;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Event\MediaDeleteEvent;
use Xibo\Factory\MediaFactory;
use Xibo\Service\MediaServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\ControllerNotImplemented;
use Xibo\Support\Exception\GeneralException;
/**
* Class Maintenance
* @package Xibo\Controller
*/
class Maintenance extends Base
{
/** @var StorageServiceInterface */
private $store;
/** @var MediaFactory */
private $mediaFactory;
/** @var MediaServiceInterface */
private $mediaService;
/**
* Set common dependencies.
* @param StorageServiceInterface $store
* @param MediaFactory $mediaFactory
* @param MediaServiceInterface $mediaService
*/
public function __construct($store, $mediaFactory, MediaServiceInterface $mediaService)
{
$this->store = $store;
$this->mediaFactory = $mediaFactory;
$this->mediaService = $mediaService;
}
/**
* Tidy Library Form
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws ControllerNotImplemented
* @throws GeneralException
*/
public function tidyLibraryForm(Request $request, Response $response)
{
$this->getState()->template = 'maintenance-form-tidy';
return $this->render($request, $response);
}
/**
* Tidies up the library
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws ControllerNotImplemented
* @throws GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function tidyLibrary(Request $request, Response $response)
{
$sanitizedParams = $this->getSanitizer($request->getParams());
$tidyOldRevisions = $sanitizedParams->getCheckbox('tidyOldRevisions');
$cleanUnusedFiles = $sanitizedParams->getCheckbox('cleanUnusedFiles');
$tidyGenericFiles = $sanitizedParams->getCheckbox('tidyGenericFiles');
if ($this->getConfig()->getSetting('SETTING_LIBRARY_TIDY_ENABLED') != 1) {
throw new AccessDeniedException(__('Sorry this function is disabled.'));
}
$this->getLog()->audit('Media', 0, 'Tidy library started from Settings', [
'tidyOldRevisions' => $tidyOldRevisions,
'cleanUnusedFiles' => $cleanUnusedFiles,
'tidyGenericFiles' => $tidyGenericFiles,
'initiator' => $this->getUser()->userId
]);
// Also run a script to tidy up orphaned media in the library
$library = $this->getConfig()->getSetting('LIBRARY_LOCATION');
$this->getLog()->debug('Library Location: ' . $library);
// Remove temporary files
$this->mediaService->removeTempFiles();
$media = [];
$unusedMedia = [];
$unusedRevisions = [];
// DataSets with library images
$dataSetSql = '
SELECT dataset.dataSetId, datasetcolumn.heading
FROM dataset
INNER JOIN datasetcolumn
ON datasetcolumn.DataSetID = dataset.DataSetID
WHERE DataTypeID = 5 AND DataSetColumnTypeID <> 2;
';
$dataSets = $this->store->select($dataSetSql, []);
// Run a query to get an array containing all of the media in the library
// this must contain ALL media, so that we can delete files in the storage that aren;t in the table
$sql = '
SELECT media.mediaid, media.storedAs, media.type, media.isedited,
SUM(CASE WHEN IFNULL(lkwidgetmedia.widgetId, 0) = 0 THEN 0 ELSE 1 END) AS UsedInLayoutCount,
SUM(CASE WHEN IFNULL(lkmediadisplaygroup.mediaId, 0) = 0 THEN 0 ELSE 1 END) AS UsedInDisplayCount,
SUM(CASE WHEN IFNULL(layout.layoutId, 0) = 0 THEN 0 ELSE 1 END) AS UsedInBackgroundImageCount,
SUM(CASE WHEN IFNULL(menu_category.menuCategoryId, 0) = 0 THEN 0 ELSE 1 END) AS UsedInMenuBoardCategoryCount,
SUM(CASE WHEN IFNULL(menu_product.menuProductId, 0) = 0 THEN 0 ELSE 1 END) AS UsedInMenuBoardProductCount
';
if (count($dataSets) > 0) {
$sql .= ' , SUM(CASE WHEN IFNULL(dataSetImages.mediaId, 0) = 0 THEN 0 ELSE 1 END) AS UsedInDataSetCount ';
} else {
$sql .= ' , 0 AS UsedInDataSetCount ';
}
$sql .= '
FROM `media`
LEFT OUTER JOIN `lkwidgetmedia`
ON lkwidgetmedia.mediaid = media.mediaid
LEFT OUTER JOIN `lkmediadisplaygroup`
ON lkmediadisplaygroup.mediaid = media.mediaid
LEFT OUTER JOIN `layout`
ON `layout`.backgroundImageId = `media`.mediaId
LEFT OUTER JOIN `menu_category`
ON `menu_category`.mediaId = `media`.mediaId
LEFT OUTER JOIN `menu_product`
ON `menu_product`.mediaId = `media`.mediaId
';
if (count($dataSets) > 0) {
$sql .= ' LEFT OUTER JOIN (';
$first = true;
foreach ($dataSets as $dataSet) {
$sanitizedDataSet = $this->getSanitizer($dataSet);
if (!$first)
$sql .= ' UNION ALL ';
$first = false;
$dataSetId = $sanitizedDataSet->getInt('dataSetId');
$heading = $sanitizedDataSet->getString('heading');
$sql .= ' SELECT `' . $heading . '` AS mediaId FROM `dataset_' . $dataSetId . '`';
}
$sql .= ') dataSetImages
ON dataSetImages.mediaId = `media`.mediaId
';
}
$sql .= '
GROUP BY media.mediaid, media.storedAs, media.type, media.isedited
';
foreach ($this->store->select($sql, []) as $row) {
$media[$row['storedAs']] = $row;
$sanitizedRow = $this->getSanitizer($row);
$type = $sanitizedRow->getString('type');
// Ignore any module files or fonts
if ($type == 'module'
|| $type == 'font'
|| $type == 'playersoftware'
|| ($type == 'genericfile' && $tidyGenericFiles != 1)
) {
continue;
}
// Collect media revisions that aren't used
if ($tidyOldRevisions && $this->isSafeToDelete($row) && $row['isedited'] > 0) {
$unusedRevisions[$row['storedAs']] = $row;
}
// Collect any files that aren't used
else if ($cleanUnusedFiles && $this->isSafeToDelete($row)) {
$unusedMedia[$row['storedAs']] = $row;
}
}
$i = 0;
// Library location
$libraryLocation = $this->getConfig()->getSetting("LIBRARY_LOCATION");
// Get a list of all media files
foreach(scandir($library) as $file) {
if ($file == '.' || $file == '..')
continue;
if (is_dir($library . $file))
continue;
// Ignore thumbnails
if (strstr($file, 'tn_'))
continue;
// Ignore XLF files
if (strstr($file, '.xlf'))
continue;
$i++;
// Is this file in the system anywhere?
if (!array_key_exists($file, $media)) {
// Totally missing
$this->getLog()->alert('tidyLibrary: Deleting file which is not in the media table: ' . $file);
// If not, delete it
unlink($libraryLocation . $file);
} else if (array_key_exists($file, $unusedRevisions)) {
// It exists but isn't being used anymore
$this->getLog()->alert('tidyLibrary: Deleting unused revision media: ' . $media[$file]['mediaid']);
$mediaToDelete = $this->mediaFactory->getById($media[$file]['mediaid']);
$this->getDispatcher()->dispatch(new MediaDeleteEvent($mediaToDelete), MediaDeleteEvent::$NAME);
$mediaToDelete->delete();
} else if (array_key_exists($file, $unusedMedia)) {
// It exists but isn't being used anymore
$this->getLog()->alert('tidyLibrary: Deleting unused media: ' . $media[$file]['mediaid']);
$mediaToDelete = $this->mediaFactory->getById($media[$file]['mediaid']);
$this->getDispatcher()->dispatch(new MediaDeleteEvent($mediaToDelete), MediaDeleteEvent::$NAME);
$mediaToDelete->delete();
} else {
$i--;
}
}
$this->getLog()->audit('Media', 0, 'Tidy library from settings complete', [
'countDeleted' => $i,
'initiator' => $this->getUser()->userId
]);
// Return
$this->getState()->hydrate([
'message' => __('Library Tidy Complete'),
'data' => [
'tidied' => $i
]
]);
return $this->render($request, $response);
}
private function isSafeToDelete($row): bool
{
return ($row['UsedInLayoutCount'] <= 0
&& $row['UsedInDisplayCount'] <= 0
&& $row['UsedInBackgroundImageCount'] <= 0
&& $row['UsedInDataSetCount'] <= 0
&& $row['UsedInMenuBoardCategoryCount'] <= 0
&& $row['UsedInMenuBoardProductCount'] <= 0
);
}
}

View File

@@ -0,0 +1,137 @@
<?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\Controller;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Factory\MediaFactory;
use Xibo\Factory\ModuleFactory;
use Xibo\Helper\ByteFormatter;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\NotFoundException;
/**
* Class MediaManager
* @package Xibo\Controller
*/
class MediaManager extends Base
{
private StorageServiceInterface $store;
private ModuleFactory $moduleFactory;
private MediaFactory $mediaFactory;
/**
* Set common dependencies.
*/
public function __construct(
StorageServiceInterface $store,
ModuleFactory $moduleFactory,
MediaFactory $mediaFactory
) {
$this->store = $store;
$this->moduleFactory = $moduleFactory;
$this->mediaFactory = $mediaFactory;
}
/**
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function displayPage(Request $request, Response $response)
{
$this->getState()->template = 'media-manager-page';
$this->getState()->setData([
'library' => $this->getLibraryUsage()
]);
return $this->render($request, $response);
}
/**
* Get the library usage
* @return array
* @throws \Xibo\Support\Exception\NotFoundException
*/
private function getLibraryUsage(): array
{
// Set up some suffixes
$suffixes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
$params = [];
// Library Size in Bytes
$sql = '
SELECT COUNT(`mediaId`) AS countOf,
IFNULL(SUM(`FileSize`), 0) AS SumSize,
`type`
FROM `media`
WHERE 1 = 1 ';
$this->mediaFactory->viewPermissionSql(
'Xibo\Entity\Media',
$sql,
$params,
'`media`.mediaId',
'`media`.userId',
[],
'media.permissionsFolderId'
);
$sql .= ' GROUP BY type ';
$sql .= ' ORDER BY 2 ';
$results = $this->store->select($sql, $params);
$libraryUsage = [];
$totalCount = 0;
$totalSize = 0;
foreach ($results as $library) {
$bytes = doubleval($library['SumSize']);
$totalSize += $bytes;
$totalCount += $library['countOf'];
try {
$title = $this->moduleFactory->getByType($library['type'])->name;
} catch (NotFoundException) {
$title = $library['type'] === 'module' ? __('Widget cache') : ucfirst($library['type']);
}
$libraryUsage[] = [
'title' => $title,
'count' => $library['countOf'],
'size' => $bytes,
];
}
// Decide what our units are going to be, based on the size
$base = ($totalSize === 0) ? 0 : floor(log($totalSize) / log(1024));
return [
'countOf' => $totalCount,
'size' => ByteFormatter::format($totalSize, 1, true),
'types' => $libraryUsage,
'typesSuffix' => $suffixes[$base],
'typesBase' => $base,
];
}
}

View File

@@ -0,0 +1,629 @@
<?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\Controller;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Factory\FolderFactory;
use Xibo\Factory\MenuBoardFactory;
use Xibo\Factory\UserFactory;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
/**
* Menu Board Controller
*/
class MenuBoard extends Base
{
/**
* Set common dependencies.
* @param MenuBoardFactory $menuBoardFactory
* @param FolderFactory $folderFactory
*/
public function __construct(
private readonly MenuBoardFactory $menuBoardFactory,
private readonly FolderFactory $folderFactory
) {
}
/**
* Displays the Menu Board Page
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function displayPage(Request $request, Response $response)
{
// Call to render the template
$this->getState()->template = 'menuboard-page';
return $this->render($request, $response);
}
/**
* Returns a Grid of Menu Boards
*
* @SWG\Get(
* path="/menuboards",
* operationId="menuBoardSearch",
* tags={"menuBoard"},
* summary="Search Menu Boards",
* description="Search all Menu Boards this user has access to",
* @SWG\Parameter(
* name="menuId",
* in="query",
* description="Filter by Menu board Id",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="userId",
* in="query",
* description="Filter by Owner Id",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="folderId",
* in="query",
* description="Filter by Folder Id",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="name",
* in="query",
* description="Filter by name",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="code",
* in="query",
* description="Filter by code",
* type="string",
* required=false
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="#/definitions/MenuBoard")
* )
* )
* )
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
*/
public function grid(Request $request, Response $response): Response
{
$parsedParams = $this->getSanitizer($request->getQueryParams());
$filter = [
'menuId' => $parsedParams->getInt('menuId'),
'userId' => $parsedParams->getInt('userId'),
'name' => $parsedParams->getString('name'),
'code' => $parsedParams->getString('code'),
'folderId' => $parsedParams->getInt('folderId'),
'logicalOperatorName' => $parsedParams->getString('logicalOperatorName'),
];
$menuBoards = $this->menuBoardFactory->query(
$this->gridRenderSort($parsedParams),
$this->gridRenderFilter($filter, $parsedParams)
);
foreach ($menuBoards as $menuBoard) {
if ($this->isApi($request)) {
continue;
}
$menuBoard->includeProperty('buttons');
$menuBoard->buttons = [];
if ($this->getUser()->featureEnabled('menuBoard.modify') && $this->getUser()->checkEditable($menuBoard)) {
$menuBoard->buttons[] = [
'id' => 'menuBoard_button_viewcategories',
'url' => $this->urlFor($request, 'menuBoard.category.view', ['id' => $menuBoard->menuId]),
'class' => 'XiboRedirectButton',
'text' => __('View Categories')
];
$menuBoard->buttons[] = [
'id' => 'menuBoard_edit_button',
'url' => $this->urlFor($request, 'menuBoard.edit.form', ['id' => $menuBoard->menuId]),
'text' => __('Edit')
];
if ($this->getUser()->featureEnabled('folder.view')) {
// Select Folder
$menuBoard->buttons[] = [
'id' => 'menuBoard_button_selectfolder',
'url' => $this->urlFor($request, 'menuBoard.selectfolder.form', ['id' => $menuBoard->menuId]),
'text' => __('Select Folder'),
'multi-select' => true,
'dataAttributes' => [
[
'name' => 'commit-url',
'value' => $this->urlFor($request, 'menuBoard.selectfolder', ['id' => $menuBoard->menuId])
],
['name' => 'commit-method', 'value' => 'put'],
['name' => 'id', 'value' => 'menuBoard_button_selectfolder'],
['name' => 'text', 'value' => __('Move to Folder')],
['name' => 'rowtitle', 'value' => $menuBoard->name],
['name' => 'form-callback', 'value' => 'moveFolderMultiSelectFormOpen']
]
];
}
}
if ($this->getUser()->featureEnabled('menuBoard.modify') && $this->getUser()->checkPermissionsModifyable($menuBoard)) {
$menuBoard->buttons[] = ['divider' => true];
// Share button
$menuBoard->buttons[] = [
'id' => 'menuBoard_button_permissions',
'url' => $this->urlFor($request, 'user.permissions.form', ['entity' => 'MenuBoard', 'id' => $menuBoard->menuId]),
'text' => __('Share'),
'dataAttributes' => [
[
'name' => 'commit-url',
'value' => $this->urlFor($request, 'user.permissions.multi', ['entity' => 'MenuBoard', 'id' => $menuBoard->menuId])
],
['name' => 'commit-method', 'value' => 'post'],
['name' => 'id', 'value' => 'menuBoard_button_permissions'],
['name' => 'text', 'value' => __('Share')],
['name' => 'rowtitle', 'value' => $menuBoard->name],
['name' => 'sort-group', 'value' => 2],
['name' => 'custom-handler', 'value' => 'XiboMultiSelectPermissionsFormOpen'],
[
'name' => 'custom-handler-url',
'value' => $this->urlFor($request, 'user.permissions.multi.form', ['entity' => 'MenuBoard'])
],
['name' => 'content-id-name', 'value' => 'menuId']
]
];
}
if ($this->getUser()->featureEnabled('menuBoard.modify')
&& $this->getUser()->checkDeleteable($menuBoard)
) {
$menuBoard->buttons[] = ['divider' => true];
$menuBoard->buttons[] = [
'id' => 'menuBoard_delete_button',
'url' => $this->urlFor($request, 'menuBoard.delete.form', ['id' => $menuBoard->menuId]),
'text' => __('Delete')
];
}
}
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = $this->menuBoardFactory->countLast();
$this->getState()->setData($menuBoards);
return $this->render($request, $response);
}
/**
* Menu Board Add Form
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
*/
public function addForm(Request $request, Response $response): Response
{
$this->getState()->template = 'menuboard-form-add';
return $this->render($request, $response);
}
/**
* Add a new Menu Board
*
* @SWG\Post(
* path="/menuboard",
* operationId="menuBoardAdd",
* tags={"menuBoard"},
* summary="Add Menu Board",
* description="Add a new Menu Board",
* @SWG\Parameter(
* name="name",
* in="formData",
* description="Menu Board name",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="description",
* in="formData",
* description="Menu Board description",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="code",
* in="formData",
* description="Menu Board code identifier",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="folderId",
* in="formData",
* description="Menu Board Folder Id",
* type="integer",
* required=false
* ),
* @SWG\Response(
* response=201,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/MenuBoard"),
* @SWG\Header(
* header="Location",
* description="Location of the new record",
* type="string"
* )
* )
* )
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws \Xibo\Support\Exception\InvalidArgumentException
*/
public function add(Request $request, Response $response): Response
{
$sanitizedParams = $this->getSanitizer($request->getParams());
$name = $sanitizedParams->getString('name');
$description = $sanitizedParams->getString('description');
$code = $sanitizedParams->getString('code');
$folderId = $sanitizedParams->getInt('folderId');
if ($folderId === 1) {
$this->checkRootFolderAllowSave();
}
if (empty($folderId) || !$this->getUser()->featureEnabled('folder.view')) {
$folderId = $this->getUser()->homeFolderId;
}
$folder = $this->folderFactory->getById($folderId, 0);
$menuBoard = $this->menuBoardFactory->create($name, $description, $code);
$menuBoard->folderId = $folder->getId();
$menuBoard->permissionsFolderId = $folder->getPermissionFolderIdOrThis();
$menuBoard->save();
// Return
$this->getState()->hydrate([
'message' => __('Added Menu Board'),
'httpStatus' => 201,
'id' => $menuBoard->menuId,
'data' => $menuBoard,
]);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @param int $id
* @return Response
* @throws GeneralException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function editForm(Request $request, Response $response, $id): Response
{
$menuBoard = $this->menuBoardFactory->getById($id);
if (!$this->getUser()->checkEditable($menuBoard)) {
throw new AccessDeniedException();
}
$this->getState()->template = 'menuboard-form-edit';
$this->getState()->setData([
'menuBoard' => $menuBoard
]);
return $this->render($request, $response);
}
/**
* @SWG\Put(
* path="/menuboard/{menuId}",
* operationId="menuBoardEdit",
* tags={"menuBoard"},
* summary="Edit Menu Board",
* description="Edit existing Menu Board",
* @SWG\Parameter(
* name="menuId",
* in="path",
* description="The Menu Board ID to Edit",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="name",
* in="formData",
* description="Menu Board name",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="description",
* in="formData",
* description="Menu Board description",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="code",
* in="formData",
* description="Menu Board code identifier",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="folderId",
* in="formData",
* description="Menu Board Folder Id",
* type="integer",
* required=false
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*
* @param Request $request
* @param Response $response
* @param int $id
* @return Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
*/
public function edit(Request $request, Response $response, $id): Response
{
$menuBoard = $this->menuBoardFactory->getById($id);
if (!$this->getUser()->checkEditable($menuBoard)) {
throw new AccessDeniedException();
}
$sanitizedParams = $this->getSanitizer($request->getParams());
$menuBoard->name = $sanitizedParams->getString('name');
$menuBoard->description = $sanitizedParams->getString('description');
$menuBoard->code = $sanitizedParams->getString('code');
$menuBoard->folderId = $sanitizedParams->getInt('folderId', ['default' => $menuBoard->folderId]);
if ($menuBoard->hasPropertyChanged('folderId')) {
if ($menuBoard->folderId === 1) {
$this->checkRootFolderAllowSave();
}
$folder = $this->folderFactory->getById($menuBoard->folderId);
$menuBoard->permissionsFolderId = ($folder->getPermissionFolderId() == null) ? $folder->id : $folder->getPermissionFolderId();
}
$menuBoard->save();
// Success
$this->getState()->hydrate([
'httpStatus' => 200,
'message' => sprintf(__('Edited %s'), $menuBoard->name),
'id' => $menuBoard->menuId,
'data' => $menuBoard
]);
return $this->render($request, $response);
}
/**
*
* @param Request $request
* @param Response $response
* @param int $id
* @return Response
* @throws GeneralException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function deleteForm(Request $request, Response $response, $id): Response
{
$menuBoard = $this->menuBoardFactory->getById($id);
if (!$this->getUser()->checkDeleteable($menuBoard)) {
throw new AccessDeniedException();
}
$this->getState()->template = 'menuboard-form-delete';
$this->getState()->setData([
'menuBoard' => $menuBoard
]);
return $this->render($request, $response);
}
/**
* @SWG\Delete(
* path="/menuboard/{menuId}",
* operationId="menuBoardDelete",
* tags={"menuBoard"},
* summary="Delete Menu Board",
* description="Delete existing Menu Board",
* @SWG\Parameter(
* name="menuId",
* in="path",
* description="The Menu Board ID to Delete",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*
* @param Request $request
* @param Response $response
* @param $id
* @return Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
*/
public function delete(Request $request, Response $response, $id): Response
{
$menuBoard = $this->menuBoardFactory->getById($id);
if (!$this->getUser()->checkDeleteable($menuBoard)) {
throw new AccessDeniedException();
}
// Issue the delete
$menuBoard->delete();
// Success
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Deleted %s'), $menuBoard->name)
]);
return $this->render($request, $response);
}
/**
* Select Folder Form
* @param Request $request
* @param Response $response
* @param int $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function selectFolderForm(Request $request, Response $response, $id)
{
// Get the Menu Board
$menuBoard = $this->menuBoardFactory->getById($id);
// Check Permissions
if (!$this->getUser()->checkEditable($menuBoard)) {
throw new AccessDeniedException();
}
$data = [
'menuBoard' => $menuBoard
];
$this->getState()->template = 'menuboard-form-selectfolder';
$this->getState()->setData($data);
return $this->render($request, $response);
}
/**
* @SWG\Put(
* path="/menuboard/{id}/selectfolder",
* operationId="menuBoardSelectFolder",
* tags={"menuBoard"},
* summary="Menu Board Select folder",
* description="Select Folder for Menu Board",
* @SWG\Parameter(
* name="menuId",
* in="path",
* description="The Menu Board ID",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="folderId",
* in="formData",
* description="Folder ID to which this object should be assigned to",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/MenuBoard")
* )
* )
*
* @param Request $request
* @param Response $response
* @param int $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function selectFolder(Request $request, Response $response, $id)
{
// Get the Menu Board
$menuBoard = $this->menuBoardFactory->getById($id);
// Check Permissions
if (!$this->getUser()->checkEditable($menuBoard)) {
throw new AccessDeniedException();
}
$folderId = $this->getSanitizer($request->getParams())->getInt('folderId');
if ($folderId === 1) {
$this->checkRootFolderAllowSave();
}
$menuBoard->folderId = $folderId;
$folder = $this->folderFactory->getById($menuBoard->folderId);
$menuBoard->permissionsFolderId = ($folder->getPermissionFolderId() == null) ? $folder->id : $folder->getPermissionFolderId();
// Save
$menuBoard->save();
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Menu Board %s moved to Folder %s'), $menuBoard->name, $folder->text)
]);
return $this->render($request, $response);
}
}

View File

@@ -0,0 +1,524 @@
<?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\Controller;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Factory\MediaFactory;
use Xibo\Factory\MenuBoardCategoryFactory;
use Xibo\Factory\MenuBoardFactory;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
class MenuBoardCategory extends Base
{
/**
* @var MenuBoardFactory
*/
private $menuBoardFactory;
/**
* @var MenuBoardCategoryFactory
*/
private $menuBoardCategoryFactory;
/**
* @var MediaFactory
*/
private $mediaFactory;
/**
* Set common dependencies.
* @param MenuBoardFactory $menuBoardFactory
* @param $menuBoardCategoryFactory
* @param MediaFactory $mediaFactory
*/
public function __construct(
$menuBoardFactory,
$menuBoardCategoryFactory,
$mediaFactory
) {
$this->menuBoardFactory = $menuBoardFactory;
$this->menuBoardCategoryFactory = $menuBoardCategoryFactory;
$this->mediaFactory = $mediaFactory;
}
/**
* Displays the Menu Board Categories Page
* @param Request $request
* @param Response $response
* @param int $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function displayPage(Request $request, Response $response, $id)
{
$menuBoard = $this->menuBoardFactory->getById($id);
// Call to render the template
$this->getState()->template = 'menuboard-category-page';
$this->getState()->setData([
'menuBoard' => $menuBoard
]);
return $this->render($request, $response);
}
/**
* Returns a Grid of Menu Board Categories
*
* @SWG\Get(
* path="/menuboard/{menuId}/categories",
* operationId="menuBoardCategorySearch",
* tags={"menuBoard"},
* summary="Search Menu Board Categories",
* description="Search all Menu Boards Categories this user has access to",
* @SWG\Parameter(
* name="menuId",
* in="path",
* description="Filter by Menu board Id",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="menuCategoryId",
* in="query",
* description="Filter by Menu Board Category Id",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="name",
* in="query",
* description="Filter by name",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="code",
* in="query",
* description="Filter by code",
* type="string",
* required=false
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="#/definitions/MenuBoard")
* )
* )
* )
* @param Request $request
* @param Response $response
* @param int $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
*/
public function grid(Request $request, Response $response, $id): Response
{
$parsedParams = $this->getSanitizer($request->getQueryParams());
$menuBoard = $this->menuBoardFactory->getById($id);
$filter = [
'menuId' => $menuBoard->menuId,
'menuCategoryId' => $parsedParams->getInt('menuCategoryId'),
'name' => $parsedParams->getString('name'),
'code' => $parsedParams->getString('code')
];
$menuBoardCategories = $this->menuBoardCategoryFactory->query(
$this->gridRenderSort($parsedParams),
$this->gridRenderFilter($filter, $parsedParams)
);
foreach ($menuBoardCategories as $menuBoardCategory) {
if ($this->isApi($request)) {
continue;
}
if ($menuBoardCategory->mediaId != 0) {
$menuBoardCategory->setUnmatchedProperty(
'thumbnail',
$this->urlFor(
$request,
'library.download',
['id' => $menuBoardCategory->mediaId],
['preview' => 1],
)
);
}
$menuBoardCategory->includeProperty('buttons');
$menuBoardCategory->buttons = [];
if ($this->getUser()->featureEnabled('menuBoard.modify') && $this->getUser()->checkEditable($menuBoard)) {
$menuBoardCategory->buttons[] = [
'id' => 'menuBoardCategory_button_viewproducts',
'url' => $this->urlFor($request, 'menuBoard.product.view', ['id' => $menuBoardCategory->menuCategoryId]),
'class' => 'XiboRedirectButton',
'text' => __('View Products')
];
$menuBoardCategory->buttons[] = [
'id' => 'menuBoardCategory_edit_button',
'url' => $this->urlFor($request, 'menuBoard.category.edit.form', ['id' => $menuBoardCategory->menuCategoryId]),
'text' => __('Edit')
];
}
if ($this->getUser()->featureEnabled('menuBoard.modify') && $this->getUser()->checkDeleteable($menuBoard)) {
$menuBoardCategory->buttons[] = ['divider' => true];
$menuBoardCategory->buttons[] = [
'id' => 'menuBoardCategory_delete_button',
'url' => $this->urlFor($request, 'menuBoard.category.delete.form', ['id' => $menuBoardCategory->menuCategoryId]),
'text' => __('Delete')
];
}
}
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = $this->menuBoardCategoryFactory->countLast();
$this->getState()->setData($menuBoardCategories);
return $this->render($request, $response);
}
/**
* Menu Board Category Add Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
*/
public function addForm(Request $request, Response $response, $id): Response
{
$menuBoard = $this->menuBoardFactory->getById($id);
if (!$this->getUser()->checkEditable($menuBoard)) {
throw new AccessDeniedException();
}
$this->getState()->template = 'menuboard-category-form-add';
$this->getState()->setData([
'menuBoard' => $menuBoard
]);
return $this->render($request, $response);
}
/**
* Add a new Menu Board Category
*
* @SWG\Post(
* path="/menuboard/{menuId}/category",
* operationId="menuBoardCategoryAdd",
* tags={"menuBoard"},
* summary="Add Menu Board",
* description="Add a new Menu Board Category",
* @SWG\Parameter(
* name="menuId",
* in="path",
* description="The Menu Board ID to which we want to add this Category to",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="name",
* in="formData",
* description="Menu Board Category name",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="mediaId",
* in="formData",
* description="Media ID associated with this Menu Board Category",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="code",
* in="formData",
* description="Menu Board Category code identifier",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="description",
* in="formData",
* description="Menu Board Category description",
* type="string",
* required=false
* ),
* @SWG\Response(
* response=201,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/MenuBoard"),
* @SWG\Header(
* header="Location",
* description="Location of the new record",
* type="string"
* )
* )
* )
* @param Request $request
* @param Response $response
* @param int $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws InvalidArgumentException
*/
public function add(Request $request, Response $response, $id): Response
{
$sanitizedParams = $this->getSanitizer($request->getParams());
$menuBoard = $this->menuBoardFactory->getById($id);
if (!$this->getUser()->checkEditable($menuBoard)) {
throw new AccessDeniedException();
}
$name = $sanitizedParams->getString('name');
$mediaId = $sanitizedParams->getInt('mediaId');
$code = $sanitizedParams->getString('code');
$description = $sanitizedParams->getString('description');
$menuBoardCategory = $this->menuBoardCategoryFactory->create($id, $name, $mediaId, $code, $description);
$menuBoardCategory->save();
$menuBoard->save(['audit' => false]);
// Return
$this->getState()->hydrate([
'message' => __('Added Menu Board Category'),
'httpStatus' => 201,
'id' => $menuBoardCategory->menuCategoryId,
'data' => $menuBoardCategory,
]);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @param int $id
* @return Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
*/
public function editForm(Request $request, Response $response, $id): Response
{
$menuBoard = $this->menuBoardFactory->getByMenuCategoryId($id);
if (!$this->getUser()->checkEditable($menuBoard)) {
throw new AccessDeniedException();
}
$menuBoardCategory = $this->menuBoardCategoryFactory->getById($id);
$this->getState()->template = 'menuboard-category-form-edit';
$this->getState()->setData([
'menuBoardCategory' => $menuBoardCategory,
'media' => $menuBoardCategory->mediaId != null ? $this->mediaFactory->getById($menuBoardCategory->mediaId) : null
]);
return $this->render($request, $response);
}
/**
* @SWG\Put(
* path="/menuboard/{menuCategoryId}/category",
* operationId="menuBoardCategoryEdit",
* tags={"menuBoard"},
* summary="Edit Menu Board Category",
* description="Edit existing Menu Board Category",
* @SWG\Parameter(
* name="menuCategoryId",
* in="path",
* description="The Menu Board Category ID to Edit",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="name",
* in="formData",
* description="Menu Board name",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="mediaId",
* in="formData",
* description="Media ID from CMS Library to associate with this Menu Board Category",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="code",
* in="formData",
* description="Menu Board Category code identifier",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="description",
* in="formData",
* description="Menu Board Category description",
* type="string",
* required=false
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*
* @param Request $request
* @param Response $response
* @param int $id
* @return Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
*/
public function edit(Request $request, Response $response, $id): Response
{
$menuBoard = $this->menuBoardFactory->getByMenuCategoryId($id);
if (!$this->getUser()->checkEditable($menuBoard)) {
throw new AccessDeniedException();
}
$sanitizedParams = $this->getSanitizer($request->getParams());
$menuBoardCategory = $this->menuBoardCategoryFactory->getById($id);
$menuBoardCategory->name = $sanitizedParams->getString('name');
$menuBoardCategory->mediaId = $sanitizedParams->getInt('mediaId');
$menuBoardCategory->code = $sanitizedParams->getString('code');
$menuBoardCategory->description = $sanitizedParams->getString('description');
$menuBoardCategory->save();
$menuBoard->save();
// Success
$this->getState()->hydrate([
'httpStatus' => 200,
'message' => sprintf(__('Edited %s'), $menuBoardCategory->name),
'id' => $menuBoardCategory->menuCategoryId,
'data' => $menuBoardCategory
]);
return $this->render($request, $response);
}
/**
*
* @param Request $request
* @param Response $response
* @param int $id
* @return Response
* @throws GeneralException
*/
public function deleteForm(Request $request, Response $response, $id): Response
{
$menuBoard = $this->menuBoardFactory->getByMenuCategoryId($id);
if (!$this->getUser()->checkDeleteable($menuBoard)) {
throw new AccessDeniedException();
}
$menuBoardCategory = $this->menuBoardCategoryFactory->getById($id);
$this->getState()->template = 'menuboard-category-form-delete';
$this->getState()->setData([
'menuBoardCategory' => $menuBoardCategory
]);
return $this->render($request, $response);
}
/**
* @SWG\Delete(
* path="/menuboard/{menuCategoryId}/category",
* operationId="menuBoardCategoryDelete",
* tags={"menuBoard"},
* summary="Delete Menu Board Category",
* description="Delete existing Menu Board Category",
* @SWG\Parameter(
* name="menuId",
* in="path",
* description="The menuId to Delete",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*
* @param Request $request
* @param Response $response
* @param $id
* @return Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
*/
public function delete(Request $request, Response $response, $id): Response
{
$menuBoard = $this->menuBoardFactory->getByMenuCategoryId($id);
if (!$this->getUser()->checkDeleteable($menuBoard)) {
throw new AccessDeniedException();
}
$menuBoardCategory = $this->menuBoardCategoryFactory->getById($id);
// Issue the delete
$menuBoardCategory->delete();
// Success
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Deleted %s'), $menuBoardCategory->name)
]);
return $this->render($request, $response);
}
}

View File

@@ -0,0 +1,727 @@
<?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\Controller;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Factory\MediaFactory;
use Xibo\Factory\MenuBoardCategoryFactory;
use Xibo\Factory\MenuBoardFactory;
use Xibo\Factory\MenuBoardProductOptionFactory;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
class MenuBoardProduct extends Base
{
/**
* @var MenuBoardFactory
*/
private $menuBoardFactory;
/**
* @var MenuBoardCategoryFactory
*/
private $menuBoardCategoryFactory;
/**
* @var MenuBoardProductOptionFactory
*/
private $menuBoardProductOptionFactory;
/**
* @var MediaFactory
*/
private $mediaFactory;
/**
* Set common dependencies.
* @param MenuBoardFactory $menuBoardFactory
* @param MenuBoardCategoryFactory $menuBoardCategoryFactory
* @param MenuBoardProductOptionFactory $menuBoardProductOptionFactory
* @param MediaFactory $mediaFactory
*/
public function __construct(
$menuBoardFactory,
$menuBoardCategoryFactory,
$menuBoardProductOptionFactory,
$mediaFactory
) {
$this->menuBoardFactory = $menuBoardFactory;
$this->menuBoardCategoryFactory = $menuBoardCategoryFactory;
$this->menuBoardProductOptionFactory = $menuBoardProductOptionFactory;
$this->mediaFactory = $mediaFactory;
}
/**
* Displays the Menu Board Page
* @param Request $request
* @param Response $response
* @param int $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function displayPage(Request $request, Response $response, $id)
{
$menuBoard = $this->menuBoardFactory->getByMenuCategoryId($id);
$menuBoardCategory = $this->menuBoardCategoryFactory->getById($id);
$categories = $this->menuBoardCategoryFactory->getByMenuId($menuBoard->menuId);
// Call to render the template
$this->getState()->template = 'menuboard-product-page';
$this->getState()->setData([
'menuBoard' => $menuBoard,
'menuBoardCategory' => $menuBoardCategory,
'categories' => $categories
]);
return $this->render($request, $response);
}
/**
* Returns a Grid of Menu Board Products
*
* @SWG\Get(
* path="/menuboard/{menuCategoryId}/products",
* operationId="menuBoardProductsSearch",
* tags={"menuBoard"},
* summary="Search Menu Board Products",
* description="Search all Menu Boards Products this user has access to",
* @SWG\Parameter(
* name="menuCategoryId",
* in="path",
* description="Filter by Menu Board Category Id",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="menuId",
* in="query",
* description="Filter by Menu board Id",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="name",
* in="query",
* description="Filter by name",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="code",
* in="query",
* description="Filter by code",
* type="string",
* required=false
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="#/definitions/MenuBoard")
* )
* )
* )
* @param Request $request
* @param Response $response
* @param int $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
*/
public function grid(Request $request, Response $response, $id): Response
{
$parsedParams = $this->getSanitizer($request->getQueryParams());
$menuBoard = $this->menuBoardFactory->getByMenuCategoryId($id);
$filter = [
'menuProductId' => $parsedParams->getInt('menuProductId'),
'menuCategoryId' => $id,
'name' => $parsedParams->getString('name'),
'code' => $parsedParams->getString('code')
];
$menuBoardProducts = $this->menuBoardCategoryFactory->getProductData(
$this->gridRenderSort($parsedParams),
$this->gridRenderFilter($filter, $parsedParams)
);
foreach ($menuBoardProducts as $menuBoardProduct) {
if ($this->isApi($request)) {
continue;
}
$menuBoardProduct->includeProperty('buttons');
$menuBoardProduct->buttons = [];
if ($menuBoardProduct->mediaId != 0) {
$menuBoardProduct->setUnmatchedProperty(
'thumbnail',
$this->urlFor($request, 'library.download', ['id' => $menuBoardProduct->mediaId], ['preview' => 1]),
);
}
if ($this->getUser()->featureEnabled('menuBoard.modify') && $this->getUser()->checkEditable($menuBoard)) {
$menuBoardProduct->buttons[] = [
'id' => 'menuBoardProduct_edit_button',
'url' => $this->urlFor($request, 'menuBoard.product.edit.form', ['id' => $menuBoardProduct->menuProductId]),
'text' => __('Edit')
];
}
if ($this->getUser()->featureEnabled('menuBoard.modify') && $this->getUser()->checkDeleteable($menuBoard)) {
$menuBoardProduct->buttons[] = ['divider' => true];
$menuBoardProduct->buttons[] = [
'id' => 'menuBoardProduct_delete_button',
'url' => $this->urlFor($request, 'menuBoard.product.delete.form', ['id' => $menuBoardProduct->menuProductId]),
'text' => __('Delete'),
'multi-select' => true,
'dataAttributes' => [
['name' => 'commit-url', 'value' => $this->urlFor($request, 'menuBoard.product.delete', ['id' => $menuBoardProduct->menuProductId])],
['name' => 'commit-method', 'value' => 'delete'],
['name' => 'id', 'value' => 'menuBoardProduct_delete_button'],
['name' => 'text', 'value' => __('Delete')],
['name' => 'sort-group', 'value' => 1],
['name' => 'rowtitle', 'value' => $menuBoardProduct->name]
]
];
}
}
$menuBoard->setActive();
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = $this->menuBoardCategoryFactory->countLast();
$this->getState()->setData($menuBoardProducts);
return $this->render($request, $response);
}
public function productsForWidget(Request $request, Response $response): Response
{
$parsedParams = $this->getSanitizer($request->getQueryParams());
$categories = $parsedParams->getString('categories');
$filter = [
'menuId' => $parsedParams->getInt('menuId'),
'menuProductId' => $parsedParams->getInt('menuProductId'),
'menuCategoryId' => $parsedParams->getInt('menuCategoryId'),
'name' => $parsedParams->getString('name'),
'availability' => $parsedParams->getInt('availability'),
'categories' => $categories
];
$menuBoardProducts = $this->menuBoardCategoryFactory->getProductData(
$this->gridRenderSort($parsedParams),
$this->gridRenderFilter($filter, $parsedParams)
);
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = $this->menuBoardCategoryFactory->countLast();
$this->getState()->setData($menuBoardProducts);
return $this->render($request, $response);
}
/**
* Menu Board Category Add Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
*/
public function addForm(Request $request, Response $response, $id): Response
{
$menuBoard = $this->menuBoardFactory->getByMenuCategoryId($id);
if (!$this->getUser()->checkEditable($menuBoard)) {
throw new AccessDeniedException();
}
$menuBoardCategory = $this->menuBoardCategoryFactory->getById($id);
$this->getState()->template = 'menuboard-product-form-add';
$this->getState()->setData([
'menuBoard' => $menuBoard,
'menuBoardCategory' => $menuBoardCategory
]);
return $this->render($request, $response);
}
/**
* Add a new Menu Board Product
*
* @SWG\Post(
* path="/menuboard/{menuCategoryId}/product",
* operationId="menuBoardProductAdd",
* tags={"menuBoard"},
* summary="Add Menu Board Product",
* description="Add a new Menu Board Product",
* @SWG\Parameter(
* name="name",
* in="formData",
* description="Menu Board Product name",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="description",
* in="formData",
* description="Menu Board Product description",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="price",
* in="formData",
* description="Menu Board Product price",
* type="number",
* required=false
* ),
* @SWG\Parameter(
* name="allergyInfo",
* in="formData",
* description="Menu Board Product allergyInfo",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="calories",
* in="formData",
* description="Menu Board Product calories",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="displayOrder",
* in="formData",
* description="Menu Board Product Display Order, used for sorting",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="availability",
* in="formData",
* description="Menu Board Product availability",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="mediaId",
* in="formData",
* description="Media ID from CMS Library to associate with this Menu Board Product",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="code",
* in="formData",
* description="Menu Board Product code",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="productOptions",
* in="formData",
* description="An array of optional Product Option names",
* type="array",
* required=false,
* @SWG\Items(type="string")
* ),
* @SWG\Parameter(
* name="productValues",
* in="formData",
* description="An array of optional Product Option values",
* type="array",
* required=false,
* @SWG\Items(type="string")
* ),
* @SWG\Response(
* response=201,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/MenuBoard"),
* @SWG\Header(
* header="Location",
* description="Location of the new record",
* type="string"
* )
* )
* )
* @param Request $request
* @param Response $response
* @param int $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws InvalidArgumentException
*/
public function add(Request $request, Response $response, $id): Response
{
$menuBoard = $this->menuBoardFactory->getByMenuCategoryId($id);
if (!$this->getUser()->checkEditable($menuBoard)) {
throw new AccessDeniedException();
}
$menuBoardCategory = $this->menuBoardCategoryFactory->getById($id);
$sanitizedParams = $this->getSanitizer($request->getParams());
$name = $sanitizedParams->getString('name');
$mediaId = $sanitizedParams->getInt('mediaId');
$price = $sanitizedParams->getDouble('price');
$description = $sanitizedParams->getString('description');
$allergyInfo = $sanitizedParams->getString('allergyInfo');
$calories = $sanitizedParams->getInt('calories');
$displayOrder = $sanitizedParams->getInt('displayOrder');
$availability = $sanitizedParams->getCheckbox('availability');
$productOptions = $sanitizedParams->getArray('productOptions', ['default' => []]);
$productValues = $sanitizedParams->getArray('productValues', ['default' => []]);
$code = $sanitizedParams->getString('code');
// If the display order is empty, get the next highest one.
if ($displayOrder === null) {
$displayOrder = $this->menuBoardCategoryFactory->getNextDisplayOrder($menuBoardCategory->menuCategoryId);
}
$menuBoardProduct = $this->menuBoardCategoryFactory->createProduct(
$menuBoard->menuId,
$menuBoardCategory->menuCategoryId,
$name,
$price,
$description,
$allergyInfo,
$calories,
$displayOrder,
$availability,
$mediaId,
$code
);
$menuBoardProduct->save();
if (!empty(array_filter($productOptions)) && !empty(array_filter($productValues))) {
$productDetails = array_filter(array_combine($productOptions, $productValues));
$parsedDetails = $this->getSanitizer($productDetails);
foreach ($productDetails as $option => $value) {
$productOption = $this->menuBoardProductOptionFactory->create(
$menuBoardProduct->menuProductId,
$option,
$parsedDetails->getDouble($option)
);
$productOption->save();
}
}
$menuBoardProduct->productOptions = $menuBoardProduct->getOptions();
$menuBoard->save();
// Return
$this->getState()->hydrate([
'message' => __('Added Menu Board Product'),
'httpStatus' => 201,
'id' => $menuBoardProduct->menuProductId,
'data' => $menuBoardProduct
]);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @param int $id
* @return Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
*/
public function editForm(Request $request, Response $response, $id): Response
{
$menuBoardProduct = $this->menuBoardCategoryFactory->getByProductId($id);
$menuBoard = $this->menuBoardFactory->getById($menuBoardProduct->menuId);
if (!$this->getUser()->checkEditable($menuBoard)) {
throw new AccessDeniedException();
}
$this->getState()->template = 'menuboard-product-form-edit';
$this->getState()->setData([
'menuBoardProduct' => $menuBoardProduct,
'media' => $menuBoardProduct->mediaId != null ? $this->mediaFactory->getById($menuBoardProduct->mediaId) : null
]);
return $this->render($request, $response);
}
/**
* @SWG\Put(
* path="/menuboard/{menuProductId}/product",
* operationId="menuBoardProductEdit",
* tags={"menuBoard"},
* summary="Edit Menu Board Product",
* description="Edit existing Menu Board Product",
* @SWG\Parameter(
* name="menuProductId",
* in="path",
* description="The Menu Board Product ID to Edit",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="name",
* in="formData",
* description="Menu Board Product name",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="description",
* in="formData",
* description="Menu Board Product description",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="price",
* in="formData",
* description="Menu Board Product price",
* type="number",
* required=false
* ),
* @SWG\Parameter(
* name="allergyInfo",
* in="formData",
* description="Menu Board Product allergyInfo",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="calories",
* in="formData",
* description="Menu Board Product calories",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="displayOrder",
* in="formData",
* description="Menu Board Product Display Order, used for sorting",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="availability",
* in="formData",
* description="Menu Board Product availability",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="mediaId",
* in="formData",
* description="Media ID from CMS Library to associate with this Menu Board Product",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="code",
* in="formData",
* description="Menu Board Product code",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="productOptions",
* in="formData",
* description="An array of optional Product Option names",
* type="array",
* required=false,
* @SWG\Items(type="string")
* ),
* @SWG\Parameter(
* name="productValues",
* in="formData",
* description="An array of optional Product Option values",
* type="array",
* required=false,
* @SWG\Items(type="string")
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*
* @param Request $request
* @param Response $response
* @param int $id
* @return Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
*/
public function edit(Request $request, Response $response, $id): Response
{
$menuBoardProduct = $this->menuBoardCategoryFactory->getByProductId($id);
$menuBoard = $this->menuBoardFactory->getById($menuBoardProduct->menuId);
if (!$this->getUser()->checkEditable($menuBoard)) {
throw new AccessDeniedException();
}
$sanitizedParams = $this->getSanitizer($request->getParams());
$menuBoardProduct->name = $sanitizedParams->getString('name');
$menuBoardProduct->description = $sanitizedParams->getString('description');
$menuBoardProduct->price = $sanitizedParams->getDouble('price');
$menuBoardProduct->allergyInfo = $sanitizedParams->getString('allergyInfo');
$menuBoardProduct->calories = $sanitizedParams->getInt('calories');
$menuBoardProduct->displayOrder = $sanitizedParams->getInt('displayOrder');
$menuBoardProduct->availability = $sanitizedParams->getCheckbox('availability');
$menuBoardProduct->mediaId = $sanitizedParams->getInt('mediaId');
$menuBoardProduct->code = $sanitizedParams->getString('code');
$productOptions = $sanitizedParams->getArray('productOptions', ['default' => []]);
$productValues = $sanitizedParams->getArray('productValues', ['default' => []]);
if (!empty(array_filter($productOptions)) && !empty(array_filter($productValues))) {
$productDetails = array_filter(array_combine($productOptions, $productValues));
$parsedDetails = $this->getSanitizer($productDetails);
if (count($menuBoardProduct->getOptions()) > count($productDetails)) {
$menuBoardProduct->removeOptions();
}
foreach ($productDetails as $option => $value) {
$productOption = $this->menuBoardProductOptionFactory->create(
$menuBoardProduct->menuProductId,
$option,
$parsedDetails->getDouble($option)
);
$productOption->save();
}
} else {
$menuBoardProduct->removeOptions();
}
$menuBoardProduct->productOptions = $menuBoardProduct->getOptions();
$menuBoardProduct->save();
$menuBoard->save();
// Success
$this->getState()->hydrate([
'httpStatus' => 200,
'message' => sprintf(__('Edited %s'), $menuBoardProduct->name),
'id' => $menuBoardProduct->menuProductId,
'data' => $menuBoardProduct
]);
return $this->render($request, $response);
}
/**
*
* @param Request $request
* @param Response $response
* @param int $id
* @return Response
* @throws GeneralException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function deleteForm(Request $request, Response $response, $id): Response
{
$menuBoardProduct = $this->menuBoardCategoryFactory->getByProductId($id);
$menuBoardCategory = $this->menuBoardCategoryFactory->getById($menuBoardProduct->menuCategoryId);
$menuBoard = $this->menuBoardFactory->getById($menuBoardProduct->menuId);
if (!$this->getUser()->checkEditable($menuBoard)) {
throw new AccessDeniedException();
}
$this->getState()->template = 'menuboard-product-form-delete';
$this->getState()->setData([
'menuBoard' => $menuBoard,
'menuBoardCategory' => $menuBoardCategory,
'menuBoardProduct' => $menuBoardProduct
]);
return $this->render($request, $response);
}
/**
* @SWG\Delete(
* path="/menuboard/{menuProductId}/product",
* operationId="menuBoardProductDelete",
* tags={"menuBoard"},
* summary="Delete Menu Board",
* description="Delete existing Menu Board Product",
* @SWG\Parameter(
* name="menuProductId",
* in="path",
* description="The Menu Board Product ID to Delete",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*
* @param Request $request
* @param Response $response
* @param $id
* @return Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
*/
public function delete(Request $request, Response $response, $id): Response
{
$menuBoardProduct = $this->menuBoardCategoryFactory->getByProductId($id);
$menuBoard = $this->menuBoardFactory->getById($menuBoardProduct->menuId);
if (!$this->getUser()->checkDeleteable($menuBoard)) {
throw new AccessDeniedException();
}
// Issue the delete
$menuBoardProduct->delete();
// Success
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Deleted %s'), $menuBoardProduct->name)
]);
return $this->render($request, $response);
}
}

464
lib/Controller/Module.php Normal file
View File

@@ -0,0 +1,464 @@
<?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\Controller;
use Psr\Http\Message\ResponseInterface;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Factory\ModuleFactory;
use Xibo\Factory\ModuleTemplateFactory;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\ControllerNotImplemented;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
/**
* Class Module
* @package Xibo\Controller
*/
class Module extends Base
{
/** @var ModuleFactory */
private $moduleFactory;
/** @var \Xibo\Factory\ModuleTemplateFactory */
private $moduleTemplateFactory;
/**
* Set common dependencies.
* @param StorageServiceInterface $store
* @param ModuleFactory $moduleFactory
*/
public function __construct(
ModuleFactory $moduleFactory,
ModuleTemplateFactory $moduleTemplateFactory
) {
$this->moduleFactory = $moduleFactory;
$this->moduleTemplateFactory = $moduleTemplateFactory;
}
/**
* Display the module page
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function displayPage(Request $request, Response $response)
{
$this->getState()->template = 'module-page';
return $this->render($request, $response);
}
/**
* @SWG\Get(
* path="/module",
* operationId="moduleSearch",
* tags={"module"},
* summary="Module Search",
* description="Get a list of all modules available to this CMS",
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Module")
* )
* )
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function grid(Request $request, Response $response)
{
$parsedQueryParams = $this->getSanitizer($request->getQueryParams());
$filter = [
'name' => $parsedQueryParams->getString('name'),
'extension' => $parsedQueryParams->getString('extension'),
'moduleId' => $parsedQueryParams->getInt('moduleId')
];
$modules = $this->moduleFactory->getAllExceptCanvas($filter);
foreach ($modules as $module) {
/* @var \Xibo\Entity\Module $module */
if ($this->isApi($request)) {
break;
}
$module->includeProperty('buttons');
// Edit button
$module->buttons[] = [
'id' => 'module_button_edit',
'url' => $this->urlFor($request, 'module.settings.form', ['id' => $module->moduleId]),
'text' => __('Configure')
];
// Clear cache
if ($module->regionSpecific == 1) {
$module->buttons[] = [
'id' => 'module_button_clear_cache',
'url' => $this->urlFor($request, 'module.clear.cache.form', ['id' => $module->moduleId]),
'text' => __('Clear Cache'),
'dataAttributes' => [
['name' => 'auto-submit', 'value' => true],
[
'name' => 'commit-url',
'value' => $this->urlFor($request, 'module.clear.cache', ['id' => $module->moduleId])
],
['name' => 'commit-method', 'value' => 'PUT']
]
];
}
}
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = 0;
$this->getState()->setData($modules);
return $this->render($request, $response);
}
// phpcs:disable
/**
* @SWG\Get(
* path="/module/properties/{id}",
* operationId="getModuleProperties",
* tags={"module"},
* summary="Get Module Properties",
* description="Get a module properties which are needed to for the editWidget call",
* @SWG\Parameter(
* name="id",
* in="path",
* description="The ModuleId",
* type="string",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Property")
* )
* )
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
// phpcs:enable
public function getProperties(Request $request, Response $response, $id)
{
// Get properties, but return a key->value object for easy parsing.
$props = [];
foreach ($this->moduleFactory->getById($id)->properties as $property) {
$props[$property->id] = [
'type' => $property->type,
'title' => $property->title,
'helpText' => $property->helpText,
'options' => $property->options,
];
}
$this->getState()->setData($props);
return $this->render($request, $response);
}
/**
* Settings Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function settingsForm(Request $request, Response $response, $id)
{
// Can we edit?
if (!$this->getUser()->userTypeId == 1) {
throw new AccessDeniedException();
}
$module = $this->moduleFactory->getById($id);
// Pass to view
$this->getState()->template = 'module-form-settings';
$this->getState()->setData([
'moduleId' => $id,
'module' => $module,
]);
return $this->render($request, $response);
}
/**
* Settings
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function settings(Request $request, Response $response, $id)
{
if (!$this->getUser()->isSuperAdmin()) {
throw new AccessDeniedException();
}
$sanitizedParams = $this->getSanitizer($request->getParams());
// Get the module
$module = $this->moduleFactory->getById($id);
// Default settings
$module->enabled = $sanitizedParams->getCheckbox('enabled');
$module->previewEnabled = $sanitizedParams->getCheckbox('previewEnabled');
$module->defaultDuration = $sanitizedParams->getInt('defaultDuration');
// Parse out any settings we ought to expect.
foreach ($module->settings as $setting) {
$setting->setValueByType($sanitizedParams, null, true);
}
// Preview is not allowed for generic file type
if ($module->allowPreview === 0 && $sanitizedParams->getCheckbox('previewEnabled') == 1) {
throw new InvalidArgumentException(__('Preview is disabled'));
}
// Save
$module->save();
// Successful
$this->getState()->hydrate([
'message' => sprintf(__('Configured %s'), $module->name),
'id' => $module->moduleId,
'data' => $module
]);
return $this->render($request, $response);
}
/**
* Clear Cache Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function clearCacheForm(Request $request, Response $response, $id)
{
$module = $this->moduleFactory->getById($id);
$this->getState()->template = 'module-form-clear-cache';
$this->getState()->autoSubmit = $this->getAutoSubmit('clearCache');
$this->getState()->setData([
'module' => $module,
]);
return $this->render($request, $response);
}
/**
* Clear Cache
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function clearCache(Request $request, Response $response, $id)
{
$module = $this->moduleFactory->getById($id);
if ($module->isDataProviderExpected()) {
$this->moduleFactory->clearCacheForDataType($module->dataType);
}
$this->getState()->hydrate([
'message' => __('Cleared the Cache')
]);
return $this->render($request, $response);
}
/**
* @SWG\Get(
* path="/module/templates/{dataType}",
* operationId="moduleTemplateSearch",
* tags={"module"},
* summary="Module Template Search",
* description="Get a list of templates available for a particular data type",
* @SWG\Parameter(
* name="dataType",
* in="path",
* description="DataType to return templates for",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="type",
* in="query",
* description="Type to return templates for",
* type="string",
* required=false
* ),
* @SWG\Response(
* response=200,
* description="An array of module templates for the provided datatype",
* @SWG\Schema(ref="#/definitions/ModuleTemplate")
* )
* )
* @param \Slim\Http\ServerRequest $request
* @param \Slim\Http\Response $response
* @param string $dataType
* @return \Slim\Http\Response
* @throws \Xibo\Support\Exception\GeneralException
*/
public function templateGrid(Request $request, Response $response, string $dataType): Response
{
if (empty($dataType)) {
throw new InvalidArgumentException(__('Please provide a datatype'), 'dataType');
}
$params = $this->getSanitizer($request->getParams());
$type = $params->getString('type');
$templates = !empty($type)
? $this->moduleTemplateFactory->getByTypeAndDataType($type, $dataType)
: $this->moduleTemplateFactory->getByDataType($dataType);
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = 0;
$this->getState()->setData($templates);
return $this->render($request, $response);
}
// phpcs:disable
/**
* @SWG\Get(
* path="/module/template/{dataType}/properties/{id}",
* operationId="getModuleProperties",
* tags={"module"},
* summary="Get Module Template Properties",
* description="Get a module template properties which are needed to for the editWidget call",
* @SWG\Parameter(
* name="dataType",
* in="path",
* description="The Template DataType",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="id",
* in="path",
* description="The Template Id",
* type="string",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(
* type="object",
* additionalProperties={"id":"string", "type":"string", "title":"string", "helpText":"string", "options":"array"}
* )
* )
* )
* @param Request $request
* @param Response $response
* @param string $dataType
* @param string $id
* @return ResponseInterface|Response
* @throws GeneralException
* @throws NotFoundException
* @throws ControllerNotImplemented
*/
// phpcs:enable
public function getTemplateProperties(Request $request, Response $response, string $dataType, string $id)
{
// Get properties, but return a key->value object for easy parsing.
$props = [];
foreach ($this->moduleTemplateFactory->getByDataTypeAndId($dataType, $id)->properties as $property) {
$props[$property->id] = [
'id' => $property->id,
'type' => $property->type,
'title' => $property->title,
'helpText' => $property->helpText,
'options' => $property->options,
];
}
$this->getState()->setData($props);
return $this->render($request, $response);
}
/**
* Serve an asset
* @param \Slim\Http\ServerRequest $request
* @param \Slim\Http\Response $response
* @param string $assetId the ID of the asset to serve
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\InvalidArgumentException
* @throws \Xibo\Support\Exception\NotFoundException
* @throws \Xibo\Support\Exception\GeneralException
*/
public function assetDownload(Request $request, Response $response, string $assetId): Response
{
if (empty($assetId)) {
throw new InvalidArgumentException(__('Please provide an assetId'), 'assetId');
}
// Get this asset from somewhere
$asset = $this->moduleFactory->getAssetsFromAnywhereById(
$assetId,
$this->moduleTemplateFactory,
$this->getSanitizer($request->getParams())->getCheckbox('isAlias')
);
$asset->updateAssetCache($this->getConfig()->getSetting('LIBRARY_LOCATION'));
$this->getLog()->debug('assetDownload: found appropriate asset for assetId ' . $assetId);
// The asset can serve itself.
return $asset->psrResponse($request, $response, $this->getConfig()->getSetting('SENDFILE_MODE'));
}
}

View File

@@ -0,0 +1,850 @@
<?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\Controller;
use Carbon\Carbon;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Entity\UserGroup;
use Xibo\Factory\DisplayGroupFactory;
use Xibo\Factory\NotificationFactory;
use Xibo\Factory\UserGroupFactory;
use Xibo\Factory\UserNotificationFactory;
use Xibo\Helper\AttachmentUploadHandler;
use Xibo\Helper\DateFormatHelper;
use Xibo\Helper\SendFile;
use Xibo\Service\DisplayNotifyService;
use Xibo\Service\MediaService;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\ConfigurationException;
/**
* Class Notification
* @package Xibo\Controller
*/
class Notification extends Base
{
/** @var NotificationFactory */
private $notificationFactory;
/** @var UserNotificationFactory */
private $userNotificationFactory;
/** @var DisplayGroupFactory */
private $displayGroupFactory;
/** @var UserGroupFactory */
private $userGroupFactory;
/** @var DisplayNotifyService */
private $displayNotifyService;
/**
* Notification constructor.
* @param NotificationFactory $notificationFactory
* @param UserNotificationFactory $userNotificationFactory
* @param DisplayGroupFactory $displayGroupFactory
* @param UserGroupFactory $userGroupFactory
* @param DisplayNotifyService $displayNotifyService
*/
public function __construct(
$notificationFactory,
$userNotificationFactory,
$displayGroupFactory,
$userGroupFactory,
$displayNotifyService
) {
$this->notificationFactory = $notificationFactory;
$this->userNotificationFactory = $userNotificationFactory;
$this->displayGroupFactory = $displayGroupFactory;
$this->userGroupFactory = $userGroupFactory;
$this->displayNotifyService = $displayNotifyService;
}
/**
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function displayPage(Request $request, Response $response)
{
// Call to render the template
$this->getState()->template = 'notification-page';
return $this->render($request, $response);
}
/**
* Show a notification
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function interrupt(Request $request, Response $response, $id)
{
$notification = $this->userNotificationFactory->getByNotificationId($id);
// Mark it as read
$notification->setRead(Carbon::now()->format('U'));
$notification->save();
$this->getState()->template = 'notification-interrupt';
$this->getState()->setData(['notification' => $notification]);
return $this->render($request, $response);
}
/**
* Show a notification
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function show(Request $request, Response $response, $id)
{
$params = $this->getSanitizer($request->getParams());
$notification = $this->userNotificationFactory->getByNotificationId($id);
// Mark it as read
$notification->setRead(Carbon::now()->format('U'));
$notification->save();
if ($params->getCheckbox('multiSelect')) {
return $response->withStatus(201);
} else {
$this->getState()->template = 'notification-form-show';
$this->getState()->setData(['notification' => $notification]);
return $this->render($request, $response);
}
}
/**
* @SWG\Get(
* path="/notification",
* operationId="notificationSearch",
* tags={"notification"},
* summary="Notification Search",
* description="Search this users Notifications",
* @SWG\Parameter(
* name="notificationId",
* in="query",
* description="Filter by Notification Id",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="subject",
* in="query",
* description="Filter by Subject",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="embed",
* in="query",
* description="Embed related data such as userGroups,displayGroups",
* type="string",
* required=false
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="#/definitions/Notification")
* )
* )
* )
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function grid(Request $request, Response $response): Response|\Psr\Http\Message\ResponseInterface
{
$sanitizedQueryParams = $this->getSanitizer($request->getQueryParams());
$filter = [
'notificationId' => $sanitizedQueryParams->getInt('notificationId'),
'subject' => $sanitizedQueryParams->getString('subject'),
'read' => $sanitizedQueryParams->getInt('read'),
'releaseDt' => $sanitizedQueryParams->getDate('releaseDt')?->format('U'),
'type' => $sanitizedQueryParams->getString('type'),
];
$embed = ($sanitizedQueryParams->getString('embed') != null)
? explode(',', $sanitizedQueryParams->getString('embed'))
: [];
$notifications = $this->notificationFactory->query(
$this->gridRenderSort($sanitizedQueryParams),
$this->gridRenderFilter($filter, $sanitizedQueryParams)
);
foreach ($notifications as $notification) {
if (in_array('userGroups', $embed) || in_array('displayGroups', $embed)) {
$notification->load([
'loadUserGroups' => in_array('userGroups', $embed),
'loadDisplayGroups' => in_array('displayGroups', $embed),
]);
}
if ($this->isApi($request)) {
continue;
}
$notification->includeProperty('buttons');
// View Notification
$notification->buttons[] = [
'id' => 'notification_button_view',
'url' => $this->urlFor(
$request,
'notification.show',
['id' => $notification->notificationId]
),
'text' => __('View'),
'multi-select' => true,
'dataAttributes' => [
[
'name' => 'commit-url',
'value' => $this->urlFor(
$request,
'notification.show',
['id' => $notification->notificationId, 'multiSelect' => true]
),
],
['name' => 'commit-method', 'value' => 'get'],
['name' => 'id', 'value' => 'notification_button_view'],
['name' => 'text', 'value' => __('Mark as read?')],
['name' => 'sort-group', 'value' => 1],
['name' => 'rowtitle', 'value' => $notification->subject]
]
];
// Edit Notification
if ($this->getUser()->checkEditable($notification) &&
$this->getUser()->featureEnabled('notification.modify')
) {
$notification->buttons[] = [
'id' => 'notification_button_edit',
'url' => $this->urlFor(
$request,
'notification.edit.form',
['id' => $notification->notificationId]
),
'text' => __('Edit')
];
}
// Delete Notifications
if ($this->getUser()->checkDeleteable($notification) &&
$this->getUser()->featureEnabled('notification.modify')
) {
$notification->buttons[] = ['divider' => true];
$notification->buttons[] = [
'id' => 'notification_button_delete',
'url' => $this->urlFor(
$request,
'notification.delete.form',
['id' => $notification->notificationId]
),
'text' => __('Delete'),
'multi-select' => true,
'dataAttributes' => [
[
'name' => 'commit-url',
'value' => $this->urlFor(
$request,
'notification.delete',
['id' => $notification->notificationId]
)
],
['name' => 'commit-method', 'value' => 'delete'],
['name' => 'id', 'value' => 'notification_button_delete'],
['name' => 'text', 'value' => __('Delete?')],
['name' => 'sort-group', 'value' => 2],
['name' => 'rowtitle', 'value' => $notification->subject]
]
];
}
}
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = $this->notificationFactory->countLast();
$this->getState()->setData($notifications);
return $this->render($request, $response);
}
/**
* Add Notification Form
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function addForm(Request $request, Response $response)
{
$groups = [];
$displays = [];
$userGroups = [];
$users = [];
foreach ($this->displayGroupFactory->query(['displayGroup'], ['isDisplaySpecific' => -1]) as $displayGroup) {
/* @var \Xibo\Entity\DisplayGroup $displayGroup */
if ($displayGroup->isDisplaySpecific == 1) {
$displays[] = $displayGroup;
} else {
$groups[] = $displayGroup;
}
}
foreach ($this->userGroupFactory->query(['`group`'], ['isUserSpecific' => -1]) as $userGroup) {
/* @var UserGroup $userGroup */
if ($userGroup->isUserSpecific == 0) {
$userGroups[] = $userGroup;
} else {
$users[] = $userGroup;
}
}
$this->getState()->template = 'notification-form-add';
$this->getState()->setData([
'displays' => $displays,
'displayGroups' => $groups,
'users' => $users,
'userGroups' => $userGroups,
]);
return $this->render($request, $response);
}
/**
* Edit Notification Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function editForm(Request $request, Response $response, $id)
{
$notification = $this->notificationFactory->getById($id);
$notification->load();
// Adjust the dates
$notification->createDt = Carbon::createFromTimestamp($notification->createDt)
->format(DateFormatHelper::getSystemFormat());
$notification->releaseDt = Carbon::createFromTimestamp($notification->releaseDt)
->format(DateFormatHelper::getSystemFormat());
if (!$this->getUser()->checkEditable($notification)) {
throw new AccessDeniedException();
}
$groups = [];
$displays = [];
$userGroups = [];
$users = [];
foreach ($this->displayGroupFactory->query(['displayGroup'], ['isDisplaySpecific' => -1]) as $displayGroup) {
/* @var \Xibo\Entity\DisplayGroup $displayGroup */
if ($displayGroup->isDisplaySpecific == 1) {
$displays[] = $displayGroup;
} else {
$groups[] = $displayGroup;
}
}
foreach ($this->userGroupFactory->query(['`group`'], ['isUserSpecific' => -1]) as $userGroup) {
/* @var UserGroup $userGroup */
if ($userGroup->isUserSpecific == 0) {
$userGroups[] = $userGroup;
} else {
$users[] = $userGroup;
}
}
$this->getState()->template = 'notification-form-edit';
$this->getState()->setData([
'notification' => $notification,
'displays' => $displays,
'displayGroups' => $groups,
'users' => $users,
'userGroups' => $userGroups,
'displayGroupIds' => array_map(function ($element) {
return $element->displayGroupId;
}, $notification->displayGroups),
'userGroupIds' => array_map(function ($element) {
return $element->groupId;
}, $notification->userGroups)
]);
return $this->render($request, $response);
}
/**
* Delete Notification Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function deleteForm(Request $request, Response $response, $id)
{
$notification = $this->notificationFactory->getById($id);
if (!$this->getUser()->checkDeleteable($notification)) {
throw new AccessDeniedException();
}
$this->getState()->template = 'notification-form-delete';
$this->getState()->setData([
'notification' => $notification
]);
return $this->render($request, $response);
}
/**
* Add attachment
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|\Slim\Http\Response
* @throws \Xibo\Support\Exception\ConfigurationException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function addAttachment(Request $request, Response $response)
{
$libraryFolder = $this->getConfig()->getSetting('LIBRARY_LOCATION');
// Make sure the library exists
MediaService::ensureLibraryExists($this->getConfig()->getSetting('LIBRARY_LOCATION'));
$options = [
'userId' => $this->getUser()->userId,
'controller' => $this,
'accept_file_types' => '/\.jpg|.jpeg|.png|.bmp|.gif|.zip|.pdf/i'
];
// Output handled by UploadHandler
$this->setNoOutput(true);
$this->getLog()->debug('Hand off to Upload Handler with options: ' . json_encode($options));
// Hand off to the Upload Handler provided by jquery-file-upload
new AttachmentUploadHandler($libraryFolder . 'temp/', $this->getLog()->getLoggerInterface(), $options);
// Explicitly set the Content-Type header to application/json
$response = $response->withHeader('Content-Type', 'application/json');
return $this->render($request, $response);
}
/**
* Add Notification
*
* @SWG\Post(
* path="/notification",
* operationId="notificationAdd",
* tags={"notification"},
* summary="Notification Add",
* description="Add a Notification",
* @SWG\Parameter(
* name="subject",
* in="formData",
* description="The Subject",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="body",
* in="formData",
* description="The Body",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="releaseDt",
* in="formData",
* description="ISO date representing the release date for this notification",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="isInterrupt",
* in="formData",
* description="Flag indication whether this notification should interrupt the web portal nativation/login",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="displayGroupIds",
* in="formData",
* description="The display group ids to assign this notification to",
* type="array",
* required=true,
* @SWG\Items(type="integer")
* ),
* @SWG\Parameter(
* name="userGroupIds",
* in="formData",
* description="The user group ids to assign to this notification",
* type="array",
* required=true,
* @SWG\Items(type="integer")
* ),
* @SWG\Response(
* response=201,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Notification"),
* @SWG\Header(
* header="Location",
* description="Location of the new record",
* type="string"
* )
* )
* )
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws ConfigurationException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function add(Request $request, Response $response)
{
$sanitizedParams = $this->getSanitizer($request->getParams());
$notification = $this->notificationFactory->createEmpty();
$notification->subject = $sanitizedParams->getString('subject');
$notification->body = $request->getParam('body', '');
$notification->createDt = Carbon::now()->format('U');
$notification->releaseDt = $sanitizedParams->getDate('releaseDt');
if ($notification->releaseDt !== null) {
$notification->releaseDt = $notification->releaseDt->format('U');
} else {
$notification->releaseDt = $notification->createDt;
}
$notification->isInterrupt = $sanitizedParams->getCheckbox('isInterrupt');
$notification->userId = $this->getUser()->userId;
$notification->nonusers = $sanitizedParams->getString('nonusers');
$notification->type = 'custom';
// Displays and Users to link
foreach ($sanitizedParams->getIntArray('displayGroupIds', ['default' => [] ]) as $displayGroupId) {
$notification->assignDisplayGroup($this->displayGroupFactory->getById($displayGroupId));
// Notify (don't collect)
$this->displayNotifyService->collectLater()->notifyByDisplayGroupId($displayGroupId);
}
foreach ($sanitizedParams->getIntArray('userGroupIds', ['default' => [] ]) as $userGroupId) {
$notification->assignUserGroup($this->userGroupFactory->getById($userGroupId));
}
$notification->save();
$attachedFilename = $sanitizedParams->getString('attachedFilename', ['defaultOnEmptyString' => true]);
$libraryFolder = $this->getConfig()->getSetting('LIBRARY_LOCATION');
if (!empty($attachedFilename)) {
$saveName = $notification->notificationId .'_' .$attachedFilename;
$notification->filename = $saveName;
$notification->originalFileName = $attachedFilename;
// Move the file into the library
// Try to move the file first
$from = $libraryFolder . 'temp/' . $attachedFilename;
$to = $libraryFolder . 'attachment/' . $saveName;
$moved = rename($from, $to);
if (!$moved) {
$this->getLog()->info(
'Cannot move file: ' . $from . ' to ' . $to . ', will try and copy/delete instead.'
);
// Copy
$moved = copy($from, $to);
// Delete
if (!@unlink($from)) {
$this->getLog()->error('Cannot delete file: ' . $from . ' after copying to ' . $to);
}
}
if (!$moved) {
throw new ConfigurationException(__('Problem moving uploaded file into the Attachment Folder'));
}
$notification->save();
}
// Return
$this->getState()->hydrate([
'httpStatus' => 201,
'message' => sprintf(__('Added %s'), $notification->subject),
'id' => $notification->notificationId,
'data' => $notification
]);
return $this->render($request, $response);
}
/**
* Edit Notification
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
* @SWG\Put(
* path="/notification/{notificationId}",
* operationId="notificationEdit",
* tags={"notification"},
* summary="Notification Edit",
* description="Edit a Notification",
* @SWG\Parameter(
* name="notificationId",
* in="path",
* description="The NotificationId",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="subject",
* in="formData",
* description="The Subject",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="body",
* in="formData",
* description="The Body",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="releaseDt",
* in="formData",
* description="ISO date representing the release date for this notification",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="isInterrupt",
* in="formData",
* description="Flag indication whether this notification should interrupt the web portal nativation/login",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="displayGroupIds",
* in="formData",
* description="The display group ids to assign this notification to",
* type="array",
* required=true,
* @SWG\Items(type="integer")
* ),
* @SWG\Parameter(
* name="userGroupIds",
* in="formData",
* description="The user group ids to assign to this notification",
* type="array",
* required=true,
* @SWG\Items(type="integer")
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Notification")
* )
* )
*/
public function edit(Request $request, Response $response, $id)
{
$notification = $this->notificationFactory->getById($id);
$sanitizedParams = $this->getSanitizer($request->getParams());
$notification->load();
// Check Permissions
if (!$this->getUser()->checkEditable($notification)) {
throw new AccessDeniedException();
}
$notification->subject = $sanitizedParams->getString('subject');
$notification->body = $request->getParam('body', '');
$notification->createDt = Carbon::now()->format('U');
$notification->releaseDt = $sanitizedParams->getDate('releaseDt')->format('U');
$notification->isInterrupt = $sanitizedParams->getCheckbox('isInterrupt');
$notification->userId = $this->getUser()->userId;
$notification->nonusers = $sanitizedParams->getString('nonusers');
// Clear existing assignments
$notification->displayGroups = [];
$notification->userGroups = [];
// Displays and Users to link
foreach ($sanitizedParams->getIntArray('displayGroupIds', ['default' => []]) as $displayGroupId) {
$notification->assignDisplayGroup($this->displayGroupFactory->getById($displayGroupId));
// Notify (don't collect)
$this->displayNotifyService->collectLater()->notifyByDisplayGroupId($displayGroupId);
}
foreach ($sanitizedParams->getIntArray('userGroupIds', ['default' => []]) as $userGroupId) {
$notification->assignUserGroup($this->userGroupFactory->getById($userGroupId));
}
$notification->save();
// Return
$this->getState()->hydrate([
'httpStatus' => 201,
'message' => sprintf(__('Edited %s'), $notification->subject),
'id' => $notification->notificationId,
'data' => $notification
]);
return $this->render($request, $response);
}
/**
* Delete Notification
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
* @SWG\Delete(
* path="/notification/{notificationId}",
* operationId="notificationDelete",
* tags={"notification"},
* summary="Delete Notification",
* description="Delete the provided notification",
* @SWG\Parameter(
* name="notificationId",
* in="path",
* description="The Notification Id to Delete",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*/
public function delete(Request $request, Response $response, $id)
{
$notification = $this->notificationFactory->getById($id);
if (!$this->getUser()->checkDeleteable($notification)) {
throw new AccessDeniedException();
}
$notification->delete();
/*Delete the attachment*/
if (!empty($notification->filename)) {
// Library location
$attachmentLocation = $this->getConfig()->getSetting('LIBRARY_LOCATION'). 'attachment/';
if (file_exists($attachmentLocation . $notification->filename)) {
unlink($attachmentLocation . $notification->filename);
}
}
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Deleted %s'), $notification->subject)
]);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function exportAttachment(Request $request, Response $response, $id)
{
$notification = $this->notificationFactory->getById($id);
$fileName = $this->getConfig()->getSetting('LIBRARY_LOCATION') . 'attachment/' . $notification->filename;
// Return the file with PHP
$this->setNoOutput(true);
return $this->render($request, SendFile::decorateResponse(
$response,
$this->getConfig()->getSetting('SENDFILE_MODE'),
$fileName
));
}
}

View File

@@ -0,0 +1,73 @@
<?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\Controller;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Factory\PlayerFaultFactory;
class PlayerFault extends Base
{
/** @var PlayerFaultFactory */
private $playerFaultFactory;
/**
* PlayerFault constructor.
* @param PlayerFaultFactory $playerFaultFactory
*/
public function __construct(PlayerFaultFactory $playerFaultFactory)
{
$this->playerFaultFactory = $playerFaultFactory;
}
/**
* @param Request $request
* @param Response $response
* @param int $displayId
* @return Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function grid(Request $request, Response $response, int $displayId) : Response
{
$parsedParams = $this->getSanitizer($request->getQueryParams());
if ($displayId != null) {
$playerFaults = $this->playerFaultFactory->getByDisplayId($displayId, $this->gridRenderSort($parsedParams));
} else {
$filter = [
'code' => $parsedParams->getInt('code'),
'incidentDt' => $parsedParams->getDate('incidentDt'),
'displayId' => $parsedParams->getInt('displayId')
];
$playerFaults = $this->playerFaultFactory->query($this->gridRenderSort($parsedParams), $this->gridRenderFilter($filter, $parsedParams));
}
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = $this->playerFaultFactory->countLast();
$this->getState()->setData($playerFaults);
return $this->render($request, $response);
}
}

View File

@@ -0,0 +1,752 @@
<?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\Controller;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Factory\DisplayFactory;
use Xibo\Factory\DisplayProfileFactory;
use Xibo\Factory\MediaFactory;
use Xibo\Factory\ModuleFactory;
use Xibo\Factory\PlayerVersionFactory;
use Xibo\Helper\ByteFormatter;
use Xibo\Service\DownloadService;
use Xibo\Service\MediaService;
use Xibo\Service\MediaServiceInterface;
use Xibo\Service\UploadService;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\ConfigurationException;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
/**
* Class PlayerSoftware
* @package Xibo\Controller
*/
class PlayerSoftware extends Base
{
/** @var \Stash\Interfaces\PoolInterface */
private $pool;
/** @var DisplayProfileFactory */
private $displayProfileFactory;
/** @var PlayerVersionFactory */
private $playerVersionFactory;
/** @var DisplayFactory */
private $displayFactory;
/**
* @var MediaServiceInterface
*/
private $mediaService;
/**
* Notification constructor.
* @param MediaFactory $mediaFactory
* @param PlayerVersionFactory $playerVersionFactory
* @param DisplayProfileFactory $displayProfileFactory
* @param ModuleFactory $moduleFactory
* @param DisplayFactory $displayFactory
*/
public function __construct($pool, $playerVersionFactory, $displayProfileFactory, $displayFactory)
{
$this->pool = $pool;
$this->playerVersionFactory = $playerVersionFactory;
$this->displayProfileFactory = $displayProfileFactory;
$this->displayFactory = $displayFactory;
}
public function getPlayerVersionFactory() : PlayerVersionFactory
{
return $this->playerVersionFactory;
}
public function useMediaService(MediaServiceInterface $mediaService)
{
$this->mediaService = $mediaService;
}
public function getMediaService(): MediaServiceInterface
{
return $this->mediaService->setUser($this->getUser());
}
/**
* Displays the page logic
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
function displayPage(Request $request, Response $response)
{
$this->getState()->template = 'playersoftware-page';
$this->getState()->setData([
'types' => array_map(function ($element) {
return $element->jsonSerialize();
}, $this->playerVersionFactory->getDistinctType()),
'versions' => $this->playerVersionFactory->getDistinctVersion(),
'validExt' => implode('|', $this->getValidExtensions()),
]);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
function grid(Request $request, Response $response)
{
$sanitizedQueryParams = $this->getSanitizer($request->getParams());
$filter = [
'playerType' => $sanitizedQueryParams->getString('playerType'),
'playerVersion' => $sanitizedQueryParams->getString('playerVersion'),
'playerCode' => $sanitizedQueryParams->getInt('playerCode'),
'versionId' => $sanitizedQueryParams->getInt('versionId'),
'useRegexForName' => $sanitizedQueryParams->getCheckbox('useRegexForName'),
'playerShowVersion' => $sanitizedQueryParams->getString('playerShowVersion')
];
$versions = $this->playerVersionFactory->query($this->gridRenderSort($sanitizedQueryParams), $this->gridRenderFilter($filter, $sanitizedQueryParams));
// add row buttons
foreach ($versions as $version) {
$version->setUnmatchedProperty('fileSizeFormatted', ByteFormatter::format($version->size));
if ($this->isApi($request)) {
continue;
}
$version->includeProperty('buttons');
$version->buttons = [];
// Buttons
// Edit
$version->buttons[] = [
'id' => 'content_button_edit',
'url' => $this->urlFor($request, 'playersoftware.edit.form', ['id' => $version->versionId]),
'text' => __('Edit')
];
// Delete Button
$version->buttons[] = [
'id' => 'content_button_delete',
'url' => $this->urlFor($request, 'playersoftware.delete.form', ['id' => $version->versionId]),
'text' => __('Delete'),
'multi-select' => true,
'dataAttributes' => [
[
'name' => 'commit-url',
'value' => $this->urlFor($request, 'playersoftware.delete', ['id' => $version->versionId])
],
['name' => 'commit-method', 'value' => 'delete'],
['name' => 'id', 'value' => 'content_button_delete'],
['name' => 'text', 'value' => __('Delete')],
['name' => 'sort-group', 'value' => 1],
['name' => 'rowtitle', 'value' => $version->fileName]
]
];
// Download
$version->buttons[] = array(
'id' => 'content_button_download',
'linkType' => '_self',
'external' => true,
'url' => $this->urlFor($request, 'playersoftware.download', ['id' => $version->versionId]) . '?attachment=' . $version->fileName,
'text' => __('Download')
);
}
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = $this->playerVersionFactory->countLast();
$this->getState()->setData($versions);
return $this->render($request, $response);
}
/**
* Version Delete Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function deleteForm(Request $request, Response $response, $id)
{
$version = $this->playerVersionFactory->getById($id);
$version->load();
$this->getState()->template = 'playersoftware-form-delete';
$this->getState()->setData([
'version' => $version,
]);
return $this->render($request, $response);
}
/**
* Delete Version
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @SWG\Delete(
* path="/playersoftware/{versionId}",
* operationId="playerSoftwareDelete",
* tags={"Player Software"},
* summary="Delete Version",
* description="Delete Version file from the Library and Player Versions table",
* @SWG\Parameter(
* name="versionId",
* in="path",
* description="The Version ID to Delete",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*/
public function delete(Request $request, Response $response, $id)
{
$version = $this->playerVersionFactory->getById($id);
$version->load();
// Unset player version from Display Profile
$displayProfiles = $this->displayProfileFactory->query();
foreach ($displayProfiles as $displayProfile) {
if (in_array($displayProfile->type, ['android', 'lg', 'sssp'])) {
$currentVersionId = $displayProfile->getSetting('versionMediaId');
if ($currentVersionId === $version->versionId) {
$displayProfile->setSetting('versionMediaId', null);
$displayProfile->save();
}
} else if ($displayProfile->type === 'chromeOS') {
$currentVersionId = $displayProfile->getSetting('playerVersionId');
if ($currentVersionId === $version->versionId) {
$displayProfile->setSetting('playerVersionId', null);
$displayProfile->save();
}
}
}
// Delete
$version->delete();
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Deleted %s'), $version->playerShowVersion)
]);
return $this->render($request, $response);
}
/**
* Edit Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function editForm(Request $request, Response $response, $id)
{
$version = $this->playerVersionFactory->getById($id);
$this->getState()->template = 'playersoftware-form-edit';
$this->getState()->setData([
'version' => $version,
]);
return $this->render($request, $response);
}
/**
* Edit Player Version
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @SWG\Put(
* path="/playersoftware/{versionId}",
* operationId="playersoftwareEdit",
* tags={"Player Software"},
* summary="Edit Player Version",
* description="Edit a Player Version file information",
* @SWG\Parameter(
* name="versionId",
* in="path",
* description="The Version ID to Edit",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="playerShowVersion",
* in="formData",
* description="The Name of the player version application, this will be displayed in Version dropdowns in Display Profile and Display",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="version",
* in="formData",
* description="The Version number",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="code",
* in="formData",
* description="The Code number",
* type="integer",
* required=false
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Media")
* )
* )
*/
public function edit(Request $request, Response $response, $id)
{
$version = $this->playerVersionFactory->getById($id);
$sanitizedParams = $this->getSanitizer($request->getParams());
$version->version = $sanitizedParams->getString('version');
$version->code = $sanitizedParams->getInt('code');
$version->playerShowVersion = $sanitizedParams->getString('playerShowVersion');
$version->modifiedBy = $this->getUser()->userName;
$version->save();
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Edited %s'), $version->playerShowVersion),
'id' => $version->versionId,
'data' => $version
]);
return $this->render($request, $response);
}
/**
* Install Route for SSSP XML
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function getSsspInstall(Request $request, Response $response)
{
// Get the default SSSP display profile
$profile = $this->displayProfileFactory->getDefaultByType('sssp');
// See if it has a version file (if not or we can't load it, 404)
$versionId = $profile->getSetting('versionMediaId');
if ($versionId !== null) {
$version = $this->playerVersionFactory->getById($versionId);
$xml = $this->outputSsspXml($version->version . '.' . $version->code, $version->size);
$response = $response
->withHeader('Content-Type', 'application/xml')
->write($xml);
} else {
return $response->withStatus(404);
}
$this->setNoOutput(true);
return $this->render($request, $response);
}
/**
* Install Route for SSSP WGT
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function getSsspInstallDownload(Request $request, Response $response)
{
// Get the default SSSP display profile
$profile = $this->displayProfileFactory->getDefaultByType('sssp');
// See if it has a version file (if not, or we can't load it, 404)
$versionId = $profile->getSetting('versionMediaId');
if ($versionId !== null) {
$response = $this->download($request, $response, $versionId);
} else {
return $response->withStatus(404);
}
$this->setNoOutput();
return $this->render($request, $response);
}
/**
* Upgrade Route for SSSP XML
* @param Request $request
* @param Response $response
* @param $nonce
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function getSssp(Request $request, Response $response, $nonce)
{
// Use the cache to get the displayId for this nonce
$cache = $this->pool->getItem('/playerVersion/' . $nonce);
if ($cache->isMiss()) {
$response = $response->withStatus(404);
$this->setNoOutput(true);
return $this->render($request, $response);
}
$displayId = $cache->get();
// Get the Display
$display = $this->displayFactory->getById($displayId);
// Check if display is SSSP, throw Exception if it's not
if ($display->clientType != 'sssp') {
throw new InvalidArgumentException(__('File available only for SSSP displays'), 'clientType');
}
// Add the correct header
$response = $response->withHeader('content-type', 'application/xml');
// get the media ID from display profile
$versionId = $display->getSetting('versionMediaId', null, ['displayOverride' => true]);
if ($versionId !== null) {
$versionInformation = $this->playerVersionFactory->getById($versionId);
$xml = $this->outputSsspXml($versionInformation->version . '.' . $versionInformation->code, $versionInformation->size);
$response = $response->write($xml);
} else {
return $response->withStatus(404);
}
$this->setNoOutput(true);
return $this->render($request, $response);
}
/**
* Upgrade Route for SSSP WGT
* @param Request $request
* @param Response $response
* @param $nonce
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function getVersionFile(Request $request, Response $response, $nonce)
{
// Use the cache to get the displayId for this nonce
$cache = $this->pool->getItem('/playerVersion/' . $nonce);
if ($cache->isMiss()) {
$response = $response->withStatus(404);
$this->setNoOutput(true);
return $this->render($request, $response);
}
$displayId = $cache->get();
// Get display and media
$display = $this->displayFactory->getById($displayId);
$versionId = $display->getSetting('versionMediaId', null, ['displayOverride' => true]);
if ($versionId !== null) {
$response = $this->download($request, $response, $versionId);
} else {
return $response->withStatus(404);
}
$this->setNoOutput(true);
return $this->render($request, $response);
}
/**
* Player Software Upload
*
* @SWG\Post(
* path="/playersoftware",
* operationId="playersoftwareUpload",
* tags={"Player Software"},
* summary="Player Software Upload",
* description="Upload a new Player version file",
* @SWG\Parameter(
* name="files",
* in="formData",
* description="The Uploaded File",
* type="file",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="successful operation"
* )
* )
*
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws ConfigurationException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\DuplicateEntityException
*/
public function add(Request $request, Response $response)
{
if (!$this->getUser()->featureEnabled('playersoftware.add')) {
throw new AccessDeniedException();
}
$libraryFolder = $this->getConfig()->getSetting('LIBRARY_LOCATION');
// Make sure the library exists
MediaService::ensureLibraryExists($libraryFolder);
$validExt = $this->getValidExtensions();
// Make sure there is room in the library
$libraryLimit = $this->getConfig()->getSetting('LIBRARY_SIZE_LIMIT_KB') * 1024;
$options = [
'accept_file_types' => '/\.' . implode('|', $validExt) . '$/i',
'libraryLimit' => $libraryLimit,
'libraryQuotaFull' => ($libraryLimit > 0 && $this->getMediaService()->libraryUsage() > $libraryLimit),
];
// Output handled by UploadHandler
$this->setNoOutput(true);
$this->getLog()->debug('Hand off to Upload Handler with options: ' . json_encode($options));
// Hand off to the Upload Handler provided by jquery-file-upload
$uploadService = new UploadService($libraryFolder . 'temp/', $options, $this->getLog(), $this->getState());
$uploadHandler = $uploadService->createUploadHandler();
$uploadHandler->setPostProcessor(function ($file, $uploadHandler) use ($libraryFolder, $request) {
// Return right away if the file already has an error.
if (!empty($file->error)) {
$this->getState()->setCommitState(false);
return $file;
}
$this->getUser()->isQuotaFullByUser(true);
// Get the uploaded file and move it to the right place
$filePath = $libraryFolder . 'temp/' . $file->fileName;
// Add the Player Software record
$playerSoftware = $this->getPlayerVersionFactory()->createEmpty();
$playerSoftware->modifiedBy = $this->getUser()->userName;
// SoC players have issues parsing fileNames with spaces in them
// replace any unexpected character in fileName with -
$playerSoftware->fileName = preg_replace('/[^a-zA-Z0-9_.]+/', '-', $file->fileName);
$playerSoftware->size = filesize($filePath);
$playerSoftware->md5 = md5_file($filePath);
$playerSoftware->decorateRecord();
// if the name was provided on upload use that here.
if (!empty($file->name)) {
$playerSoftware->playerShowVersion = $file->name;
}
$playerSoftware->save();
// Test to ensure the final file size is the same as the file size we're expecting
if ($file->size != $playerSoftware->size) {
throw new InvalidArgumentException(
__('Sorry this is a corrupted upload, the file size doesn\'t match what we\'re expecting.'),
'size'
);
}
// everything is fine, move the file from temp folder.
rename($filePath, $libraryFolder . 'playersoftware/' . $playerSoftware->fileName);
// Unpack if necessary
$playerSoftware->unpack($libraryFolder, $request);
// return
$file->id = $playerSoftware->versionId;
$file->md5 = $playerSoftware->md5;
$file->name = $playerSoftware->fileName;
return $file;
});
$uploadHandler->post();
// Explicitly set the Content-Type header to application/json
$response = $response->withHeader('Content-Type', 'application/json');
return $this->render($request, $response);
}
/**
* @SWG\Get(
* path="/playersoftware/download/{id}",
* operationId="playersoftwareDownload",
* tags={"Player Software"},
* summary="Download Player Version file",
* description="Download Player Version file",
* produces={"application/octet-stream"},
* @SWG\Parameter(
* name="id",
* in="path",
* description="The Player Version ID to Download",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(type="file"),
* @SWG\Header(
* header="X-Sendfile",
* description="Apache Send file header - if enabled.",
* type="string"
* ),
* @SWG\Header(
* header="X-Accel-Redirect",
* description="nginx send file header - if enabled.",
* type="string"
* )
* )
* )
*
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\GeneralException
*/
public function download(Request $request, Response $response, $id)
{
$playerVersion = $this->playerVersionFactory->getById($id);
$this->getLog()->debug('Download request for player software versionId: ' . $id);
$library = $this->getConfig()->getSetting('LIBRARY_LOCATION');
$sendFileMode = $this->getConfig()->getSetting('SENDFILE_MODE');
$libraryPath = $library . 'playersoftware' . DIRECTORY_SEPARATOR . $playerVersion->fileName;
$attachmentName = urlencode($playerVersion->fileName);
$downLoadService = new DownloadService($libraryPath, $sendFileMode);
$downLoadService->useLogger($this->getLog()->getLoggerInterface());
return $downLoadService->returnFile(
$response,
$attachmentName,
'/download/playersoftware/' . $playerVersion->fileName
);
}
/**
* Output the SSSP XML
* @param $version
* @param $size
* @return string
*/
private function outputSsspXml($version, $size)
{
// create sssp_config XML file with provided information
$ssspDocument = new \DOMDocument('1.0', 'UTF-8');
$versionNode = $ssspDocument->createElement('widget');
$version = $ssspDocument->createElement('ver', $version);
$size = $ssspDocument->createElement('size', $size);
// Our widget name is always sssp_dl (this is appended to both the install and upgrade routes)
$name = $ssspDocument->createElement('widgetname', 'sssp_dl');
$ssspDocument->appendChild($versionNode);
$versionNode->appendChild($version);
$versionNode->appendChild($size);
$versionNode->appendChild($name);
$versionNode->appendChild($ssspDocument->createElement('webtype', 'tizen'));
$ssspDocument->formatOutput = true;
return $ssspDocument->saveXML();
}
/**
* @return string[]
*/
private function getValidExtensions()
{
return ['apk', 'ipk', 'wgt', 'chrome'];
}
}

2113
lib/Controller/Playlist.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,261 @@
<?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\Controller;
use Psr\Container\ContainerInterface;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Event\SubPlaylistItemsEvent;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\NotFoundException;
/**
* Class PlaylistDashboard
* @package Xibo\Controller
*/
class PlaylistDashboard extends Base
{
/** @var \Xibo\Factory\PlaylistFactory */
private $playlistFactory;
/** @var \Xibo\Factory\ModuleFactory */
private $moduleFactory;
/** @var \Xibo\Factory\WidgetFactory */
private $widgetFactory;
/** @var \Xibo\Factory\MediaFactory */
private $mediaFactory;
/** @var ContainerInterface */
private $container;
/**
* PlaylistDashboard constructor.
* @param $playlistFactory
* @param $moduleFactory
* @param $widgetFactory
* @param \Xibo\Factory\MediaFactory $mediaFactory
* @param ContainerInterface $container
*/
public function __construct($playlistFactory, $moduleFactory, $widgetFactory, $mediaFactory, ContainerInterface $container)
{
$this->playlistFactory = $playlistFactory;
$this->moduleFactory = $moduleFactory;
$this->widgetFactory = $widgetFactory;
$this->mediaFactory = $mediaFactory;
$this->container = $container;
}
/**
* @param \Slim\Http\ServerRequest $request
* @param \Slim\Http\Response $response
* @return \Psr\Http\Message\ResponseInterface|\Slim\Http\Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function displayPage(Request $request, Response $response)
{
// Do we have a Playlist already in our User Preferences?
$playlist = null;
try {
$playlistId = $this->getUser()->getOption('playlistDashboardSelectedPlaylistId');
if ($playlistId->value != 0) {
$playlist = $this->playlistFactory->getById($playlistId->value);
}
} catch (NotFoundException $notFoundException) {
// this is fine, no need to throw errors here.
$this->getLog()->debug(
'Problem getting playlistDashboardSelectedPlaylistId user option. e = ' .
$notFoundException->getMessage()
);
}
$this->getState()->template = 'playlist-dashboard';
$this->getState()->setData([
'playlist' => $playlist,
'validExtensions' => implode('|', $this->moduleFactory->getValidExtensions())
]);
return $this->render($request, $response);
}
/**
* Grid used for the Playlist drop down list
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function grid(Request $request, Response $response)
{
$sanitizedParams = $this->getSanitizer($request->getParams());
// Playlists
$playlists = $this->playlistFactory->query($this->gridRenderSort($sanitizedParams), $this->gridRenderFilter([
'name' => $this->getSanitizer($request->getParams())->getString('name'),
'regionSpecific' => 0
], $sanitizedParams));
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = $this->playlistFactory->countLast();
$this->getState()->setData($playlists);
return $this->render($request, $response);
}
/**
* Show a particular playlist
* the output from this is very much like a form.
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function show(Request $request, Response $response, $id)
{
// Record this Playlist as the one we have currently selected.
try {
$this->getUser()->setOptionValue('playlistDashboardSelectedPlaylistId', $id);
$this->getUser()->save();
} catch (GeneralException $exception) {
$this->getLog()->error('Problem setting playlistDashboardSelectedPlaylistId user option. e = ' . $exception->getMessage());
}
// Spots
$spotsFound = 0;
$playlist = $this->playlistFactory->getById($id);
// Only edit permissions
if (!$this->getUser()->checkEditable($playlist)) {
throw new AccessDeniedException();
}
$this->getLog()->debug('show: testing to see if ' . $playlist->name . ' / ' . $playlist->playlistId
. ' is the first playlist in any other ones.');
// Work out the slot size of the first sub-playlist we are in.
foreach ($this->playlistFactory->query(null, [
'childId' => $playlist->playlistId,
'depth' => 1,
'disableUserCheck' => 1
]) as $parent) {
// $parent is a playlist to which we belong.
$this->getLog()->debug('show: This playlist is a sub-playlist in ' . $parent->name . '.');
$parent->load();
foreach ($parent->widgets as $parentWidget) {
if ($parentWidget->type === 'subplaylist') {
$this->getLog()->debug('show: matched against a sub playlist widget ' . $parentWidget->widgetId . '.');
// Get the sub-playlist widgets
$event = new SubPlaylistItemsEvent($parentWidget);
$this->getDispatcher()->dispatch($event, SubPlaylistItemsEvent::$NAME);
foreach ($event->getItems() as $subPlaylistItem) {
$this->getLog()->debug('show: Assessing playlist ' . $subPlaylistItem->playlistId . ' on ' . $playlist->name);
if ($subPlaylistItem->playlistId == $playlist->playlistId) {
// Take the highest number of Spots we can find out of all the assignments.
$spotsFound = max($subPlaylistItem->spots ?? 0, $spotsFound);
// Assume this one isn't in the list more than one time.
break 2;
}
}
$this->getLog()->debug('show: no matching playlists found.');
}
}
}
// Load my Playlist and information about its widgets
if ($spotsFound > 0) {
// We are in a sub-playlist with spots, so now we load our widgets.
$playlist->load();
$user = $this->getUser();
foreach ($playlist->widgets as $widget) {
// Create a module for the widget and load in some extra data
$module = $this->moduleFactory->getByType($widget->type);
$widget->setUnmatchedProperty('name', $widget->getOptionValue('name', $module->name));
$widget->setUnmatchedProperty('regionSpecific', $module->regionSpecific);
$widget->setUnmatchedProperty('moduleIcon', $module->icon);
// Check my permissions
if ($module->regionSpecific == 0) {
$media = $this->mediaFactory->getById($widget->getPrimaryMediaId());
$widget->setUnmatchedProperty('viewble', $user->checkViewable($media));
$widget->setUnmatchedProperty('editable', $user->checkEditable($media));
$widget->setUnmatchedProperty('deletable', $user->checkDeleteable($media));
} else {
$widget->setUnmatchedProperty('viewble', $user->checkViewable($widget));
$widget->setUnmatchedProperty('editable', $user->checkEditable($widget));
$widget->setUnmatchedProperty('deletable', $user->checkDeleteable($widget));
}
}
}
$this->getState()->template = 'playlist-dashboard-spots';
$this->getState()->setData([
'playlist' => $playlist,
'spotsFound' => $spotsFound
]);
return $this->render($request, $response);
}
/**
* Delete Playlist Widget Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function deletePlaylistWidgetForm(Request $request, Response $response, $id)
{
$widget = $this->widgetFactory->loadByWidgetId($id);
if (!$this->getUser()->checkDeleteable($widget)) {
throw new AccessDeniedException();
}
// Pass to view
$this->getState()->template = 'playlist-module-form-delete';
$this->getState()->setData([
'widget' => $widget,
]);
return $this->render($request, $response);
}
}

155
lib/Controller/Preview.php Normal file
View File

@@ -0,0 +1,155 @@
<?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\Controller;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Factory\LayoutFactory;
use Xibo\Support\Exception\AccessDeniedException;
/**
* Class Preview
* @package Xibo\Controller
*/
class Preview extends Base
{
/**
* @var LayoutFactory
*/
private $layoutFactory;
/**
* Set common dependencies.
* @param LayoutFactory $layoutFactory
*/
public function __construct($layoutFactory)
{
$this->layoutFactory = $layoutFactory;
}
/**
* Layout Preview
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function show(Request $request, Response $response, $id)
{
$sanitizedParams = $this->getSanitizer($request->getParams());
// Get the layout
if ($sanitizedParams->getInt('findByCode') === 1) {
$layout = $this->layoutFactory->getByCode($id);
} else {
$layout = $this->layoutFactory->getById($id);
}
if (!$this->getUser()->checkViewable($layout)
|| !$this->getUser()->featureEnabled(['layout.view', 'playlist.view', 'campaign.view'])
) {
throw new AccessDeniedException();
}
// Do we want to preview the draft version of this Layout?
if ($sanitizedParams->getCheckbox('isPreviewDraft') && $layout->hasDraft()) {
$layout = $this->layoutFactory->getByParentId($layout->layoutId);
}
$this->getState()->template = 'layout-renderer';
$this->getState()->setData([
'layout' => $layout,
'previewOptions' => [
'getXlfUrl' => $this->urlFor($request, 'layout.getXlf', ['id' => $layout->layoutId]),
'getResourceUrl' => $this->urlFor($request, 'module.getResource', [
'regionId' => ':regionId', 'id' => ':id'
]),
'libraryDownloadUrl' => $this->urlFor($request, 'library.download', ['id' => ':id']),
'layoutBackgroundDownloadUrl' => $this->urlFor($request, 'layout.download.background', ['id' => ':id']),
'loaderUrl' => $this->getConfig()->uri('img/loader.gif'),
'layoutPreviewUrl' => $this->urlFor($request, 'layout.preview', ['id' => '[layoutCode]'])
]
]);
return $this->render($request, $response);
}
/**
* Get the XLF for a Layout
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\InvalidArgumentException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function getXlf(Request $request, Response $response, $id)
{
$layout = $this->layoutFactory->concurrentRequestLock($this->layoutFactory->getById($id));
try {
if (!$this->getUser()->checkViewable($layout)) {
throw new AccessDeniedException();
}
echo file_get_contents($layout->xlfToDisk([
'notify' => false,
'collectNow' => false,
]));
$this->setNoOutput();
} finally {
// Release lock
$this->layoutFactory->concurrentRequestRelease($layout);
}
return $this->render($request, $response);
}
/**
* Return the player bundle
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
*/
public function playerBundle(Request $request, Response $response)
{
$params = $this->getSanitizer($request->getParams());
$isMap = $params->getCheckbox('map');
if ($isMap) {
$bundle = file_get_contents(PROJECT_ROOT . '/modules/bundle.min.js.map');
} else {
$bundle = file_get_contents(PROJECT_ROOT . '/modules/bundle.min.js');
}
$response->getBody()->write($bundle);
return $response->withStatus(200)
->withHeader('Content-Size', strlen($bundle))
->withHeader('Content-Type', 'application/javascript');
}
}

207
lib/Controller/Pwa.php Normal file
View File

@@ -0,0 +1,207 @@
<?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\Controller;
use Psr\Container\ContainerInterface;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Factory\DisplayFactory;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Xmds\Soap7;
/**
* PWA
* routes for a PWA to download resources which live in an iframe
*/
class Pwa extends Base
{
public function __construct(
private readonly DisplayFactory $displayFactory,
private readonly ContainerInterface $container
) {
}
/**
* @throws \Xibo\Support\Exception\InvalidArgumentException
* @throws \Psr\Container\NotFoundExceptionInterface
* @throws \Xibo\Support\Exception\NotFoundException
* @throws \Psr\Container\ContainerExceptionInterface
* @throws \Xibo\Support\Exception\AccessDeniedException
* @throws \Xibo\Support\Exception\GeneralException
*/
public function getResource(Request $request, Response $response): Response
{
// Create a Soap client and call it.
$params = $this->getSanitizer($request->getParams());
try {
// Which version are we?
$version = $params->getInt('v', [
'default' => 7,
'throw' => function () {
throw new InvalidArgumentException(__('Missing Version'), 'v');
}
]);
if ($version < 7) {
throw new InvalidArgumentException(__('PWA supported from XMDS schema 7 onward.'), 'v');
}
// Validate that this display should call this service.
$hardwareKey = $params->getString('hardwareKey');
$display = $this->displayFactory->getByLicence($hardwareKey);
if (!$display->isPwa()) {
throw new AccessDeniedException(__('Please use XMDS API'), 'hardwareKey');
}
// Check it is still authorised.
if ($display->licensed == 0) {
throw new AccessDeniedException(__('Display unauthorised'));
}
/** @var Soap7 $soap */
$soap = $this->getSoap($version);
$this->getLog()->debug('getResource: passing to Soap class');
$body = $soap->GetResource(
$params->getString('serverKey'),
$params->getString('hardwareKey'),
$params->getInt('layoutId'),
$params->getInt('regionId') . '',
$params->getInt('mediaId') . '',
);
$response->getBody()->write($body);
return $response
->withoutHeader('Content-Security-Policy');
} catch (\SoapFault $e) {
throw new GeneralException($e->getMessage());
}
}
/**
* @throws \Xibo\Support\Exception\InvalidArgumentException
* @throws \Psr\Container\NotFoundExceptionInterface
* @throws \Psr\Container\ContainerExceptionInterface
* @throws \Xibo\Support\Exception\NotFoundException
* @throws \Xibo\Support\Exception\GeneralException
*/
public function getData(Request $request, Response $response): Response
{
$params = $this->getSanitizer($request->getParams());
try {
$version = $params->getInt('v', [
'default' => 7,
'throw' => function () {
throw new InvalidArgumentException(__('Missing Version'), 'v');
}
]);
if ($version < 7) {
throw new InvalidArgumentException(__('PWA supported from XMDS schema 7 onward.'), 'v');
}
// Validate that this display should call this service.
$hardwareKey = $params->getString('hardwareKey');
$display = $this->displayFactory->getByLicence($hardwareKey);
if (!$display->isPwa()) {
throw new AccessDeniedException(__('Please use XMDS API'), 'hardwareKey');
}
// Check it is still authorised.
if ($display->licensed == 0) {
throw new AccessDeniedException(__('Display unauthorised'));
}
/** @var Soap7 $soap */
$soap = $this->getSoap($version);
$body = $soap->GetData(
$params->getString('serverKey'),
$params->getString('hardwareKey'),
$params->getInt('widgetId'),
);
$response->getBody()->write($body);
return $response
->withoutHeader('Content-Security-Policy');
} catch (\SoapFault $e) {
throw new GeneralException($e->getMessage());
}
}
/**
* @throws \Xibo\Support\Exception\InvalidArgumentException
* @throws \Psr\Container\ContainerExceptionInterface
* @throws \Psr\Container\NotFoundExceptionInterface
*/
private function getSoap(int $version): mixed
{
$class = '\Xibo\Xmds\Soap' . $version;
if (!class_exists($class)) {
throw new InvalidArgumentException(__('Unknown version'), 'version');
}
// Overwrite the logger
$uidProcessor = new \Monolog\Processor\UidProcessor(7);
$logProcessor = new \Xibo\Xmds\LogProcessor(
$this->container->get('logger'),
$uidProcessor->getUid()
);
$this->container->get('logger')->pushProcessor($logProcessor);
return new $class(
$logProcessor,
$this->container->get('pool'),
$this->container->get('store'),
$this->container->get('timeSeriesStore'),
$this->container->get('logService'),
$this->container->get('sanitizerService'),
$this->container->get('configService'),
$this->container->get('requiredFileFactory'),
$this->container->get('moduleFactory'),
$this->container->get('layoutFactory'),
$this->container->get('dataSetFactory'),
$this->displayFactory,
$this->container->get('userGroupFactory'),
$this->container->get('bandwidthFactory'),
$this->container->get('mediaFactory'),
$this->container->get('widgetFactory'),
$this->container->get('regionFactory'),
$this->container->get('notificationFactory'),
$this->container->get('displayEventFactory'),
$this->container->get('scheduleFactory'),
$this->container->get('dayPartFactory'),
$this->container->get('playerVersionFactory'),
$this->container->get('dispatcher'),
$this->container->get('campaignFactory'),
$this->container->get('syncGroupFactory'),
$this->container->get('playerFaultFactory')
);
}
}

836
lib/Controller/Region.php Normal file
View File

@@ -0,0 +1,836 @@
<?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\Controller;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Event\RegionAddedEvent;
use Xibo\Event\SubPlaylistWidgetsEvent;
use Xibo\Factory\LayoutFactory;
use Xibo\Factory\ModuleFactory;
use Xibo\Factory\RegionFactory;
use Xibo\Factory\TransitionFactory;
use Xibo\Factory\WidgetFactory;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\ControllerNotImplemented;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
/**
* Class Region
* @package Xibo\Controller
*/
class Region extends Base
{
/**
* @var RegionFactory
*/
private $regionFactory;
/** @var WidgetFactory */
private $widgetFactory;
/**
* @var ModuleFactory
*/
private $moduleFactory;
/**
* @var LayoutFactory
*/
private $layoutFactory;
/**
* @var TransitionFactory
*/
private $transitionFactory;
/**
* Set common dependencies.
* @param RegionFactory $regionFactory
* @param WidgetFactory $widgetFactory
* @param TransitionFactory $transitionFactory
* @param ModuleFactory $moduleFactory
* @param LayoutFactory $layoutFactory
*/
public function __construct(
$regionFactory,
$widgetFactory,
$transitionFactory,
$moduleFactory,
$layoutFactory
) {
$this->regionFactory = $regionFactory;
$this->widgetFactory = $widgetFactory;
$this->transitionFactory = $transitionFactory;
$this->layoutFactory = $layoutFactory;
$this->moduleFactory = $moduleFactory;
}
/**
* Get region by id
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws ControllerNotImplemented
*/
public function get(Request $request, Response $response, $id)
{
$region = $this->regionFactory->getById($id);
if (!$this->getUser()->checkEditable($region)) {
throw new AccessDeniedException();
}
$this->getState()->setData([
'region' => $region,
'layout' => $this->layoutFactory->getById($region->layoutId),
'transitions' => $this->transitionData(),
]);
return $this->render($request, $response);
}
/**
* Add a region
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws ControllerNotImplemented
* @SWG\Post(
* path="/region/{id}",
* operationId="regionAdd",
* tags={"layout"},
* summary="Add Region",
* description="Add a Region to a Layout",
* @SWG\Parameter(
* name="id",
* in="path",
* description="The Layout ID to add the Region to",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="type",
* in="formData",
* description="The type of region this should be, zone, frame, playlist or canvas. Default = frame.",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="width",
* in="formData",
* description="The Width, default 250",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="height",
* in="formData",
* description="The Height",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="top",
* in="formData",
* description="The Top Coordinate",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="left",
* in="formData",
* description="The Left Coordinate",
* type="integer",
* required=false
* ),
* @SWG\Response(
* response=201,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Region"),
* @SWG\Header(
* header="Location",
* description="Location of the new record",
* type="string"
* )
* )
* )
*/
public function add(Request $request, Response $response, $id)
{
$layout = $this->layoutFactory->getById($id);
$sanitizedParams = $this->getSanitizer($request->getParams());
if (!$this->getUser()->checkEditable($layout)) {
throw new AccessDeniedException();
}
if (!$layout->isChild()) {
throw new InvalidArgumentException(__('This Layout is not a Draft, please checkout.'), 'layoutId');
}
$layout->load([
'loadPlaylists' => true,
'loadTags' => false,
'loadPermissions' => true,
'loadCampaigns' => false
]);
// Add a new region
$region = $this->regionFactory->create(
$sanitizedParams->getString('type', ['default' => 'frame']),
$this->getUser()->userId,
'',
$sanitizedParams->getInt('width', ['default' => 250]),
$sanitizedParams->getInt('height', ['default' => 250]),
$sanitizedParams->getInt('top', ['default' => 50]),
$sanitizedParams->getInt('left', ['default' => 50]),
$sanitizedParams->getInt('zIndex', ['default' => 0])
);
$layout->regions[] = $region;
$layout->save([
'saveTags' => false
]);
// Dispatch an event to say that we have added a region
$this->getDispatcher()->dispatch(new RegionAddedEvent($layout, $region), RegionAddedEvent::$NAME);
// Return
$this->getState()->hydrate([
'httpStatus' => 201,
'message' => sprintf(__('Added %s'), $region->name),
'id' => $region->regionId,
'data' => $region
]);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws ControllerNotImplemented
* @SWG\Put(
* path="/region/{id}",
* operationId="regionEdit",
* tags={"layout"},
* summary="Edit Region",
* description="Edit Region",
* @SWG\Parameter(
* name="id",
* in="path",
* description="The Region ID to Edit",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="width",
* in="formData",
* description="The Width, default 250",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="height",
* in="formData",
* description="The Height",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="top",
* in="formData",
* description="The Top Coordinate",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="left",
* in="formData",
* description="The Left Coordinate",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="zIndex",
* in="formData",
* description="The Layer for this Region",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="transitionType",
* in="formData",
* description="The Transition Type. Must be a valid transition code as returned by /transition",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="transitionDuration",
* in="formData",
* description="The transition duration in milliseconds if required by the transition type",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="transitionDirection",
* in="formData",
* description="The transition direction if required by the transition type.",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="loop",
* in="formData",
* description="Flag indicating whether this region should loop if there is only 1 media item in the timeline",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Region")
* )
* )
*/
public function edit(Request $request, Response $response, $id)
{
$region = $this->regionFactory->getById($id);
$sanitizedParams = $this->getSanitizer($request->getParams());
if (!$this->getUser()->checkEditable($region)) {
throw new AccessDeniedException();
}
// Check that this Regions Layout is in an editable state
$layout = $this->layoutFactory->getById($region->layoutId);
if (!$layout->isChild()) {
throw new InvalidArgumentException(__('This Layout is not a Draft, please checkout.'), 'layoutId');
}
// Load before we save
$region->load();
$region->name = $sanitizedParams->getString('name');
$region->width = $sanitizedParams->getDouble('width');
$region->height = $sanitizedParams->getDouble('height');
$region->top = $sanitizedParams->getDouble('top', ['default' => 0]);
$region->left = $sanitizedParams->getDouble('left', ['default' => 0]);
$region->zIndex = $sanitizedParams->getInt('zIndex');
$region->type = $sanitizedParams->getString('type');
$region->syncKey = $sanitizedParams->getString('syncKey', ['defaultOnEmptyString' => true]);
// Loop
$region->setOptionValue('loop', $sanitizedParams->getCheckbox('loop'));
// Transitions
$region->setOptionValue('transitionType', $sanitizedParams->getString('transitionType'));
$region->setOptionValue('transitionDuration', $sanitizedParams->getInt('transitionDuration'));
$region->setOptionValue('transitionDirection', $sanitizedParams->getString('transitionDirection'));
// Save
$region->save();
// Mark the layout as needing rebuild
$layout->load(\Xibo\Entity\Layout::$loadOptionsMinimum);
$saveOptions = \Xibo\Entity\Layout::$saveOptionsMinimum;
$saveOptions['setBuildRequired'] = true;
$layout->save($saveOptions);
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Edited %s'), $region->name),
'id' => $region->regionId,
'data' => $region
]);
return $this->render($request, $response);
}
/**
* Delete a region
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws ControllerNotImplemented
* @SWG\Delete(
* path="/region/{regionId}",
* operationId="regionDelete",
* tags={"layout"},
* summary="Region Delete",
* description="Delete an existing region",
* @SWG\Parameter(
* name="regionId",
* in="path",
* description="The Region ID to Delete",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*/
public function delete(Request $request, Response $response, $id)
{
$region = $this->regionFactory->getById($id);
if (!$this->getUser()->checkDeleteable($region)) {
throw new AccessDeniedException();
}
// Check that this Regions Layout is in an editable state
$layout = $this->layoutFactory->getById($region->layoutId);
if (!$layout->isChild())
throw new InvalidArgumentException(__('This Layout is not a Draft, please checkout.'), 'layoutId');
$region->delete();
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Deleted %s'), $region->name)
]);
return $this->render($request, $response);
}
/**
* Update Positions
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws ControllerNotImplemented
* @SWG\Put(
* path="/region/position/all/{layoutId}",
* operationId="regionPositionAll",
* tags={"layout"},
* summary="Position Regions",
* description="Position all regions for a Layout",
* @SWG\Parameter(
* name="layoutId",
* in="path",
* description="The Layout ID",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="regions",
* in="formData",
* description="Array of regions and their new positions. Each array element should be json encoded and have regionId, top, left, width and height.",
* type="array",
* required=true,
* @SWG\Items(
* type="string"
* )
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Layout")
* )
* )
*/
function positionAll(Request $request, Response $response, $id)
{
// Create the layout
$layout = $this->layoutFactory->loadById($id);
if (!$this->getUser()->checkEditable($layout)) {
throw new AccessDeniedException();
}
// Check that this Layout is a Draft
if (!$layout->isChild()) {
throw new InvalidArgumentException(__('This Layout is not a Draft, please checkout.'), 'layoutId');
}
// Pull in the regions and convert them to stdObjects
$regions = $request->getParam('regions', null);
if ($regions == null) {
throw new InvalidArgumentException(__('No regions present'));
}
$regions = json_decode($regions);
// Go through each region and update the region in the layout we have
foreach ($regions as $newCoordinates) {
// TODO attempt to sanitize?
// Check that the properties we are expecting do actually exist
if (!property_exists($newCoordinates, 'regionid'))
throw new InvalidArgumentException(__('Missing regionid property'));
if (!property_exists($newCoordinates, 'top'))
throw new InvalidArgumentException(__('Missing top property'));
if (!property_exists($newCoordinates, 'left'))
throw new InvalidArgumentException(__('Missing left property'));
if (!property_exists($newCoordinates, 'width'))
throw new InvalidArgumentException(__('Missing width property'));
if (!property_exists($newCoordinates, 'height'))
throw new InvalidArgumentException(__('Missing height property'));
$regionId = $newCoordinates->regionid;
// Load the region
$region = $layout->getRegion($regionId);
// Check Permissions
if (!$this->getUser()->checkEditable($region)) {
throw new AccessDeniedException();
}
// New coordinates
$region->top = $newCoordinates->top;
$region->left = $newCoordinates->left;
$region->width = $newCoordinates->width;
$region->height = $newCoordinates->height;
$region->zIndex = $newCoordinates->zIndex;
$this->getLog()->debug('Set ' . $region);
}
// Mark the layout as having changed
$layout->status = 0;
$layout->save();
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Edited %s'), $layout->layout),
'id' => $layout->layoutId,
'data' => $layout
]);
return $this->render($request, $response);
}
/**
* Represents the Preview inside the Layout Designer
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function preview(Request $request, Response $response, $id)
{
$sanitizedQuery = $this->getSanitizer($request->getParams());
$widgetId = $sanitizedQuery->getInt('widgetId', ['default' => null]);
$seq = $sanitizedQuery->getInt('seq', ['default' => 1]);
// Load our region
try {
$region = $this->regionFactory->getById($id);
$region->load();
// What type of region are we?
$additionalContexts = [];
if ($region->type === 'canvas' || $region->type === 'playlist') {
$this->getLog()->debug('preview: canvas or playlist region');
// Get the first playlist we can find
$playlist = $region->getPlaylist()->setModuleFactory($this->moduleFactory);
// Expand this Playlist out to its individual Widgets
$widgets = $playlist->expandWidgets();
$countWidgets = count($widgets);
// Select the widget at the required sequence
$widget = $playlist->getWidgetAt($seq, $widgets);
$widget->load();
} else {
$this->getLog()->debug('preview: single widget');
// Assume we're a frame, single Widget Requested
$widget = $this->widgetFactory->getById($widgetId);
$widget->load();
if ($widget->type === 'subplaylist') {
// Get the sub-playlist widgets
$event = new SubPlaylistWidgetsEvent($widget, $widget->tempId);
$this->getDispatcher()->dispatch($event, SubPlaylistWidgetsEvent::$NAME);
$additionalContexts['countSubPlaylistWidgets'] = count($event->getWidgets());
}
$countWidgets = 1;
}
$this->getLog()->debug('There are ' . $countWidgets . ' widgets.');
// Output a preview
$module = $this->moduleFactory->getByType($widget->type);
$this->getState()->html = $this->moduleFactory
->createWidgetHtmlRenderer()
->preview(
$module,
$region,
$widget,
$sanitizedQuery,
$this->urlFor(
$request,
'library.download',
[
'regionId' => $region->regionId,
'id' => $widget->getPrimaryMedia()[0] ?? null
]
) . '?preview=1',
$additionalContexts
);
$this->getState()->extra['countOfWidgets'] = $countWidgets;
$this->getState()->extra['empty'] = false;
} catch (NotFoundException) {
$this->getState()->extra['empty'] = true;
$this->getState()->extra['text'] = __('Empty Playlist');
} catch (InvalidArgumentException $e) {
$this->getState()->extra['empty'] = true;
$this->getState()->extra['text'] = __('Please correct the error with this Widget');
}
return $this->render($request, $response);
}
/**
* @return array
*/
private function transitionData()
{
return [
'in' => $this->transitionFactory->getEnabledByType('in'),
'out' => $this->transitionFactory->getEnabledByType('out'),
'compassPoints' => array(
array('id' => 'N', 'name' => __('North')),
array('id' => 'NE', 'name' => __('North East')),
array('id' => 'E', 'name' => __('East')),
array('id' => 'SE', 'name' => __('South East')),
array('id' => 'S', 'name' => __('South')),
array('id' => 'SW', 'name' => __('South West')),
array('id' => 'W', 'name' => __('West')),
array('id' => 'NW', 'name' => __('North West'))
)
];
}
/**
* Add a drawer
* @SWG\Post(
* path="/region/drawer/{id}",
* operationId="regionDrawerAdd",
* tags={"layout"},
* summary="Add drawer Region",
* description="Add a drawer Region to a Layout",
* @SWG\Parameter(
* name="id",
* in="path",
* description="The Layout ID to add the Region to",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=201,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Region"),
* @SWG\Header(
* header="Location",
* description="Location of the new record",
* type="string"
* )
* )
* )
*
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws ControllerNotImplemented
*/
public function addDrawer(Request $request, Response $response, $id) :Response
{
$layout = $this->layoutFactory->getById($id);
if (!$this->getUser()->checkEditable($layout)) {
throw new AccessDeniedException();
}
if (!$layout->isChild()) {
throw new InvalidArgumentException(__('This Layout is not a Draft, please checkout.'), 'layoutId');
}
$layout->load([
'loadPlaylists' => true,
'loadTags' => false,
'loadPermissions' => true,
'loadCampaigns' => false
]);
// Add a new region
// we default to layout width/height/0/0
$drawer = $this->regionFactory->create(
'drawer',
$this->getUser()->userId,
$layout->layout . '-' . (count($layout->regions) + 1 . ' - drawer'),
$layout->width,
$layout->height,
0,
0,
0,
1
);
$layout->drawers[] = $drawer;
$layout->save([
'saveTags' => false
]);
// Return
$this->getState()->hydrate([
'httpStatus' => 201,
'message' => sprintf(__('Added drawer %s'), $drawer->name),
'id' => $drawer->regionId,
'data' => $drawer
]);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws ControllerNotImplemented
*
* @SWG\Put(
* path="/region/drawer/{id}",
* operationId="regionDrawerSave",
* tags={"layout"},
* summary="Save Drawer",
* description="Save Drawer",
* @SWG\Parameter(
* name="id",
* in="path",
* description="The Drawer ID to Save",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="width",
* in="formData",
* description="The Width, default 250",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="height",
* in="formData",
* description="The Height",
* type="integer",
* required=false
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Region")
* )
* )
*/
public function saveDrawer(Request $request, Response $response, $id)
{
$region = $this->regionFactory->getById($id);
$sanitizedParams = $this->getSanitizer($request->getParams());
if (!$this->getUser()->checkEditable($region)) {
throw new AccessDeniedException();
}
// Check that this Regions Layout is in an editable state
$layout = $this->layoutFactory->getById($region->layoutId);
if (!$layout->isChild()) {
throw new InvalidArgumentException(__('This Layout is not a Draft, please checkout.'), 'layoutId');
}
// Save
$region->load();
$region->width = $sanitizedParams->getDouble('width', ['default' => $layout->width]);
$region->height = $sanitizedParams->getDouble('height', ['default' => $layout->height]);
$region->save();
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Edited Drawer %s'), $region->name),
'id' => $region->regionId,
'data' => $region
]);
return $this->render($request, $response);
}
}

122
lib/Controller/Report.php Normal file
View File

@@ -0,0 +1,122 @@
<?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\Controller;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Entity\ReportResult;
use Xibo\Service\ReportServiceInterface;
use Xibo\Support\Exception\GeneralException;
/**
* Class Report
* @package Xibo\Controller
*/
class Report extends Base
{
/**
* @var ReportServiceInterface
*/
private $reportService;
/**
* Set common dependencies.
* @param ReportServiceInterface $reportService
*/
public function __construct($reportService)
{
$this->reportService = $reportService;
}
/// //<editor-fold desc="Ad hoc reports">
/**
* Displays an Ad Hoc Report form
* @param Request $request
* @param Response $response
* @param $name
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function getReportForm(Request $request, Response $response, $name)
{
$this->getLog()->debug('Get report name: '. $name);
// Get the report Class from the Json File
$className = $this->reportService->getReportClass($name);
// Create the report object
$object = $this->reportService->createReportObject($className);
// We assert the user so that we can use getUser in the report class
$object->setUser($this->getUser());
// Get the twig file template and required data of the report form
$form = $object->getReportForm();
// Show the twig
$this->getState()->template = $form->template;
$this->getState()->setData([
'reportName' => $form->reportName,
'reportCategory' => $form->reportCategory,
'reportAddBtnTitle' => $form->reportAddBtnTitle,
'availableReports' => $this->reportService->listReports(),
'defaults' => $form->defaults
]);
return $this->render($request, $response);
}
/**
* Displays Ad Hoc/ On demand Report data in charts
* @param Request $request
* @param Response $response
* @param $name
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function getReportData(Request $request, Response $response, $name)
{
$this->getLog()->debug('Get report name: '. $name);
// Get the report Class from the Json File
$className = $this->reportService->getReportClass($name);
// Create the report object
$object = $this->reportService->createReportObject($className)->setUser($this->getUser());
$sanitizedParams = $this->getSanitizer($request->getParams());
// Return data to build chart/table
$result = $object->getResults($sanitizedParams);
//
// Output Results
// --------------
return $response->withJson($result);
}
//</editor-fold>
}

View File

@@ -0,0 +1,448 @@
<?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\Controller;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Factory\ResolutionFactory;
use Xibo\Support\Exception\AccessDeniedException;
/**
* Class Resolution
* @package Xibo\Controller
*/
class Resolution extends Base
{
/**
* @var ResolutionFactory
*/
private $resolutionFactory;
/**
* Set common dependencies.
* @param ResolutionFactory $resolutionFactory
*/
public function __construct($resolutionFactory)
{
$this->resolutionFactory = $resolutionFactory;
}
/**
* Display the Resolution Page
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
function displayPage(Request $request, Response $response)
{
$this->getState()->template = 'resolution-page';
return $this->render($request, $response);
}
/**
* Resolution Grid
*
* @SWG\Get(
* path="/resolution",
* operationId="resolutionSearch",
* tags={"resolution"},
* summary="Resolution Search",
* description="Search Resolutions this user has access to",
* @SWG\Parameter(
* name="resolutionId",
* in="query",
* description="Filter by Resolution Id",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="resolution",
* in="query",
* description="Filter by Resolution Name",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="partialResolution",
* in="query",
* description="Filter by Partial Resolution Name",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="enabled",
* in="query",
* description="Filter by Enabled",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="width",
* in="query",
* description="Filter by Resolution width",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="height",
* in="query",
* description="Filter by Resolution height",
* type="integer",
* required=false
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="#/definitions/Resolution")
* )
* )
* )
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
function grid(Request $request, Response $response)
{
$sanitizedQueryParams = $this->getSanitizer($request->getQueryParams());
// Show enabled
$filter = [
'enabled' => $sanitizedQueryParams->getInt('enabled', ['default' => -1]),
'resolutionId' => $sanitizedQueryParams->getInt('resolutionId'),
'resolution' => $sanitizedQueryParams->getString('resolution'),
'partialResolution' => $sanitizedQueryParams->getString('partialResolution'),
'width' => $sanitizedQueryParams->getInt('width'),
'height' => $sanitizedQueryParams->getInt('height'),
'orientation' => $sanitizedQueryParams->getString('orientation')
];
$resolutions = $this->resolutionFactory->query($this->gridRenderSort($sanitizedQueryParams), $this->gridRenderFilter($filter, $sanitizedQueryParams));
foreach ($resolutions as $resolution) {
/* @var \Xibo\Entity\Resolution $resolution */
if ($this->isApi($request))
break;
$resolution->includeProperty('buttons');
if ($this->getUser()->featureEnabled('resolution.modify')
&& $this->getUser()->checkEditable($resolution)
) {
// Edit Button
$resolution->buttons[] = array(
'id' => 'resolution_button_edit',
'url' => $this->urlFor($request,'resolution.edit.form', ['id' => $resolution->resolutionId]),
'text' => __('Edit')
);
}
if ($this->getUser()->featureEnabled('resolution.modify')
&& $this->getUser()->checkDeleteable($resolution)
) {
// Delete Button
$resolution->buttons[] = array(
'id' => 'resolution_button_delete',
'url' => $this->urlFor($request,'resolution.delete.form', ['id' => $resolution->resolutionId]),
'text' => __('Delete')
);
}
}
$this->getState()->template = 'grid';
$this->getState()->setData($resolutions);
$this->getState()->recordsTotal = $this->resolutionFactory->countLast();
return $this->render($request, $response);
}
/**
* Resolution Add
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
function addForm(Request $request, Response $response)
{
$this->getState()->template = 'resolution-form-add';
return $this->render($request, $response);
}
/**
* Resolution Edit Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
function editForm(Request $request, Response $response, $id)
{
$resolution = $this->resolutionFactory->getById($id);
if (!$this->getUser()->checkEditable($resolution)) {
throw new AccessDeniedException();
}
$this->getState()->template = 'resolution-form-edit';
$this->getState()->setData([
'resolution' => $resolution,
]);
return $this->render($request, $response);
}
/**
* Resolution Delete Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
function deleteForm(Request $request, Response $response, $id)
{
$resolution = $this->resolutionFactory->getById($id);
if (!$this->getUser()->checkEditable($resolution)) {
throw new AccessDeniedException();
}
$this->getState()->template = 'resolution-form-delete';
$this->getState()->setData([
'resolution' => $resolution,
]);
return $this->render($request, $response);
}
/**
* Add Resolution
*
* @SWG\Post(
* path="/resolution",
* operationId="resolutionAdd",
* tags={"resolution"},
* summary="Add Resolution",
* description="Add new Resolution",
* @SWG\Parameter(
* name="resolution",
* in="formData",
* description="A name for the Resolution",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="width",
* in="formData",
* description="The Display Width of the Resolution",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="height",
* in="formData",
* description="The Display Height of the Resolution",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=201,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Resolution"),
* @SWG\Header(
* header="Location",
* description="Location of the new record",
* type="string"
* )
* )
* )
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\InvalidArgumentException
*/
function add(Request $request, Response $response)
{
$sanitizedParams = $this->getSanitizer($request->getParams());
/* @var \Xibo\Entity\Resolution $resolution */
$resolution = $this->resolutionFactory->create($sanitizedParams->getString('resolution'),
$sanitizedParams->getInt('width'),
$sanitizedParams->getInt('height'));
$resolution->userId = $this->getUser()->userId;
$resolution->save();
// Return
$this->getState()->hydrate([
'httpStatus' => 201,
'message' => sprintf(__('Added %s'), $resolution->resolution),
'id' => $resolution->resolutionId,
'data' => $resolution
]);
return $this->render($request, $response);
}
/**
* Edit Resolution
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\InvalidArgumentException
* @throws \Xibo\Support\Exception\NotFoundException
* @SWG\Put(
* path="/resolution/{resolutionId}",
* operationId="resolutionEdit",
* tags={"resolution"},
* summary="Edit Resolution",
* description="Edit new Resolution",
* @SWG\Parameter(
* name="resolutionId",
* in="path",
* description="The Resolution ID to Edit",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="resolution",
* in="formData",
* description="A name for the Resolution",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="width",
* in="formData",
* description="The Display Width of the Resolution",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="height",
* in="formData",
* description="The Display Height of the Resolution",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Resolution")
* )
* )
*/
function edit(Request $request, Response $response, $id)
{
$resolution = $this->resolutionFactory->getById($id);
if (!$this->getUser()->checkEditable($resolution)) {
throw new AccessDeniedException();
}
$sanitizedParams = $this->getSanitizer($request->getParams());
$resolution->resolution = $sanitizedParams->getString('resolution');
$resolution->width = $sanitizedParams->getInt('width');
$resolution->height = $sanitizedParams->getInt('height');
$resolution->enabled = $sanitizedParams->getCheckbox('enabled');
$resolution->save();
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Edited %s'), $resolution->resolution),
'id' => $resolution->resolutionId,
'data' => $resolution
]);
return $this->render($request, $response);
}
/**
* Delete Resolution
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
* @SWG\Delete(
* path="/resolution/{resolutionId}",
* operationId="resolutionDelete",
* tags={"resolution"},
* summary="Delete Resolution",
* description="Delete Resolution",
* @SWG\Parameter(
* name="resolutionId",
* in="path",
* description="The Resolution ID to Delete",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*/
function delete(Request $request, Response $response, $id)
{
$resolution = $this->resolutionFactory->getById($id);
if (!$this->getUser()->checkDeleteable($resolution)) {
throw new AccessDeniedException();
}
$resolution->delete();
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Deleted %s'), $resolution->resolution),
'httpStatus' => 204,
]);
return $this->render($request, $response);
}
}

View File

@@ -0,0 +1,476 @@
<?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\Controller;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Entity\ReportResult;
use Xibo\Factory\MediaFactory;
use Xibo\Factory\ReportScheduleFactory;
use Xibo\Factory\SavedReportFactory;
use Xibo\Factory\UserFactory;
use Xibo\Helper\SendFile;
use Xibo\Service\ReportServiceInterface;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\NotFoundException;
/**
* Class SavedReport
* @package Xibo\Controller
*/
class SavedReport extends Base
{
/**
* @var ReportServiceInterface
*/
private $reportService;
/**
* @var ReportScheduleFactory
*/
private $reportScheduleFactory;
/**
* @var SavedReportFactory
*/
private $savedReportFactory;
/**
* @var MediaFactory
*/
private $mediaFactory;
/**
* @var UserFactory
*/
private $userFactory;
/**
* Set common dependencies.
* @param ReportServiceInterface $reportService
* @param ReportScheduleFactory $reportScheduleFactory
* @param SavedReportFactory $savedReportFactory
* @param MediaFactory $mediaFactory
* @param UserFactory $userFactory
*/
public function __construct($reportService, $reportScheduleFactory, $savedReportFactory, $mediaFactory, $userFactory)
{
$this->reportService = $reportService;
$this->reportScheduleFactory = $reportScheduleFactory;
$this->savedReportFactory = $savedReportFactory;
$this->mediaFactory = $mediaFactory;
$this->userFactory = $userFactory;
}
//<editor-fold desc="Saved report">
/**
* Saved report Grid
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function savedReportGrid(Request $request, Response $response)
{
$sanitizedQueryParams = $this->getSanitizer($request->getQueryParams());
$savedReports = $this->savedReportFactory->query($this->gridRenderSort($sanitizedQueryParams), $this->gridRenderFilter([
'saveAs' => $sanitizedQueryParams->getString('saveAs'),
'useRegexForName' => $sanitizedQueryParams->getCheckbox('useRegexForName'),
'userId' => $sanitizedQueryParams->getInt('userId'),
'reportName' => $sanitizedQueryParams->getString('reportName'),
'onlyMyReport' => $sanitizedQueryParams->getCheckbox('onlyMyReport'),
'logicalOperatorName' => $sanitizedQueryParams->getString('logicalOperatorName'),
], $sanitizedQueryParams));
foreach ($savedReports as $savedReport) {
if ($this->isApi($request)) {
continue;
}
$savedReport->includeProperty('buttons');
// If a report class does not comply (i.e., no category or route) we get an error when trying to get the email template
// Dont show any button if the report is not compatible
// This will also check whether the report feature is enabled or not.
$compatible = true;
try {
// Get report email template
$emailTemplate = $this->reportService->getReportEmailTemplate($savedReport->reportName);
} catch (NotFoundException $exception) {
$compatible = false;
}
if ($compatible) {
// Show only convert button for schema version 1
if ($savedReport->schemaVersion == 1) {
$savedReport->buttons[] = [
'id' => 'button_convert_report',
'url' => $this->urlFor($request, 'savedreport.convert.form', ['id' => $savedReport->savedReportId]),
'text' => __('Convert')
];
} else {
$savedReport->buttons[] = [
'id' => 'button_show_report.now',
'class' => 'XiboRedirectButton',
'url' => $this->urlFor($request, 'savedreport.open', ['id' => $savedReport->savedReportId, 'name' => $savedReport->reportName]),
'text' => __('Open')
];
$savedReport->buttons[] = ['divider' => true];
$savedReport->buttons[] = [
'id' => 'button_goto_report',
'class' => 'XiboRedirectButton',
'url' => $this->urlFor($request, 'report.form', ['name' => $savedReport->reportName]),
'text' => __('Back to Reports')
];
$savedReport->buttons[] = [
'id' => 'button_goto_schedule',
'class' => 'XiboRedirectButton',
'url' => $this->urlFor($request, 'reportschedule.view') . '?reportScheduleId=' . $savedReport->reportScheduleId. '&reportName='.$savedReport->reportName,
'text' => __('Go to schedule')
];
$savedReport->buttons[] = ['divider' => true];
if (!empty($emailTemplate)) {
// Export Button
$savedReport->buttons[] = [
'id' => 'button_export_report',
'linkType' => '_self', 'external' => true,
'url' => $this->urlFor($request, 'savedreport.export', ['id' => $savedReport->savedReportId, 'name' => $savedReport->reportName]),
'text' => __('Export as PDF')
];
}
// Delete
if ($this->getUser()->checkDeleteable($savedReport)) {
// Show the delete button
$savedReport->buttons[] = array(
'id' => 'savedreport_button_delete',
'url' => $this->urlFor($request, 'savedreport.delete.form', ['id' => $savedReport->savedReportId]),
'text' => __('Delete'),
'multi-select' => true,
'dataAttributes' => array(
array('name' => 'commit-url', 'value' => $this->urlFor($request, 'savedreport.delete', ['id' => $savedReport->savedReportId])),
array('name' => 'commit-method', 'value' => 'delete'),
array('name' => 'id', 'value' => 'savedreport_button_delete'),
array('name' => 'text', 'value' => __('Delete')),
array('name' => 'sort-group', 'value' => 1),
array('name' => 'rowtitle', 'value' => $savedReport->saveAs),
)
);
}
}
}
}
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = $this->savedReportFactory->countLast();
$this->getState()->setData($savedReports);
return $this->render($request, $response);
}
/**
* Displays the Saved Report Page
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function displaySavedReportPage(Request $request, Response $response)
{
$reportsList = $this->reportService->listReports();
$availableReports = [];
foreach ($reportsList as $reports) {
foreach ($reports as $report) {
$availableReports[] = $report;
}
}
// Call to render the template
$this->getState()->template = 'saved-report-page';
$this->getState()->setData([
'availableReports' => $availableReports
]);
return $this->render($request, $response);
}
/**
* Report Schedule Delete Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function deleteSavedReportForm(Request $request, Response $response, $id)
{
$savedReport = $this->savedReportFactory->getById($id);
if (!$this->getUser()->checkDeleteable($savedReport)) {
throw new AccessDeniedException(__('You do not have permissions to delete this report schedule'));
}
$data = [
'savedReport' => $savedReport
];
$this->getState()->template = 'savedreport-form-delete';
$this->getState()->setData($data);
return $this->render($request, $response);
}
/**
* Saved Report Delete
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function savedReportDelete(Request $request, Response $response, $id)
{
$savedReport = $this->savedReportFactory->getById($id);
if (!$this->getUser()->checkDeleteable($savedReport)) {
throw new AccessDeniedException(__('You do not have permissions to delete this report schedule'));
}
$savedReport->load();
// Delete
$savedReport->delete();
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Deleted %s'), $savedReport->saveAs)
]);
return $this->render($request, $response);
}
/**
* Returns a Saved Report's preview
* @param Request $request
* @param Response $response
* @param $id
* @param $name
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function savedReportOpen(Request $request, Response $response, $id, $name)
{
// Retrieve the saved report result in array
/* @var ReportResult $results */
$results = $this->reportService->getSavedReportResults($id, $name);
// Set Template
$this->getState()->template = $this->reportService->getSavedReportTemplate($name);
$this->getState()->setData($results->jsonSerialize());
return $this->render($request, $response);
}
/**
* Exports saved report as a PDF file
* @param Request $request
* @param Response $response
* @param $id
* @param $name
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws NotFoundException
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function savedReportExport(Request $request, Response $response, $id, $name)
{
$savedReport = $this->savedReportFactory->getById($id);
// Retrieve the saved report result in array
/* @var ReportResult $results */
$results = $this->reportService->getSavedReportResults($id, $name);
// Get the report config
$report = $this->reportService->getReportByName($name);
if ($report->output_type == 'both' || $report->output_type == 'chart') {
$quickChartUrl = $this->getConfig()->getSetting('QUICK_CHART_URL');
if (!empty($quickChartUrl)) {
$quickChartUrl .= '/chart?width=1000&height=300&c=';
$script = $this->reportService->getReportChartScript($id, $name);
// Replace " with ' for the quick chart URL
$src = $quickChartUrl . str_replace('"', '\'', $script);
// If multiple charts needs to be displayed
$multipleCharts = [];
$chartScriptArray = json_decode($script, true);
foreach ($chartScriptArray as $key => $chartData) {
$multipleCharts[$key] = $quickChartUrl . str_replace('"', '\'', json_encode($chartData));
}
} else {
$placeholder = __('Chart could not be drawn because the CMS has not been configured with a Quick Chart URL.');
}
}
if ($report->output_type == 'both' || $report->output_type == 'table') { // only for tablebased report
$tableData = $results->table;
}
// Get report email template to export
$emailTemplate = $this->reportService->getReportEmailTemplate($name);
if (!empty($emailTemplate)) {
// Save PDF attachment
$showLogo = $this->getConfig()->getSetting('REPORTS_EXPORT_SHOW_LOGO', 1) == 1;
$body = $this->getView()->fetch(
$emailTemplate,
[
'header' => $report->description,
'logo' => ($showLogo) ? $this->getConfig()->uri('img/xibologo.png', true) : null,
'title' => $savedReport->saveAs,
'metadata' => $results->metadata,
'tableData' => $tableData ?? null,
'src' => $src ?? null,
'multipleCharts' => $multipleCharts ?? null,
'placeholder' => $placeholder ?? null
]
);
$fileName = $this->getConfig()->getSetting('LIBRARY_LOCATION') . 'temp/saved_report_' . $id . '.pdf';
try {
$mpdf = new \Mpdf\Mpdf([
'tempDir' => $this->getConfig()->getSetting('LIBRARY_LOCATION') . '/temp',
'orientation' => 'L',
'mode' => 'c',
'margin_left' => 20,
'margin_right' => 20,
'margin_top' => 20,
'margin_bottom' => 20,
'margin_header' => 5,
'margin_footer' => 15
]);
$mpdf->setFooter('Page {PAGENO}') ;
$mpdf->SetDisplayMode('fullpage');
$stylesheet = file_get_contents($this->getConfig()->uri('css/email-report.css', true));
$mpdf->WriteHTML($stylesheet, 1);
$mpdf->WriteHTML($body);
$mpdf->Output($fileName, \Mpdf\Output\Destination::FILE);
} catch (\Exception $error) {
$this->getLog()->error($error->getMessage());
}
}
// Return the file with PHP
$this->setNoOutput(true);
return $this->render($request, SendFile::decorateResponse(
$response,
$this->getConfig()->getSetting('SENDFILE_MODE'),
$fileName
));
}
/**
* Saved Report Convert Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function convertSavedReportForm(Request $request, Response $response, $id)
{
$savedReport = $this->savedReportFactory->getById($id);
$data = [
'savedReport' => $savedReport
];
$this->getState()->template = 'savedreport-form-convert';
$this->getState()->setData($data);
return $this->render($request, $response);
}
/**
* Converts a Saved Report from Schema Version 1 to 2
* @param Request $request
* @param Response $response
* @param $id
* @param $name
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function savedReportConvert(Request $request, Response $response, $id, $name)
{
$savedReport = $this->savedReportFactory->getById($id);
if ($savedReport->schemaVersion == 2) {
throw new GeneralException(__('This report has already been converted to the latest version.'));
}
// Convert Result to schemaVersion 2
$this->reportService->convertSavedReportResults($id, $name);
$savedReport->schemaVersion = 2;
$savedReport->save();
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Saved Report Converted to Schema Version 2'))
]);
return $this->render($request, $response);
}
//</editor-fold>
}

2787
lib/Controller/Schedule.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,763 @@
<?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\Controller;
use Carbon\Carbon;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Slim\Views\Twig;
use Xibo\Entity\ReportSchedule;
use Xibo\Factory\MediaFactory;
use Xibo\Factory\ReportScheduleFactory;
use Xibo\Factory\SavedReportFactory;
use Xibo\Factory\UserFactory;
use Xibo\Helper\DateFormatHelper;
use Xibo\Helper\SanitizerService;
use Xibo\Service\ConfigServiceInterface;
use Xibo\Service\LogServiceInterface;
use Xibo\Service\ReportServiceInterface;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
/**
* Class Report
* @package Xibo\Controller
*/
class ScheduleReport extends Base
{
/**
* @var ReportServiceInterface
*/
private $reportService;
/**
* @var ReportScheduleFactory
*/
private $reportScheduleFactory;
/**
* @var SavedReportFactory
*/
private $savedReportFactory;
/**
* @var MediaFactory
*/
private $mediaFactory;
/**
* @var UserFactory
*/
private $userFactory;
/**
* @var Twig
*/
private $view;
/**
* Set common dependencies.
* @param LogServiceInterface $log
* @param SanitizerService $sanitizerService
* @param \Xibo\Helper\ApplicationState $state
* @param \Xibo\Entity\User $user
* @param \Xibo\Service\HelpServiceInterface $help
* @param ConfigServiceInterface $config
* @param Twig $view
* @param ReportServiceInterface $reportService
* @param ReportScheduleFactory $reportScheduleFactory
* @param SavedReportFactory $savedReportFactory
* @param MediaFactory $mediaFactory
* @param UserFactory $userFactory
*/
public function __construct($reportService, $reportScheduleFactory, $savedReportFactory, $mediaFactory, $userFactory)
{
$this->reportService = $reportService;
$this->reportScheduleFactory = $reportScheduleFactory;
$this->savedReportFactory = $savedReportFactory;
$this->mediaFactory = $mediaFactory;
$this->userFactory = $userFactory;
}
/// //<editor-fold desc="Report Schedules">
/**
* Report Schedule Grid
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function reportScheduleGrid(Request $request, Response $response)
{
$sanitizedQueryParams = $this->getSanitizer($request->getQueryParams());
$reportSchedules = $this->reportScheduleFactory->query($this->gridRenderSort($sanitizedQueryParams), $this->gridRenderFilter([
'name' => $sanitizedQueryParams->getString('name'),
'useRegexForName' => $sanitizedQueryParams->getCheckbox('useRegexForName'),
'userId' => $sanitizedQueryParams->getInt('userId'),
'reportScheduleId' => $sanitizedQueryParams->getInt('reportScheduleId'),
'reportName' => $sanitizedQueryParams->getString('reportName'),
'onlyMySchedules' => $sanitizedQueryParams->getCheckbox('onlyMySchedules'),
'logicalOperatorName' => $sanitizedQueryParams->getString('logicalOperatorName'),
], $sanitizedQueryParams));
/** @var \Xibo\Entity\ReportSchedule $reportSchedule */
foreach ($reportSchedules as $reportSchedule) {
if ($this->isApi($request)) {
continue;
}
$reportSchedule->includeProperty('buttons');
$cron = \Cron\CronExpression::factory($reportSchedule->schedule);
if ($reportSchedule->lastRunDt == 0) {
$nextRunDt = Carbon::now()->format('U');
} else {
$nextRunDt = $cron->getNextRunDate(Carbon::createFromTimestamp($reportSchedule->lastRunDt))->format('U');
}
$reportSchedule->setUnmatchedProperty('nextRunDt', $nextRunDt);
// Ad hoc report name
$adhocReportName = $reportSchedule->reportName;
// We get the report description
try {
$reportSchedule->reportName = $this->reportService->getReportByName($reportSchedule->reportName)->description;
} catch (NotFoundException $notFoundException) {
$reportSchedule->reportName = __('Unknown or removed report.');
}
switch ($reportSchedule->schedule) {
case ReportSchedule::$SCHEDULE_DAILY:
$reportSchedule->schedule = __('Run once a day, midnight');
break;
case ReportSchedule::$SCHEDULE_WEEKLY:
$reportSchedule->schedule = __('Run once a week, midnight on Monday');
break;
case ReportSchedule::$SCHEDULE_MONTHLY:
$reportSchedule->schedule = __('Run once a month, midnight, first of month');
break;
case ReportSchedule::$SCHEDULE_YEARLY:
$reportSchedule->schedule = __('Run once a year, midnight, Jan. 1');
break;
}
switch ($reportSchedule->isActive) {
case 1:
$reportSchedule->setUnmatchedProperty('isActiveDescription', __('This report schedule is active'));
break;
default:
$reportSchedule->setUnmatchedProperty('isActiveDescription', __('This report schedule is paused'));
}
if ($reportSchedule->getLastSavedReportId() > 0) {
$lastSavedReport = $this->savedReportFactory->getById($reportSchedule->getLastSavedReportId());
// Hide this for schema version 1
if ($lastSavedReport->schemaVersion != 1) {
// Open Last Saved Report
$reportSchedule->buttons[] = [
'id' => 'reportSchedule_lastsaved_report_button',
'class' => 'XiboRedirectButton',
'url' => $this->urlFor($request, 'savedreport.open', ['id' => $lastSavedReport->savedReportId, 'name' => $lastSavedReport->reportName]),
'text' => __('Open last saved report')
];
}
}
// Back to Reports
$reportSchedule->buttons[] = [
'id' => 'reportSchedule_goto_report_button',
'class' => 'XiboRedirectButton',
'url' => $this->urlFor($request, 'report.form', ['name' => $adhocReportName]),
'text' => __('Back to Reports')
];
$reportSchedule->buttons[] = ['divider' => true];
// Edit
if ($this->getUser()->featureEnabled('report.scheduling')) {
$reportSchedule->buttons[] = [
'id' => 'reportSchedule_edit_button',
'url' => $this->urlFor($request, 'reportschedule.edit.form', ['id' => $reportSchedule->reportScheduleId]),
'text' => __('Edit')
];
}
// Reset to previous run
if ($this->getUser()->isSuperAdmin()) {
$reportSchedule->buttons[] = [
'id' => 'reportSchedule_reset_button',
'url' => $this->urlFor($request, 'reportschedule.reset.form', ['id' => $reportSchedule->reportScheduleId]),
'text' => __('Reset to previous run')
];
}
// Delete
if ($this->getUser()->featureEnabled('report.scheduling')
&& $this->getUser()->checkDeleteable($reportSchedule)) {
// Show the delete button
$reportSchedule->buttons[] = [
'id' => 'reportschedule_button_delete',
'url' => $this->urlFor($request, 'reportschedule.delete.form', ['id' => $reportSchedule->reportScheduleId]),
'text' => __('Delete'),
'multi-select' => true,
'dataAttributes' => [
['name' => 'commit-url', 'value' => $this->urlFor($request, 'reportschedule.delete', ['id' => $reportSchedule->reportScheduleId])],
['name' => 'commit-method', 'value' => 'delete'],
['name' => 'id', 'value' => 'reportschedule_button_delete'],
['name' => 'text', 'value' => __('Delete')],
['name' => 'sort-group', 'value' => 1],
['name' => 'rowtitle', 'value' => $reportSchedule->name]
]
];
}
// Toggle active
if ($this->getUser()->featureEnabled('report.scheduling')) {
$reportSchedule->buttons[] = [
'id' => 'reportSchedule_toggleactive_button',
'url' => $this->urlFor($request, 'reportschedule.toggleactive.form', ['id' => $reportSchedule->reportScheduleId]),
'text' => ($reportSchedule->isActive == 1) ? __('Pause') : __('Resume')
];
}
// Delete all saved report
$savedreports = $this->savedReportFactory->query(null, ['reportScheduleId'=> $reportSchedule->reportScheduleId]);
if ((count($savedreports) > 0)
&& $this->getUser()->checkDeleteable($reportSchedule)
&& $this->getUser()->featureEnabled('report.saving')
) {
$reportSchedule->buttons[] = ['divider' => true];
$reportSchedule->buttons[] = array(
'id' => 'reportschedule_button_delete_all',
'url' => $this->urlFor($request, 'reportschedule.deleteall.form', ['id' => $reportSchedule->reportScheduleId]),
'text' => __('Delete all saved reports'),
);
}
}
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = $this->reportScheduleFactory->countLast();
$this->getState()->setData($reportSchedules);
return $this->render($request, $response);
}
/**
* Report Schedule Reset
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function reportScheduleReset(Request $request, Response $response, $id)
{
$reportSchedule = $this->reportScheduleFactory->getById($id);
$this->getLog()->debug('Reset Report Schedule: '.$reportSchedule->name);
// Go back to previous run date
$reportSchedule->lastSavedReportId = 0;
$reportSchedule->lastRunDt = $reportSchedule->previousRunDt;
$reportSchedule->save();
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => 'Success'
]);
return $this->render($request, $response);
}
/**
* Report Schedule Add
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws InvalidArgumentException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function reportScheduleAdd(Request $request, Response $response)
{
$sanitizedParams = $this->getSanitizer($request->getParams());
$name = $sanitizedParams->getString('name');
$reportName = $request->getParam('reportName', null);
$fromDt = $sanitizedParams->getDate('fromDt', ['default' => 0]);
$toDt = $sanitizedParams->getDate('toDt', ['default' => 0]);
$today = Carbon::now()->startOfDay()->format('U');
// from and todt should be greater than today
if (!empty($fromDt)) {
$fromDt = $fromDt->format('U');
if ($fromDt < $today) {
throw new InvalidArgumentException(__('Start time cannot be earlier than today'), 'fromDt');
}
}
if (!empty($toDt)) {
$toDt = $toDt->format('U');
if ($toDt < $today) {
throw new InvalidArgumentException(__('End time cannot be earlier than today'), 'toDt');
}
}
$this->getLog()->debug('Add Report Schedule: '. $name);
// Set Report Schedule form data
$result = $this->reportService->setReportScheduleFormData($reportName, $request);
$reportSchedule = $this->reportScheduleFactory->createEmpty();
$reportSchedule->name = $name;
$reportSchedule->lastSavedReportId = 0;
$reportSchedule->reportName = $reportName;
$reportSchedule->filterCriteria = $result['filterCriteria'];
$reportSchedule->schedule = $result['schedule'];
$reportSchedule->lastRunDt = 0;
$reportSchedule->previousRunDt = 0;
$reportSchedule->fromDt = $fromDt;
$reportSchedule->toDt = $toDt;
$reportSchedule->userId = $this->getUser()->userId;
$reportSchedule->createdDt = Carbon::now()->format('U');
$reportSchedule->save();
// Return
$this->getState()->hydrate([
'httpStatus' => 201,
'message' => __('Added Report Schedule'),
'id' => $reportSchedule->reportScheduleId,
'data' => $reportSchedule
]);
return $this->render($request, $response);
}
/**
* Report Schedule Edit
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function reportScheduleEdit(Request $request, Response $response, $id)
{
$reportSchedule = $this->reportScheduleFactory->getById($id, 0);
if ($reportSchedule->getOwnerId() != $this->getUser()->userId && $this->getUser()->userTypeId != 1) {
throw new AccessDeniedException();
}
$sanitizedParams = $this->getSanitizer($request->getParams());
$name = $sanitizedParams->getString('name');
$reportName = $request->getParam('reportName', null);
$fromDt = $sanitizedParams->getDate('fromDt', ['default' => 0]);
$toDt = $sanitizedParams->getDate('toDt', ['default' => 0]);
$today = Carbon::now()->startOfDay()->format('U');
// from and todt should be greater than today
if (!empty($fromDt)) {
$fromDt = $fromDt->format('U');
if ($fromDt < $today) {
throw new InvalidArgumentException(__('Start time cannot be earlier than today'), 'fromDt');
}
}
if (!empty($toDt)) {
$toDt = $toDt->format('U');
if ($toDt < $today) {
throw new InvalidArgumentException(__('End time cannot be earlier than today'), 'toDt');
}
}
$reportSchedule->name = $name;
$reportSchedule->fromDt = $fromDt;
$reportSchedule->toDt = $toDt;
$reportSchedule->save();
// Return
$this->getState()->hydrate([
'httpStatus' => 200,
'message' => sprintf(__('Edited %s'), $reportSchedule->name),
'id' => $reportSchedule->reportScheduleId,
'data' => $reportSchedule
]);
return $this->render($request, $response);
}
/**
* Report Schedule Delete
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function reportScheduleDelete(Request $request, Response $response, $id)
{
$reportSchedule = $this->reportScheduleFactory->getById($id);
if (!$this->getUser()->checkDeleteable($reportSchedule)) {
throw new AccessDeniedException(__('You do not have permissions to delete this report schedule'));
}
try {
$reportSchedule->delete();
} catch (\RuntimeException $e) {
throw new InvalidArgumentException(__('Report schedule cannot be deleted. Please ensure there are no saved reports against the schedule.'), 'reportScheduleId');
}
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Deleted %s'), $reportSchedule->name)
]);
return $this->render($request, $response);
}
/**
* Report Schedule Delete All Saved Report
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function reportScheduleDeleteAllSavedReport(Request $request, Response $response, $id)
{
$sanitizedParams = $this->getSanitizer($request->getParams());
$reportSchedule = $this->reportScheduleFactory->getById($id);
if (!$this->getUser()->checkDeleteable($reportSchedule)) {
throw new AccessDeniedException(__('You do not have permissions to delete the saved report of this report schedule'));
}
// Get all saved reports of the report schedule
$savedReports = $this->savedReportFactory->query(
null,
[
'reportScheduleId' => $reportSchedule->reportScheduleId
]
);
foreach ($savedReports as $savedreport) {
try {
$savedreport->load();
// Delete
$savedreport->delete();
} catch (\RuntimeException $e) {
throw new InvalidArgumentException(__('Saved report cannot be deleted'), 'savedReportId');
}
}
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Deleted all saved reports of %s'), $reportSchedule->name)
]);
return $this->render($request, $response);
}
/**
* Report Schedule Toggle Active
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function reportScheduleToggleActive(Request $request, Response $response, $id)
{
$reportSchedule = $this->reportScheduleFactory->getById($id);
if (!$this->getUser()->checkEditable($reportSchedule)) {
throw new AccessDeniedException(__('You do not have permissions to pause/resume this report schedule'));
}
if ($reportSchedule->isActive == 1) {
$reportSchedule->isActive = 0;
$msg = sprintf(__('Paused %s'), $reportSchedule->name);
} else {
$reportSchedule->isActive = 1;
$msg = sprintf(__('Resumed %s'), $reportSchedule->name);
}
$reportSchedule->save();
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => $msg
]);
return $this->render($request, $response);
}
/**
* Displays the Report Schedule Page
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function displayReportSchedulePage(Request $request, Response $response)
{
$reportsList = $this->reportService->listReports();
$availableReports = [];
foreach ($reportsList as $reports) {
foreach ($reports as $report) {
$availableReports[] = $report;
}
}
// Call to render the template
$this->getState()->template = 'report-schedule-page';
$this->getState()->setData([
'availableReports' => $availableReports
]);
return $this->render($request, $response);
}
/**
* Displays an Add form
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function addReportScheduleForm(Request $request, Response $response)
{
$reportName = $request->getParam('reportName', null);
// Populate form title and hidden fields
$formData = $this->reportService->getReportScheduleFormData($reportName, $request);
$template = $formData['template'];
$this->getState()->template = $template;
$this->getState()->setData($formData['data']);
return $this->render($request, $response);
}
/**
* Report Schedule Edit Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function editReportScheduleForm(Request $request, Response $response, $id)
{
$reportSchedule = $this->reportScheduleFactory->getById($id, 0);
if ($reportSchedule->fromDt > 0) {
$reportSchedule->fromDt = Carbon::createFromTimestamp($reportSchedule->fromDt)->format(DateFormatHelper::getSystemFormat());
} else {
$reportSchedule->fromDt = '';
}
if ($reportSchedule->toDt > 0) {
$reportSchedule->toDt = Carbon::createFromTimestamp($reportSchedule->toDt)->format(DateFormatHelper::getSystemFormat());
} else {
$reportSchedule->toDt = '';
}
$this->getState()->template = 'reportschedule-form-edit';
$this->getState()->setData([
'reportSchedule' => $reportSchedule
]);
return $this->render($request, $response);
}
/**
* Report Schedule Reset Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function resetReportScheduleForm(Request $request, Response $response, $id)
{
$reportSchedule = $this->reportScheduleFactory->getById($id, 0);
// Only admin can reset it
if ($this->getUser()->userTypeId != 1) {
throw new AccessDeniedException(__('You do not have permissions to reset this report schedule'));
}
$data = [
'reportSchedule' => $reportSchedule
];
$this->getState()->template = 'reportschedule-form-reset';
$this->getState()->setData($data);
return $this->render($request, $response);
}
/**
* Report Schedule Delete Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function deleteReportScheduleForm(Request $request, Response $response, $id)
{
$reportSchedule = $this->reportScheduleFactory->getById($id, 0);
if (!$this->getUser()->checkDeleteable($reportSchedule)) {
throw new AccessDeniedException(__('You do not have permissions to delete this report schedule'));
}
$data = [
'reportSchedule' => $reportSchedule
];
$this->getState()->template = 'reportschedule-form-delete';
$this->getState()->setData($data);
return $this->render($request, $response);
}
/**
* Report Schedule Delete All Saved Report Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function deleteAllSavedReportReportScheduleForm(Request $request, Response $response, $id)
{
$reportSchedule = $this->reportScheduleFactory->getById($id, 0);
if (!$this->getUser()->checkDeleteable($reportSchedule)) {
throw new AccessDeniedException(__('You do not have permissions to delete saved reports of this report schedule'));
}
$data = [
'reportSchedule' => $reportSchedule
];
$this->getState()->template = 'reportschedule-form-deleteall';
$this->getState()->setData($data);
return $this->render($request, $response);
}
/**
* Report Schedule Toggle Active Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function toggleActiveReportScheduleForm(Request $request, Response $response, $id)
{
$reportSchedule = $this->reportScheduleFactory->getById($id, 0);
if (!$this->getUser()->checkEditable($reportSchedule)) {
throw new AccessDeniedException(__('You do not have permissions to pause/resume this report schedule'));
}
$data = [
'reportSchedule' => $reportSchedule
];
$this->getState()->template = 'reportschedule-form-toggleactive';
$this->getState()->setData($data);
return $this->render($request, $response);
}
//</editor-fold>
}

182
lib/Controller/Sessions.php Normal file
View File

@@ -0,0 +1,182 @@
<?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\Controller;
use Carbon\Carbon;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Factory\SessionFactory;
use Xibo\Helper\DateFormatHelper;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\AccessDeniedException;
/**
* Class Sessions
* @package Xibo\Controller
*/
class Sessions extends Base
{
/**
* @var StorageServiceInterface
*/
private $store;
/**
* @var SessionFactory
*/
private $sessionFactory;
/**
* Set common dependencies.
* @param StorageServiceInterface $store
* @param SessionFactory $sessionFactory
*/
public function __construct($store, $sessionFactory)
{
$this->store = $store;
$this->sessionFactory = $sessionFactory;
}
/**
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
function displayPage(Request $request, Response $response)
{
$this->getState()->template = 'sessions-page';
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function grid(Request $request, Response $response): Response|\Psr\Http\Message\ResponseInterface
{
$sanitizedQueryParams = $this->getSanitizer($request->getQueryParams());
$sessions = $this->sessionFactory->query($this->gridRenderSort($sanitizedQueryParams), $this->gridRenderFilter([
'type' => $sanitizedQueryParams->getString('type'),
'fromDt' => $sanitizedQueryParams->getString('fromDt')
], $sanitizedQueryParams));
foreach ($sessions as $row) {
/* @var \Xibo\Entity\Session $row */
// Normalise the date
$row->lastAccessed =
Carbon::createFromTimeString($row->lastAccessed)?->format(DateFormatHelper::getSystemFormat());
if (!$this->isApi($request) && $this->getUser()->isSuperAdmin()) {
$row->includeProperty('buttons');
// No buttons on expired sessions
if ($row->isExpired == 1) {
continue;
}
// logout, current user/session
if ($row->userId === $this->getUser()->userId && session_id() === $row->sessionId) {
$url = $this->urlFor($request, 'logout');
} else {
// logout, different user/session
$url = $this->urlFor(
$request,
'sessions.confirm.logout.form',
['id' => $row->userId]
);
}
$row->buttons[] = [
'id' => 'sessions_button_logout',
'url' => $url,
'text' => __('Logout')
];
}
}
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = $this->sessionFactory->countLast();
$this->getState()->setData($sessions);
return $this->render($request, $response);
}
/**
* Confirm Logout Form
* @param Request $request
* @param Response $response
* @param int $id The UserID
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function confirmLogoutForm(Request $request, Response $response, $id)
{
if ($this->getUser()->userTypeId != 1) {
throw new AccessDeniedException();
}
$this->getState()->template = 'sessions-form-confirm-logout';
$this->getState()->setData([
'userId' => $id,
]);
return $this->render($request, $response);
}
/**
* Logout
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function logout(Request $request, Response $response, $id)
{
if ($this->getUser()->userTypeId != 1) {
throw new AccessDeniedException();
}
// We log out all of this user's sessions.
$this->sessionFactory->expireByUserId($id);
// Return
$this->getState()->hydrate([
'message' => __('User Logged Out.')
]);
return $this->render($request, $response);
}
}

793
lib/Controller/Settings.php Normal file
View File

@@ -0,0 +1,793 @@
<?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\Controller;
use Carbon\Carbon;
use Respect\Validation\Validator as v;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Event\PlaylistMaxNumberChangedEvent;
use Xibo\Event\SystemUserChangedEvent;
use Xibo\Factory\LayoutFactory;
use Xibo\Factory\TransitionFactory;
use Xibo\Factory\UserFactory;
use Xibo\Factory\UserGroupFactory;
use Xibo\Helper\DateFormatHelper;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
/**
* Class Settings
* @package Xibo\Controller
*/
class Settings extends Base
{
/** @var LayoutFactory */
private $layoutFactory;
/** @var UserGroupFactory */
private $userGroupFactory;
/** @var TransitionFactory */
private $transitionfactory;
/** @var UserFactory */
private $userFactory;
/**
* Set common dependencies.
* @param LayoutFactory $layoutFactory
* @param UserGroupFactory $userGroupFactory
* @param TransitionFactory $transitionfactory
* @param UserFactory $userFactory
*/
public function __construct($layoutFactory, $userGroupFactory, $transitionfactory, $userFactory)
{
$this->layoutFactory = $layoutFactory;
$this->userGroupFactory = $userGroupFactory;
$this->transitionfactory = $transitionfactory;
$this->userFactory = $userFactory;
}
/**
* Display Page
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function displayPage(Request $request, Response $response)
{
// Should we hide other themes?
$themes = [];
$hideThemes = $this->getConfig()->getThemeConfig('hide_others');
if (!$hideThemes) {
// Get all theme options
$directory = new \RecursiveDirectoryIterator(PROJECT_ROOT . '/web/theme', \FilesystemIterator::SKIP_DOTS);
$filter = new \RecursiveCallbackFilterIterator($directory, function($current, $key, $iterator) {
if ($current->isDir()) {
return true;
}
return strpos($current->getFilename(), 'config.php') === 0;
});
$iterator = new \RecursiveIteratorIterator($filter);
// Add options for all themes installed
foreach($iterator as $file) {
/* @var \SplFileInfo $file */
$this->getLog()->debug('Found ' . $file->getPath());
// Include the config file
include $file->getPath() . '/' . $file->getFilename();
$themes[] = ['id' => basename($file->getPath()), 'value' => $config['theme_name']];
}
}
// A list of timezones
$timeZones = [];
foreach (DateFormatHelper::timezoneList() as $key => $value) {
$timeZones[] = ['id' => $key, 'value' => $value];
}
// A list of languages
// Build an array of supported languages
$languages = [];
$localeDir = PROJECT_ROOT . '/locale';
foreach (array_map('basename', glob($localeDir . '/*.mo')) as $lang) {
// Trim the .mo off the end
$lang = str_replace('.mo', '', $lang);
$languages[] = ['id' => $lang, 'value' => $lang];
}
// The default layout
try {
$defaultLayout = $this->layoutFactory->getById($this->getConfig()->getSetting('DEFAULT_LAYOUT'));
} catch (NotFoundException $notFoundException) {
$defaultLayout = null;
}
// The system User
try {
$systemUser = $this->userFactory->getById($this->getConfig()->getSetting('SYSTEM_USER'));
} catch (NotFoundException $notFoundException) {
$systemUser = null;
}
// The default user group
try {
$defaultUserGroup = $this->userGroupFactory->getById($this->getConfig()->getSetting('DEFAULT_USERGROUP'));
} catch (NotFoundException $notFoundException) {
$defaultUserGroup = null;
}
// The default Transition In
try {
$defaultTransitionIn = $this->transitionfactory->getByCode($this->getConfig()->getSetting('DEFAULT_TRANSITION_IN'));
} catch (NotFoundException $notFoundException) {
$defaultTransitionIn = null;
}
// The default Transition Out
try {
$defaultTransitionOut = $this->transitionfactory->getByCode($this->getConfig()->getSetting('DEFAULT_TRANSITION_OUT'));
} catch (NotFoundException $notFoundException) {
$defaultTransitionOut = null;
}
// Work out whether we're in a valid elevate log period
$elevateLogUntil = $this->getConfig()->getSetting('ELEVATE_LOG_UNTIL');
if ($elevateLogUntil != null) {
$elevateLogUntil = intval($elevateLogUntil);
if ($elevateLogUntil <= Carbon::now()->format('U')) {
$elevateLogUntil = null;
} else {
$elevateLogUntil = Carbon::createFromTimestamp($elevateLogUntil)->format(DateFormatHelper::getSystemFormat());
}
}
// Render the Theme and output
$this->getState()->template = 'settings-page';
$this->getState()->setData([
'hideThemes' => $hideThemes,
'themes' => $themes,
'languages' => $languages,
'timeZones' => $timeZones,
'defaultLayout' => $defaultLayout,
'defaultUserGroup' => $defaultUserGroup,
'elevateLogUntil' => $elevateLogUntil,
'defaultTransitionIn' => $defaultTransitionIn,
'defaultTransitionOut' => $defaultTransitionOut,
'systemUser' => $systemUser
]);
return $this->render($request, $response);
}
/**
* Update settings
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws InvalidArgumentException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @phpcs:disable Generic.Files.LineLength.TooLong
*/
public function update(Request $request, Response $response)
{
if (!$this->getUser()->isSuperAdmin()) {
throw new AccessDeniedException();
}
$changedSettings = [];
$sanitizedParams = $this->getSanitizer($request->getParams());
// Pull in all of the settings we're expecting to be submitted with this form.
if ($this->getConfig()->isSettingEditable('LIBRARY_LOCATION')) {
$libraryLocation = $sanitizedParams->getString('LIBRARY_LOCATION');
// Validate library location
// Check for a trailing slash and add it if its not there
$libraryLocation = rtrim($libraryLocation, '/');
$libraryLocation = rtrim($libraryLocation, '\\') . DIRECTORY_SEPARATOR;
// Attempt to add the directory specified
if (!file_exists($libraryLocation . 'temp')) {
// Make the directory with broad permissions recursively (so will add the whole path)
mkdir($libraryLocation . 'temp', 0777, true);
}
if (!is_writable($libraryLocation . 'temp')) {
throw new InvalidArgumentException(__('The Library Location you have picked is not writeable'), 'LIBRARY_LOCATION');
}
$this->handleChangedSettings('LIBRARY_LOCATION', $this->getConfig()->getSetting('LIBRARY_LOCATION'), $libraryLocation, $changedSettings);
$this->getConfig()->changeSetting('LIBRARY_LOCATION', $libraryLocation);
}
if ($this->getConfig()->isSettingEditable('SERVER_KEY')) {
$this->handleChangedSettings('SERVER_KEY', $this->getConfig()->getSetting('SERVER_KEY'), $sanitizedParams->getString('SERVER_KEY'), $changedSettings);
$this->getConfig()->changeSetting('SERVER_KEY', $sanitizedParams->getString('SERVER_KEY'));
}
if ($this->getConfig()->isSettingEditable('GLOBAL_THEME_NAME')) {
$this->handleChangedSettings('GLOBAL_THEME_NAME', $this->getConfig()->getSetting('GLOBAL_THEME_NAME'), $sanitizedParams->getString('GLOBAL_THEME_NAME'), $changedSettings);
$this->getConfig()->changeSetting('GLOBAL_THEME_NAME', $sanitizedParams->getString('GLOBAL_THEME_NAME'));
}
if ($this->getConfig()->isSettingEditable('NAVIGATION_MENU_POSITION')) {
$this->handleChangedSettings('NAVIGATION_MENU_POSITION', $this->getConfig()->getSetting('NAVIGATION_MENU_POSITION'), $sanitizedParams->getString('NAVIGATION_MENU_POSITION'), $changedSettings);
$this->getConfig()->changeSetting('NAVIGATION_MENU_POSITION', $sanitizedParams->getString('NAVIGATION_MENU_POSITION'));
}
if ($this->getConfig()->isSettingEditable('LIBRARY_MEDIA_UPDATEINALL_CHECKB')) {
$this->handleChangedSettings('LIBRARY_MEDIA_UPDATEINALL_CHECKB', $this->getConfig()->getSetting('LIBRARY_MEDIA_UPDATEINALL_CHECKB'), $sanitizedParams->getCheckbox('LIBRARY_MEDIA_UPDATEINALL_CHECKB'), $changedSettings);
$this->getConfig()->changeSetting('LIBRARY_MEDIA_UPDATEINALL_CHECKB', $sanitizedParams->getCheckbox('LIBRARY_MEDIA_UPDATEINALL_CHECKB'));
}
if ($this->getConfig()->isSettingEditable('LAYOUT_COPY_MEDIA_CHECKB')) {
$this->handleChangedSettings('LAYOUT_COPY_MEDIA_CHECKB', $this->getConfig()->getSetting('LAYOUT_COPY_MEDIA_CHECKB'), $sanitizedParams->getCheckbox('LAYOUT_COPY_MEDIA_CHECKB'), $changedSettings);
$this->getConfig()->changeSetting('LAYOUT_COPY_MEDIA_CHECKB', $sanitizedParams->getCheckbox('LAYOUT_COPY_MEDIA_CHECKB'));
}
if ($this->getConfig()->isSettingEditable('LIBRARY_MEDIA_DELETEOLDVER_CHECKB')) {
$this->handleChangedSettings('LIBRARY_MEDIA_DELETEOLDVER_CHECKB', $this->getConfig()->getSetting('LIBRARY_MEDIA_DELETEOLDVER_CHECKB'), $sanitizedParams->getCheckbox('LIBRARY_MEDIA_DELETEOLDVER_CHECKB'), $changedSettings);
$this->getConfig()->changeSetting('LIBRARY_MEDIA_DELETEOLDVER_CHECKB', $sanitizedParams->getCheckbox('LIBRARY_MEDIA_DELETEOLDVER_CHECKB'));
}
if ($this->getConfig()->isSettingEditable('DEFAULT_LAYOUT_AUTO_PUBLISH_CHECKB')) {
$this->handleChangedSettings('DEFAULT_LAYOUT_AUTO_PUBLISH_CHECKB', $this->getConfig()->getSetting('DEFAULT_LAYOUT_AUTO_PUBLISH_CHECKB'), $sanitizedParams->getCheckbox('DEFAULT_LAYOUT_AUTO_PUBLISH_CHECKB'), $changedSettings);
$this->getConfig()->changeSetting('DEFAULT_LAYOUT_AUTO_PUBLISH_CHECKB', $sanitizedParams->getCheckbox('DEFAULT_LAYOUT_AUTO_PUBLISH_CHECKB'));
}
if ($this->getConfig()->isSettingEditable('DEFAULT_TRANSITION_IN')) {
$this->handleChangedSettings('DEFAULT_TRANSITION_IN', $this->getConfig()->getSetting('DEFAULT_TRANSITION_IN'), $sanitizedParams->getString('DEFAULT_TRANSITION_IN'), $changedSettings);
$this->getConfig()->changeSetting('DEFAULT_TRANSITION_IN', $sanitizedParams->getString('DEFAULT_TRANSITION_IN'));
}
if ($this->getConfig()->isSettingEditable('DEFAULT_TRANSITION_OUT')) {
$this->handleChangedSettings('DEFAULT_TRANSITION_OUT', $this->getConfig()->getSetting('DEFAULT_TRANSITION_OUT'), $sanitizedParams->getString('DEFAULT_TRANSITION_OUT'), $changedSettings);
$this->getConfig()->changeSetting('DEFAULT_TRANSITION_OUT', $sanitizedParams->getString('DEFAULT_TRANSITION_OUT'));
}
if ($this->getConfig()->isSettingEditable('DEFAULT_TRANSITION_DURATION')) {
$this->handleChangedSettings('DEFAULT_TRANSITION_DURATION', $this->getConfig()->getSetting('DEFAULT_TRANSITION_DURATION'), $sanitizedParams->getInt('DEFAULT_TRANSITION_DURATION'), $changedSettings);
$this->getConfig()->changeSetting('DEFAULT_TRANSITION_DURATION', $sanitizedParams->getInt('DEFAULT_TRANSITION_DURATION'));
}
if ($this->getConfig()->isSettingEditable('DEFAULT_TRANSITION_AUTO_APPLY')) {
$this->handleChangedSettings('DEFAULT_TRANSITION_AUTO_APPLY', $this->getConfig()->getSetting('DEFAULT_TRANSITION_AUTO_APPLY'), $sanitizedParams->getCheckbox('DEFAULT_TRANSITION_AUTO_APPLY'), $changedSettings);
$this->getConfig()->changeSetting('DEFAULT_TRANSITION_AUTO_APPLY', $sanitizedParams->getCheckbox('DEFAULT_TRANSITION_AUTO_APPLY'));
}
if ($this->getConfig()->isSettingEditable('DEFAULT_RESIZE_THRESHOLD')) {
$this->handleChangedSettings('DEFAULT_RESIZE_THRESHOLD', $this->getConfig()->getSetting('DEFAULT_RESIZE_THRESHOLD'), $sanitizedParams->getInt('DEFAULT_RESIZE_THRESHOLD'), $changedSettings);
$this->getConfig()->changeSetting('DEFAULT_RESIZE_THRESHOLD', $sanitizedParams->getInt('DEFAULT_RESIZE_THRESHOLD'));
}
if ($this->getConfig()->isSettingEditable('DEFAULT_RESIZE_LIMIT')) {
$this->handleChangedSettings('DEFAULT_RESIZE_LIMIT', $this->getConfig()->getSetting('DEFAULT_RESIZE_LIMIT'), $sanitizedParams->getInt('DEFAULT_RESIZE_LIMIT'), $changedSettings);
$this->getConfig()->changeSetting('DEFAULT_RESIZE_LIMIT', $sanitizedParams->getInt('DEFAULT_RESIZE_LIMIT'));
}
if ($this->getConfig()->isSettingEditable('DATASET_HARD_ROW_LIMIT')) {
$this->handleChangedSettings('DATASET_HARD_ROW_LIMIT', $this->getConfig()->getSetting('DATASET_HARD_ROW_LIMIT'), $sanitizedParams->getInt('DATASET_HARD_ROW_LIMIT'), $changedSettings);
$this->getConfig()->changeSetting('DATASET_HARD_ROW_LIMIT', $sanitizedParams->getInt('DATASET_HARD_ROW_LIMIT'));
}
if ($this->getConfig()->isSettingEditable('DEFAULT_PURGE_LIST_TTL')) {
$this->handleChangedSettings('DEFAULT_PURGE_LIST_TTL', $this->getConfig()->getSetting('DEFAULT_PURGE_LIST_TTL'), $sanitizedParams->getInt('DEFAULT_PURGE_LIST_TTL'), $changedSettings);
$this->getConfig()->changeSetting('DEFAULT_PURGE_LIST_TTL', $sanitizedParams->getInt('DEFAULT_PURGE_LIST_TTL'));
}
if ($this->getConfig()->isSettingEditable('DEFAULT_LAYOUT')) {
$this->handleChangedSettings('DEFAULT_LAYOUT', $this->getConfig()->getSetting('DEFAULT_LAYOUT'), $sanitizedParams->getInt('DEFAULT_LAYOUT'), $changedSettings);
$this->getConfig()->changeSetting('DEFAULT_LAYOUT', $sanitizedParams->getInt('DEFAULT_LAYOUT'));
}
if ($this->getConfig()->isSettingEditable('XMR_ADDRESS')) {
$this->handleChangedSettings('XMR_ADDRESS', $this->getConfig()->getSetting('XMR_ADDRESS'), $sanitizedParams->getString('XMR_ADDRESS'), $changedSettings);
$this->getConfig()->changeSetting('XMR_ADDRESS', $sanitizedParams->getString('XMR_ADDRESS'));
}
if ($this->getConfig()->isSettingEditable('XMR_PUB_ADDRESS')) {
$this->handleChangedSettings('XMR_PUB_ADDRESS', $this->getConfig()->getSetting('XMR_PUB_ADDRESS'), $sanitizedParams->getString('XMR_PUB_ADDRESS'), $changedSettings);
$this->getConfig()->changeSetting('XMR_PUB_ADDRESS', $sanitizedParams->getString('XMR_PUB_ADDRESS'));
}
if ($this->getConfig()->isSettingEditable('XMR_WS_ADDRESS')) {
$this->handleChangedSettings('XMR_WS_ADDRESS', $this->getConfig()->getSetting('XMR_WS_ADDRESS'), $sanitizedParams->getString('XMR_WS_ADDRESS'), $changedSettings);
$this->getConfig()->changeSetting('XMR_WS_ADDRESS', $sanitizedParams->getString('XMR_WS_ADDRESS'), 1);
}
if ($this->getConfig()->isSettingEditable('DEFAULT_LAT')) {
$value = $sanitizedParams->getString('DEFAULT_LAT');
$this->handleChangedSettings('DEFAULT_LAT', $this->getConfig()->getSetting('DEFAULT_LAT'), $value, $changedSettings);
$this->getConfig()->changeSetting('DEFAULT_LAT', $value);
if (!v::latitude()->validate($value)) {
throw new InvalidArgumentException(__('The latitude entered is not valid.'), 'DEFAULT_LAT');
}
}
if ($this->getConfig()->isSettingEditable('DEFAULT_LONG')) {
$value = $sanitizedParams->getString('DEFAULT_LONG');
$this->handleChangedSettings('DEFAULT_LONG', $this->getConfig()->getSetting('DEFAULT_LONG'), $value, $changedSettings);
$this->getConfig()->changeSetting('DEFAULT_LONG', $value);
if (!v::longitude()->validate($value)) {
throw new InvalidArgumentException(__('The longitude entered is not valid.'), 'DEFAULT_LONG');
}
}
if ($this->getConfig()->isSettingEditable('DEFAULT_DYNAMIC_PLAYLIST_MAXNUMBER')) {
$this->handleChangedSettings('DEFAULT_DYNAMIC_PLAYLIST_MAXNUMBER', $this->getConfig()->getSetting('DEFAULT_DYNAMIC_PLAYLIST_MAXNUMBER'), $sanitizedParams->getInt('DEFAULT_DYNAMIC_PLAYLIST_MAXNUMBER'), $changedSettings);
$this->getConfig()->changeSetting('DEFAULT_DYNAMIC_PLAYLIST_MAXNUMBER', $sanitizedParams->getInt('DEFAULT_DYNAMIC_PLAYLIST_MAXNUMBER'));
}
if ($this->getConfig()->isSettingEditable('DEFAULT_DYNAMIC_PLAYLIST_MAXNUMBER_LIMIT')) {
$this->handleChangedSettings('DEFAULT_DYNAMIC_PLAYLIST_MAXNUMBER_LIMIT', $this->getConfig()->getSetting('DEFAULT_DYNAMIC_PLAYLIST_MAXNUMBER_LIMIT'), $sanitizedParams->getInt('DEFAULT_DYNAMIC_PLAYLIST_MAXNUMBER_LIMIT'), $changedSettings);
$this->getConfig()->changeSetting('DEFAULT_DYNAMIC_PLAYLIST_MAXNUMBER_LIMIT', $sanitizedParams->getInt('DEFAULT_DYNAMIC_PLAYLIST_MAXNUMBER_LIMIT'));
}
if ($this->getConfig()->isSettingEditable('SHOW_DISPLAY_AS_VNCLINK')) {
$this->handleChangedSettings('SHOW_DISPLAY_AS_VNCLINK', $this->getConfig()->getSetting('SHOW_DISPLAY_AS_VNCLINK'), $sanitizedParams->getString('SHOW_DISPLAY_AS_VNCLINK'), $changedSettings);
$this->getConfig()->changeSetting('SHOW_DISPLAY_AS_VNCLINK', $sanitizedParams->getString('SHOW_DISPLAY_AS_VNCLINK'));
}
if ($this->getConfig()->isSettingEditable('SHOW_DISPLAY_AS_VNC_TGT')) {
$this->handleChangedSettings('SHOW_DISPLAY_AS_VNC_TGT', $this->getConfig()->getSetting('SHOW_DISPLAY_AS_VNC_TGT'), $sanitizedParams->getString('SHOW_DISPLAY_AS_VNC_TGT'), $changedSettings);
$this->getConfig()->changeSetting('SHOW_DISPLAY_AS_VNC_TGT', $sanitizedParams->getString('SHOW_DISPLAY_AS_VNC_TGT'));
}
if ($this->getConfig()->isSettingEditable('MAX_LICENSED_DISPLAYS')) {
$this->handleChangedSettings('MAX_LICENSED_DISPLAYS', $this->getConfig()->getSetting('MAX_LICENSED_DISPLAYS'), $sanitizedParams->getInt('MAX_LICENSED_DISPLAYS'), $changedSettings);
$this->getConfig()->changeSetting('MAX_LICENSED_DISPLAYS', $sanitizedParams->getInt('MAX_LICENSED_DISPLAYS'));
}
if ($this->getConfig()->isSettingEditable('DISPLAY_PROFILE_AGGREGATION_LEVEL_DEFAULT')) {
$this->handleChangedSettings('DISPLAY_PROFILE_AGGREGATION_LEVEL_DEFAULT', $this->getConfig()->getSetting('DISPLAY_PROFILE_AGGREGATION_LEVEL_DEFAULT'), $sanitizedParams->getString('DISPLAY_PROFILE_AGGREGATION_LEVEL_DEFAULT'), $changedSettings);
$this->getConfig()->changeSetting('DISPLAY_PROFILE_AGGREGATION_LEVEL_DEFAULT', $sanitizedParams->getString('DISPLAY_PROFILE_AGGREGATION_LEVEL_DEFAULT'));
}
if ($this->getConfig()->isSettingEditable('DISPLAY_PROFILE_STATS_DEFAULT')) {
$this->handleChangedSettings('DISPLAY_PROFILE_STATS_DEFAULT', $this->getConfig()->getSetting('DISPLAY_PROFILE_STATS_DEFAULT'), $sanitizedParams->getCheckbox('DISPLAY_PROFILE_STATS_DEFAULT'), $changedSettings);
$this->getConfig()->changeSetting('DISPLAY_PROFILE_STATS_DEFAULT', $sanitizedParams->getCheckbox('DISPLAY_PROFILE_STATS_DEFAULT'));
}
if ($this->getConfig()->isSettingEditable('LAYOUT_STATS_ENABLED_DEFAULT')) {
$this->handleChangedSettings('LAYOUT_STATS_ENABLED_DEFAULT', $this->getConfig()->getSetting('LAYOUT_STATS_ENABLED_DEFAULT'), $sanitizedParams->getCheckbox('LAYOUT_STATS_ENABLED_DEFAULT'), $changedSettings);
$this->getConfig()->changeSetting('LAYOUT_STATS_ENABLED_DEFAULT', $sanitizedParams->getCheckbox('LAYOUT_STATS_ENABLED_DEFAULT'));
}
if ($this->getConfig()->isSettingEditable('PLAYLIST_STATS_ENABLED_DEFAULT')) {
$this->handleChangedSettings('PLAYLIST_STATS_ENABLED_DEFAULT', $this->getConfig()->getSetting('PLAYLIST_STATS_ENABLED_DEFAULT'), $sanitizedParams->getString('PLAYLIST_STATS_ENABLED_DEFAULT'), $changedSettings);
$this->getConfig()->changeSetting('PLAYLIST_STATS_ENABLED_DEFAULT', $sanitizedParams->getString('PLAYLIST_STATS_ENABLED_DEFAULT'));
}
if ($this->getConfig()->isSettingEditable('MEDIA_STATS_ENABLED_DEFAULT')) {
$this->handleChangedSettings('MEDIA_STATS_ENABLED_DEFAULT', $this->getConfig()->getSetting('MEDIA_STATS_ENABLED_DEFAULT'), $sanitizedParams->getString('MEDIA_STATS_ENABLED_DEFAULT'), $changedSettings);
$this->getConfig()->changeSetting('MEDIA_STATS_ENABLED_DEFAULT', $sanitizedParams->getString('MEDIA_STATS_ENABLED_DEFAULT'));
}
if ($this->getConfig()->isSettingEditable('WIDGET_STATS_ENABLED_DEFAULT')) {
$this->handleChangedSettings('WIDGET_STATS_ENABLED_DEFAULT', $this->getConfig()->getSetting('WIDGET_STATS_ENABLED_DEFAULT'), $sanitizedParams->getString('WIDGET_STATS_ENABLED_DEFAULT'), $changedSettings);
$this->getConfig()->changeSetting('WIDGET_STATS_ENABLED_DEFAULT', $sanitizedParams->getString('WIDGET_STATS_ENABLED_DEFAULT'));
}
if ($this->getConfig()->isSettingEditable('DISPLAY_PROFILE_CURRENT_LAYOUT_STATUS_ENABLED')) {
$this->handleChangedSettings('DISPLAY_PROFILE_CURRENT_LAYOUT_STATUS_ENABLED', $this->getConfig()->getSetting('DISPLAY_PROFILE_CURRENT_LAYOUT_STATUS_ENABLED'), $sanitizedParams->getCheckbox('DISPLAY_PROFILE_CURRENT_LAYOUT_STATUS_ENABLED'), $changedSettings);
$this->getConfig()->changeSetting('DISPLAY_PROFILE_CURRENT_LAYOUT_STATUS_ENABLED', $sanitizedParams->getCheckbox('DISPLAY_PROFILE_CURRENT_LAYOUT_STATUS_ENABLED'));
}
if ($this->getConfig()->isSettingEditable('DISPLAY_LOCK_NAME_TO_DEVICENAME')) {
$this->handleChangedSettings('DISPLAY_LOCK_NAME_TO_DEVICENAME', $this->getConfig()->getSetting('DISPLAY_LOCK_NAME_TO_DEVICENAME'), $sanitizedParams->getCheckbox('DISPLAY_LOCK_NAME_TO_DEVICENAME'), $changedSettings);
$this->getConfig()->changeSetting('DISPLAY_LOCK_NAME_TO_DEVICENAME', $sanitizedParams->getCheckbox('DISPLAY_LOCK_NAME_TO_DEVICENAME'));
}
if ($this->getConfig()->isSettingEditable('DISPLAY_PROFILE_SCREENSHOT_INTERVAL_ENABLED')) {
$this->handleChangedSettings('DISPLAY_PROFILE_SCREENSHOT_INTERVAL_ENABLED', $this->getConfig()->getSetting('DISPLAY_PROFILE_SCREENSHOT_INTERVAL_ENABLED'), $sanitizedParams->getCheckbox('DISPLAY_PROFILE_SCREENSHOT_INTERVAL_ENABLED'), $changedSettings);
$this->getConfig()->changeSetting('DISPLAY_PROFILE_SCREENSHOT_INTERVAL_ENABLED', $sanitizedParams->getCheckbox('DISPLAY_PROFILE_SCREENSHOT_INTERVAL_ENABLED'));
}
if ($this->getConfig()->isSettingEditable('DISPLAY_PROFILE_SCREENSHOT_SIZE_DEFAULT')) {
$this->handleChangedSettings('DISPLAY_PROFILE_SCREENSHOT_SIZE_DEFAULT', $this->getConfig()->getSetting('DISPLAY_PROFILE_SCREENSHOT_SIZE_DEFAULT'), $sanitizedParams->getInt('DISPLAY_PROFILE_SCREENSHOT_SIZE_DEFAULT'), $changedSettings);
$this->getConfig()->changeSetting('DISPLAY_PROFILE_SCREENSHOT_SIZE_DEFAULT', $sanitizedParams->getInt('DISPLAY_PROFILE_SCREENSHOT_SIZE_DEFAULT'));
}
if ($this->getConfig()->isSettingEditable('DISPLAY_SCREENSHOT_TTL')) {
$this->handleChangedSettings('DISPLAY_SCREENSHOT_TTL', $this->getConfig()->getSetting('DISPLAY_SCREENSHOT_TTL'), $sanitizedParams->getInt('DISPLAY_SCREENSHOT_TTL'), $changedSettings);
$this->getConfig()->changeSetting('DISPLAY_SCREENSHOT_TTL', $sanitizedParams->getInt('DISPLAY_SCREENSHOT_TTL'));
}
if ($this->getConfig()->isSettingEditable('DISPLAY_AUTO_AUTH')) {
$this->handleChangedSettings('DISPLAY_AUTO_AUTH', $this->getConfig()->getSetting('DISPLAY_AUTO_AUTH'), $sanitizedParams->getCheckbox('DISPLAY_AUTO_AUTH'), $changedSettings);
$this->getConfig()->changeSetting('DISPLAY_AUTO_AUTH', $sanitizedParams->getCheckbox('DISPLAY_AUTO_AUTH'));
}
if ($this->getConfig()->isSettingEditable('DISPLAY_DEFAULT_FOLDER')) {
$this->handleChangedSettings(
'DISPLAY_DEFAULT_FOLDER',
$this->getConfig()->getSetting('DISPLAY_DEFAULT_FOLDER'),
$sanitizedParams->getInt('DISPLAY_DEFAULT_FOLDER'),
$changedSettings
);
$this->getConfig()->changeSetting(
'DISPLAY_DEFAULT_FOLDER',
$sanitizedParams->getInt('DISPLAY_DEFAULT_FOLDER'),
1
);
}
if ($this->getConfig()->isSettingEditable('HELP_BASE')) {
$this->handleChangedSettings('HELP_BASE', $this->getConfig()->getSetting('HELP_BASE'), $sanitizedParams->getString('HELP_BASE'), $changedSettings);
$this->getConfig()->changeSetting('HELP_BASE', $sanitizedParams->getString('HELP_BASE'));
}
if ($this->getConfig()->isSettingEditable('QUICK_CHART_URL')) {
$this->handleChangedSettings('QUICK_CHART_URL', $this->getConfig()->getSetting('QUICK_CHART_URL'), $sanitizedParams->getString('QUICK_CHART_URL'), $changedSettings);
$this->getConfig()->changeSetting('QUICK_CHART_URL', $sanitizedParams->getString('QUICK_CHART_URL'));
}
if ($this->getConfig()->isSettingEditable('PHONE_HOME')) {
$this->handleChangedSettings('PHONE_HOME', $this->getConfig()->getSetting('PHONE_HOME'), $sanitizedParams->getCheckbox('PHONE_HOME'), $changedSettings);
$this->getConfig()->changeSetting('PHONE_HOME', $sanitizedParams->getCheckbox('PHONE_HOME'));
}
if ($this->getConfig()->isSettingEditable('PHONE_HOME_KEY')) {
$this->handleChangedSettings('PHONE_HOME_KEY', $this->getConfig()->getSetting('PHONE_HOME_KEY'), $sanitizedParams->getString('PHONE_HOME_KEY'), $changedSettings);
$this->getConfig()->changeSetting('PHONE_HOME_KEY', $sanitizedParams->getString('PHONE_HOME_KEY'));
}
if ($this->getConfig()->isSettingEditable('PHONE_HOME_DATE')) {
$this->handleChangedSettings('PHONE_HOME_DATE', $this->getConfig()->getSetting('PHONE_HOME_DATE'), $sanitizedParams->getInt('PHONE_HOME_DATE'), $changedSettings);
$this->getConfig()->changeSetting('PHONE_HOME_DATE', $sanitizedParams->getInt('PHONE_HOME_DATE'));
}
if ($this->getConfig()->isSettingEditable('SCHEDULE_LOOKAHEAD')) {
$this->handleChangedSettings('SCHEDULE_LOOKAHEAD', $this->getConfig()->getSetting('SCHEDULE_LOOKAHEAD'), $sanitizedParams->getCheckbox('SCHEDULE_LOOKAHEAD'), $changedSettings);
$this->getConfig()->changeSetting('SCHEDULE_LOOKAHEAD', $sanitizedParams->getCheckbox('SCHEDULE_LOOKAHEAD'));
}
if ($this->getConfig()->isSettingEditable('REQUIRED_FILES_LOOKAHEAD')) {
$this->handleChangedSettings('REQUIRED_FILES_LOOKAHEAD', $this->getConfig()->getSetting('REQUIRED_FILES_LOOKAHEAD'), $sanitizedParams->getInt('REQUIRED_FILES_LOOKAHEAD'), $changedSettings);
$this->getConfig()->changeSetting('REQUIRED_FILES_LOOKAHEAD', $sanitizedParams->getInt('REQUIRED_FILES_LOOKAHEAD'));
}
if ($this->getConfig()->isSettingEditable('SETTING_IMPORT_ENABLED')) {
$this->handleChangedSettings('SETTING_IMPORT_ENABLED', $this->getConfig()->getSetting('SETTING_IMPORT_ENABLED'), $sanitizedParams->getCheckbox('SETTING_IMPORT_ENABLED'), $changedSettings);
$this->getConfig()->changeSetting('SETTING_IMPORT_ENABLED', $sanitizedParams->getCheckbox('SETTING_IMPORT_ENABLED'));
}
if ($this->getConfig()->isSettingEditable('SETTING_LIBRARY_TIDY_ENABLED')) {
$this->handleChangedSettings('SETTING_LIBRARY_TIDY_ENABLED', $this->getConfig()->getSetting('SETTING_LIBRARY_TIDY_ENABLED'), $sanitizedParams->getCheckbox('SETTING_LIBRARY_TIDY_ENABLED'), $changedSettings);
$this->getConfig()->changeSetting('SETTING_LIBRARY_TIDY_ENABLED', $sanitizedParams->getCheckbox('SETTING_LIBRARY_TIDY_ENABLED'));
}
if ($this->getConfig()->isSettingEditable('EMBEDDED_STATUS_WIDGET')) {
$this->handleChangedSettings('EMBEDDED_STATUS_WIDGET', $this->getConfig()->getSetting('EMBEDDED_STATUS_WIDGET'), $sanitizedParams->getString('EMBEDDED_STATUS_WIDGET'), $changedSettings);
$this->getConfig()->changeSetting('EMBEDDED_STATUS_WIDGET', $sanitizedParams->getString('EMBEDDED_STATUS_WIDGET'));
}
if ($this->getConfig()->isSettingEditable('DEFAULTS_IMPORTED')) {
$this->handleChangedSettings('DEFAULTS_IMPORTED', $this->getConfig()->getSetting('DEFAULTS_IMPORTED'), $sanitizedParams->getCheckbox('DEFAULTS_IMPORTED'), $changedSettings);
$this->getConfig()->changeSetting('DEFAULTS_IMPORTED', $sanitizedParams->getCheckbox('DEFAULTS_IMPORTED'));
}
if ($this->getConfig()->isSettingEditable('DASHBOARD_LATEST_NEWS_ENABLED')) {
$this->handleChangedSettings('DASHBOARD_LATEST_NEWS_ENABLED', $this->getConfig()->getSetting('DASHBOARD_LATEST_NEWS_ENABLED'), $sanitizedParams->getCheckbox('DASHBOARD_LATEST_NEWS_ENABLED'), $changedSettings);
$this->getConfig()->changeSetting('DASHBOARD_LATEST_NEWS_ENABLED', $sanitizedParams->getCheckbox('DASHBOARD_LATEST_NEWS_ENABLED'));
}
if ($this->getConfig()->isSettingEditable('INSTANCE_SUSPENDED')) {
$this->handleChangedSettings('INSTANCE_SUSPENDED', $this->getConfig()->getSetting('INSTANCE_SUSPENDED'), $sanitizedParams->getString('INSTANCE_SUSPENDED'), $changedSettings);
$this->getConfig()->changeSetting('INSTANCE_SUSPENDED', $sanitizedParams->getString('INSTANCE_SUSPENDED'));
}
if ($this->getConfig()->isSettingEditable('LATEST_NEWS_URL')) {
$this->handleChangedSettings('LATEST_NEWS_URL', $this->getConfig()->getSetting('LATEST_NEWS_URL'), $sanitizedParams->getString('LATEST_NEWS_URL'), $changedSettings);
$this->getConfig()->changeSetting('LATEST_NEWS_URL', $sanitizedParams->getString('LATEST_NEWS_URL'));
}
if ($this->getConfig()->isSettingEditable('REPORTS_EXPORT_SHOW_LOGO')) {
$this->handleChangedSettings(
'REPORTS_EXPORT_SHOW_LOGO',
$this->getConfig()->getSetting('REPORTS_EXPORT_SHOW_LOGO'),
$sanitizedParams->getCheckbox('REPORTS_EXPORT_SHOW_LOGO'),
$changedSettings
);
$this->getConfig()->changeSetting(
'REPORTS_EXPORT_SHOW_LOGO',
$sanitizedParams->getCheckbox('REPORTS_EXPORT_SHOW_LOGO')
);
}
if ($this->getConfig()->isSettingEditable('MAINTENANCE_ENABLED')) {
$this->handleChangedSettings('MAINTENANCE_ENABLED', $this->getConfig()->getSetting('MAINTENANCE_ENABLED'), $sanitizedParams->getString('MAINTENANCE_ENABLED'), $changedSettings);
$this->getConfig()->changeSetting('MAINTENANCE_ENABLED', $sanitizedParams->getString('MAINTENANCE_ENABLED'));
}
if ($this->getConfig()->isSettingEditable('MAINTENANCE_EMAIL_ALERTS')) {
$this->handleChangedSettings('MAINTENANCE_EMAIL_ALERTS', $this->getConfig()->getSetting('MAINTENANCE_EMAIL_ALERTS'), $sanitizedParams->getCheckbox('MAINTENANCE_EMAIL_ALERTS'), $changedSettings);
$this->getConfig()->changeSetting('MAINTENANCE_EMAIL_ALERTS', $sanitizedParams->getCheckbox('MAINTENANCE_EMAIL_ALERTS'));
}
if ($this->getConfig()->isSettingEditable('MAINTENANCE_LOG_MAXAGE')) {
$this->handleChangedSettings('MAINTENANCE_LOG_MAXAGE', $this->getConfig()->getSetting('MAINTENANCE_LOG_MAXAGE'), $sanitizedParams->getInt('MAINTENANCE_LOG_MAXAGE'), $changedSettings);
$this->getConfig()->changeSetting('MAINTENANCE_LOG_MAXAGE', $sanitizedParams->getInt('MAINTENANCE_LOG_MAXAGE'));
}
if ($this->getConfig()->isSettingEditable('MAINTENANCE_STAT_MAXAGE')) {
$this->handleChangedSettings('MAINTENANCE_STAT_MAXAGE', $this->getConfig()->getSetting('MAINTENANCE_STAT_MAXAGE'), $sanitizedParams->getInt('MAINTENANCE_STAT_MAXAGE'), $changedSettings);
$this->getConfig()->changeSetting('MAINTENANCE_STAT_MAXAGE', $sanitizedParams->getInt('MAINTENANCE_STAT_MAXAGE'));
}
if ($this->getConfig()->isSettingEditable('MAINTENANCE_ALERT_TOUT')) {
$this->handleChangedSettings('MAINTENANCE_ALERT_TOUT', $this->getConfig()->getSetting('MAINTENANCE_ALERT_TOUT'), $sanitizedParams->getInt('MAINTENANCE_ALERT_TOUT'), $changedSettings);
$this->getConfig()->changeSetting('MAINTENANCE_ALERT_TOUT', $sanitizedParams->getInt('MAINTENANCE_ALERT_TOUT'));
}
if ($this->getConfig()->isSettingEditable('MAINTENANCE_ALWAYS_ALERT')) {
$this->handleChangedSettings('MAINTENANCE_ALWAYS_ALERT', $this->getConfig()->getSetting('MAINTENANCE_ALWAYS_ALERT'), $sanitizedParams->getCheckbox('MAINTENANCE_ALWAYS_ALERT'), $changedSettings);
$this->getConfig()->changeSetting('MAINTENANCE_ALWAYS_ALERT', $sanitizedParams->getCheckbox('MAINTENANCE_ALWAYS_ALERT'));
}
if ($this->getConfig()->isSettingEditable('mail_to')) {
$this->handleChangedSettings('mail_to', $this->getConfig()->getSetting('mail_to'), $sanitizedParams->getString('mail_to'), $changedSettings);
$this->getConfig()->changeSetting('mail_to', $sanitizedParams->getString('mail_to'));
}
if ($this->getConfig()->isSettingEditable('mail_from')) {
$this->handleChangedSettings('mail_from', $this->getConfig()->getSetting('mail_from'), $sanitizedParams->getString('mail_from'), $changedSettings);
$this->getConfig()->changeSetting('mail_from', $sanitizedParams->getString('mail_from'));
}
if ($this->getConfig()->isSettingEditable('mail_from_name')) {
$this->handleChangedSettings('mail_from_name', $this->getConfig()->getSetting('mail_from_name'), $sanitizedParams->getString('mail_from_name'), $changedSettings);
$this->getConfig()->changeSetting('mail_from_name', $sanitizedParams->getString('mail_from_name'));
}
if ($this->getConfig()->isSettingEditable('SENDFILE_MODE')) {
$this->handleChangedSettings('SENDFILE_MODE', $this->getConfig()->getSetting('SENDFILE_MODE'), $sanitizedParams->getString('SENDFILE_MODE'), $changedSettings);
$this->getConfig()->changeSetting('SENDFILE_MODE', $sanitizedParams->getString('SENDFILE_MODE'));
}
if ($this->getConfig()->isSettingEditable('PROXY_HOST')) {
$this->handleChangedSettings('PROXY_HOST', $this->getConfig()->getSetting('PROXY_HOST'), $sanitizedParams->getString('PROXY_HOST'), $changedSettings);
$this->getConfig()->changeSetting('PROXY_HOST', $sanitizedParams->getString('PROXY_HOST'));
}
if ($this->getConfig()->isSettingEditable('PROXY_PORT')) {
$this->handleChangedSettings('PROXY_PORT', $this->getConfig()->getSetting('PROXY_PORT'), $sanitizedParams->getString('PROXY_PORT'), $changedSettings);
$this->getConfig()->changeSetting('PROXY_PORT', $sanitizedParams->getString('PROXY_PORT'));
}
if ($this->getConfig()->isSettingEditable('PROXY_AUTH')) {
$this->handleChangedSettings('PROXY_AUTH', $this->getConfig()->getSetting('PROXY_AUTH'), $sanitizedParams->getString('PROXY_AUTH'), $changedSettings);
$this->getConfig()->changeSetting('PROXY_AUTH', $sanitizedParams->getString('PROXY_AUTH'));
}
if ($this->getConfig()->isSettingEditable('PROXY_EXCEPTIONS')) {
$this->handleChangedSettings('PROXY_EXCEPTIONS', $this->getConfig()->getSetting('PROXY_EXCEPTIONS'), $sanitizedParams->getString('PROXY_EXCEPTIONS'), $changedSettings);
$this->getConfig()->changeSetting('PROXY_EXCEPTIONS', $sanitizedParams->getString('PROXY_EXCEPTIONS'));
}
if ($this->getConfig()->isSettingEditable('CDN_URL')) {
$this->handleChangedSettings('CDN_URL', $this->getConfig()->getSetting('CDN_URL'), $sanitizedParams->getString('CDN_URL'), $changedSettings);
$this->getConfig()->changeSetting('CDN_URL', $sanitizedParams->getString('CDN_URL'));
}
if ($this->getConfig()->isSettingEditable('MONTHLY_XMDS_TRANSFER_LIMIT_KB')) {
$this->handleChangedSettings('MONTHLY_XMDS_TRANSFER_LIMIT_KB', $this->getConfig()->getSetting('MONTHLY_XMDS_TRANSFER_LIMIT_KB'), $sanitizedParams->getInt('MONTHLY_XMDS_TRANSFER_LIMIT_KB'), $changedSettings);
$this->getConfig()->changeSetting('MONTHLY_XMDS_TRANSFER_LIMIT_KB', $sanitizedParams->getInt('MONTHLY_XMDS_TRANSFER_LIMIT_KB'));
}
if ($this->getConfig()->isSettingEditable('LIBRARY_SIZE_LIMIT_KB')) {
$this->handleChangedSettings('LIBRARY_SIZE_LIMIT_KB', $this->getConfig()->getSetting('LIBRARY_SIZE_LIMIT_KB'), $sanitizedParams->getInt('LIBRARY_SIZE_LIMIT_KB'), $changedSettings);
$this->getConfig()->changeSetting('LIBRARY_SIZE_LIMIT_KB', $sanitizedParams->getInt('LIBRARY_SIZE_LIMIT_KB'));
}
if ($this->getConfig()->isSettingEditable('FORCE_HTTPS')) {
$this->handleChangedSettings('FORCE_HTTPS', $this->getConfig()->getSetting('FORCE_HTTPS'), $sanitizedParams->getCheckbox('FORCE_HTTPS'), $changedSettings);
$this->getConfig()->changeSetting('FORCE_HTTPS', $sanitizedParams->getCheckbox('FORCE_HTTPS'));
}
if ($this->getConfig()->isSettingEditable('ISSUE_STS')) {
$this->handleChangedSettings('ISSUE_STS', $this->getConfig()->getSetting('ISSUE_STS'), $sanitizedParams->getCheckbox('ISSUE_STS'), $changedSettings);
$this->getConfig()->changeSetting('ISSUE_STS', $sanitizedParams->getCheckbox('ISSUE_STS'));
}
if ($this->getConfig()->isSettingEditable('STS_TTL')) {
$this->handleChangedSettings('STS_TTL', $this->getConfig()->getSetting('STS_TTL'), $sanitizedParams->getInt('STS_TTL'), $changedSettings);
$this->getConfig()->changeSetting('STS_TTL', $sanitizedParams->getInt('STS_TTL'));
}
if ($this->getConfig()->isSettingEditable('WHITELIST_LOAD_BALANCERS')) {
$this->handleChangedSettings('WHITELIST_LOAD_BALANCERS', $this->getConfig()->getSetting('WHITELIST_LOAD_BALANCERS'), $sanitizedParams->getString('WHITELIST_LOAD_BALANCERS'), $changedSettings);
$this->getConfig()->changeSetting('WHITELIST_LOAD_BALANCERS', $sanitizedParams->getString('WHITELIST_LOAD_BALANCERS'));
}
if ($this->getConfig()->isSettingEditable('REGION_OPTIONS_COLOURING')) {
$this->handleChangedSettings('REGION_OPTIONS_COLOURING', $this->getConfig()->getSetting('REGION_OPTIONS_COLOURING'), $sanitizedParams->getString('REGION_OPTIONS_COLOURING'), $changedSettings);
$this->getConfig()->changeSetting('REGION_OPTIONS_COLOURING', $sanitizedParams->getString('REGION_OPTIONS_COLOURING'));
}
if ($this->getConfig()->isSettingEditable('SCHEDULE_WITH_VIEW_PERMISSION')) {
$this->handleChangedSettings('SCHEDULE_WITH_VIEW_PERMISSION', $this->getConfig()->getSetting('SCHEDULE_WITH_VIEW_PERMISSION'), $sanitizedParams->getCheckbox('SCHEDULE_WITH_VIEW_PERMISSION'), $changedSettings);
$this->getConfig()->changeSetting('SCHEDULE_WITH_VIEW_PERMISSION', $sanitizedParams->getCheckbox('SCHEDULE_WITH_VIEW_PERMISSION'));
}
if ($this->getConfig()->isSettingEditable('SCHEDULE_SHOW_LAYOUT_NAME')) {
$this->handleChangedSettings('SCHEDULE_SHOW_LAYOUT_NAME', $this->getConfig()->getSetting('SCHEDULE_SHOW_LAYOUT_NAME'), $sanitizedParams->getCheckbox('SCHEDULE_SHOW_LAYOUT_NAME'), $changedSettings);
$this->getConfig()->changeSetting('SCHEDULE_SHOW_LAYOUT_NAME', $sanitizedParams->getCheckbox('SCHEDULE_SHOW_LAYOUT_NAME'));
}
if ($this->getConfig()->isSettingEditable('TASK_CONFIG_LOCKED_CHECKB')) {
$this->handleChangedSettings('TASK_CONFIG_LOCKED_CHECKB', $this->getConfig()->getSetting('TASK_CONFIG_LOCKED_CHECKB'), $sanitizedParams->getCheckbox('TASK_CONFIG_LOCKED_CHECKB'), $changedSettings);
$this->getConfig()->changeSetting('TASK_CONFIG_LOCKED_CHECKB', $sanitizedParams->getCheckbox('TASK_CONFIG_LOCKED_CHECKB'));
}
if ($this->getConfig()->isSettingEditable('TRANSITION_CONFIG_LOCKED_CHECKB')) {
$this->handleChangedSettings('TRANSITION_CONFIG_LOCKED_CHECKB', $this->getConfig()->getSetting('TRANSITION_CONFIG_LOCKED_CHECKB'), $sanitizedParams->getCheckbox('TRANSITION_CONFIG_LOCKED_CHECKB'), $changedSettings);
$this->getConfig()->changeSetting('TRANSITION_CONFIG_LOCKED_CHECKB', $sanitizedParams->getCheckbox('TRANSITION_CONFIG_LOCKED_CHECKB'));
}
if ($this->getConfig()->isSettingEditable('FOLDERS_ALLOW_SAVE_IN_ROOT')) {
$this->handleChangedSettings('FOLDERS_ALLOW_SAVE_IN_ROOT', $this->getConfig()->getSetting('FOLDERS_ALLOW_SAVE_IN_ROOT'), $sanitizedParams->getCheckbox('FOLDERS_ALLOW_SAVE_IN_ROOT'), $changedSettings);
$this->getConfig()->changeSetting('FOLDERS_ALLOW_SAVE_IN_ROOT', $sanitizedParams->getCheckbox('FOLDERS_ALLOW_SAVE_IN_ROOT'));
}
if ($this->getConfig()->isSettingEditable('DEFAULT_LANGUAGE')) {
$this->handleChangedSettings('DEFAULT_LANGUAGE', $this->getConfig()->getSetting('DEFAULT_LANGUAGE'), $sanitizedParams->getString('DEFAULT_LANGUAGE'), $changedSettings);
$this->getConfig()->changeSetting('DEFAULT_LANGUAGE', $sanitizedParams->getString('DEFAULT_LANGUAGE'));
}
if ($this->getConfig()->isSettingEditable('defaultTimezone')) {
$this->handleChangedSettings('defaultTimezone', $this->getConfig()->getSetting('defaultTimezone'), $sanitizedParams->getString('defaultTimezone'), $changedSettings);
$this->getConfig()->changeSetting('defaultTimezone', $sanitizedParams->getString('defaultTimezone'));
}
if ($this->getConfig()->isSettingEditable('DATE_FORMAT')) {
$this->handleChangedSettings('DATE_FORMAT', $this->getConfig()->getSetting('DATE_FORMAT'), $sanitizedParams->getString('DATE_FORMAT'), $changedSettings);
$this->getConfig()->changeSetting('DATE_FORMAT', $sanitizedParams->getString('DATE_FORMAT'));
}
if ($this->getConfig()->isSettingEditable('DETECT_LANGUAGE')) {
$this->handleChangedSettings('DETECT_LANGUAGE', $this->getConfig()->getSetting('DETECT_LANGUAGE'), $sanitizedParams->getCheckbox('DETECT_LANGUAGE'), $changedSettings);
$this->getConfig()->changeSetting('DETECT_LANGUAGE', $sanitizedParams->getCheckbox('DETECT_LANGUAGE'));
}
if ($this->getConfig()->isSettingEditable('CALENDAR_TYPE')) {
$this->handleChangedSettings('CALENDAR_TYPE', $this->getConfig()->getSetting('CALENDAR_TYPE'), $sanitizedParams->getString('CALENDAR_TYPE'), $changedSettings);
$this->getConfig()->changeSetting('CALENDAR_TYPE', $sanitizedParams->getString('CALENDAR_TYPE'));
}
if ($this->getConfig()->isSettingEditable('RESTING_LOG_LEVEL')) {
$this->handleChangedSettings('RESTING_LOG_LEVEL', $this->getConfig()->getSetting('RESTING_LOG_LEVEL'), $sanitizedParams->getString('RESTING_LOG_LEVEL'), $changedSettings);
$this->getConfig()->changeSetting('RESTING_LOG_LEVEL', $sanitizedParams->getString('RESTING_LOG_LEVEL'));
}
// Handle changes to log level
$newLogLevel = null;
$newElevateUntil = null;
$currentLogLevel = $this->getConfig()->getSetting('audit');
if ($this->getConfig()->isSettingEditable('audit')) {
$newLogLevel = $sanitizedParams->getString('audit');
$this->handleChangedSettings('audit', $this->getConfig()->getSetting('audit'), $newLogLevel, $changedSettings);
$this->getConfig()->changeSetting('audit', $newLogLevel);
}
if ($this->getConfig()->isSettingEditable('ELEVATE_LOG_UNTIL') && $sanitizedParams->getDate('ELEVATE_LOG_UNTIL') != null) {
$newElevateUntil = $sanitizedParams->getDate('ELEVATE_LOG_UNTIL')->format('U');
$this->handleChangedSettings('ELEVATE_LOG_UNTIL', $this->getConfig()->getSetting('ELEVATE_LOG_UNTIL'), $newElevateUntil, $changedSettings);
$this->getConfig()->changeSetting('ELEVATE_LOG_UNTIL', $newElevateUntil);
}
// Have we changed log level? If so, were we also provided the elevate until setting?
if ($newElevateUntil === null && $currentLogLevel != $newLogLevel) {
// We haven't provided an elevate until (meaning it is not visible)
$this->getConfig()->changeSetting('ELEVATE_LOG_UNTIL', Carbon::now()->addHour()->format('U'));
}
if ($this->getConfig()->isSettingEditable('SERVER_MODE')) {
$this->handleChangedSettings('SERVER_MODE', $this->getConfig()->getSetting('SERVER_MODE'), $sanitizedParams->getString('SERVER_MODE'), $changedSettings);
$this->getConfig()->changeSetting('SERVER_MODE', $sanitizedParams->getString('SERVER_MODE'));
}
if ($this->getConfig()->isSettingEditable('SYSTEM_USER')) {
$this->handleChangedSettings('SYSTEM_USER', $this->getConfig()->getSetting('SYSTEM_USER'), $sanitizedParams->getInt('SYSTEM_USER'), $changedSettings);
$this->getConfig()->changeSetting('SYSTEM_USER', $sanitizedParams->getInt('SYSTEM_USER'));
}
if ($this->getConfig()->isSettingEditable('DEFAULT_USERGROUP')) {
$this->handleChangedSettings('DEFAULT_USERGROUP', $this->getConfig()->getSetting('DEFAULT_USERGROUP'), $sanitizedParams->getInt('DEFAULT_USERGROUP'), $changedSettings);
$this->getConfig()->changeSetting('DEFAULT_USERGROUP', $sanitizedParams->getInt('DEFAULT_USERGROUP'));
}
if ($this->getConfig()->isSettingEditable('defaultUsertype')) {
$this->handleChangedSettings('defaultUsertype', $this->getConfig()->getSetting('defaultUsertype'), $sanitizedParams->getString('defaultUsertype'), $changedSettings);
$this->getConfig()->changeSetting('defaultUsertype', $sanitizedParams->getString('defaultUsertype'));
}
if ($this->getConfig()->isSettingEditable('USER_PASSWORD_POLICY')) {
$this->handleChangedSettings('USER_PASSWORD_POLICY', $this->getConfig()->getSetting('USER_PASSWORD_POLICY'), $sanitizedParams->getString('USER_PASSWORD_POLICY'), $changedSettings);
$this->getConfig()->changeSetting('USER_PASSWORD_POLICY', $sanitizedParams->getString('USER_PASSWORD_POLICY'));
}
if ($this->getConfig()->isSettingEditable('USER_PASSWORD_ERROR')) {
$this->handleChangedSettings('USER_PASSWORD_ERROR', $this->getConfig()->getSetting('USER_PASSWORD_ERROR'), $sanitizedParams->getString('USER_PASSWORD_ERROR'), $changedSettings);
$this->getConfig()->changeSetting('USER_PASSWORD_ERROR', $sanitizedParams->getString('USER_PASSWORD_ERROR'));
}
if ($this->getConfig()->isSettingEditable('PASSWORD_REMINDER_ENABLED')) {
$this->handleChangedSettings('PASSWORD_REMINDER_ENABLED', $this->getConfig()->getSetting('PASSWORD_REMINDER_ENABLED'), $sanitizedParams->getString('PASSWORD_REMINDER_ENABLED'), $changedSettings);
$this->getConfig()->changeSetting('PASSWORD_REMINDER_ENABLED', $sanitizedParams->getString('PASSWORD_REMINDER_ENABLED'));
}
if ($this->getConfig()->isSettingEditable('TWOFACTOR_ISSUER')) {
$this->handleChangedSettings('TWOFACTOR_ISSUER', $this->getConfig()->getSetting('TWOFACTOR_ISSUER'), $sanitizedParams->getString('TWOFACTOR_ISSUER'), $changedSettings);
$this->getConfig()->changeSetting('TWOFACTOR_ISSUER', $sanitizedParams->getString('TWOFACTOR_ISSUER'));
}
if ($changedSettings != []) {
$this->getLog()->audit('Settings', 0, 'Updated', $changedSettings);
}
// Return
$this->getState()->hydrate([
'message' => __('Settings Updated')
]);
return $this->render($request, $response);
}
private function handleChangedSettings($setting, $oldValue, $newValue, &$changedSettings)
{
if ($oldValue != $newValue) {
if ($setting === 'SYSTEM_USER') {
$newSystemUser = $this->userFactory->getById($newValue);
$oldSystemUser = $this->userFactory->getById($oldValue);
$this->getDispatcher()->dispatch(SystemUserChangedEvent::$NAME, new SystemUserChangedEvent($oldSystemUser, $newSystemUser));
} elseif ($setting === 'DEFAULT_DYNAMIC_PLAYLIST_MAXNUMBER_LIMIT') {
$this->getDispatcher()->dispatch(PlaylistMaxNumberChangedEvent::$NAME, new PlaylistMaxNumberChangedEvent($newValue));
}
if ($setting === 'ELEVATE_LOG_UNTIL') {
$changedSettings[$setting] = Carbon::createFromTimestamp($oldValue)->format(DateFormatHelper::getSystemFormat()) . ' > ' . Carbon::createFromTimestamp($newValue)->format(DateFormatHelper::getSystemFormat());
} else {
$changedSettings[$setting] = $oldValue . ' > ' . $newValue;
}
}
}
}

1085
lib/Controller/Stats.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,550 @@
<?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\Controller;
use Carbon\Carbon;
use Exception;
use GuzzleHttp\Client;
use PicoFeed\PicoFeedException;
use PicoFeed\Reader\Reader;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Stash\Interfaces\PoolInterface;
use Xibo\Factory\DisplayFactory;
use Xibo\Factory\DisplayGroupFactory;
use Xibo\Factory\MediaFactory;
use Xibo\Factory\UserFactory;
use Xibo\Helper\ByteFormatter;
use Xibo\Helper\DateFormatHelper;
use Xibo\Service\MediaService;
use Xibo\Storage\StorageServiceInterface;
/**
* Class StatusDashboard
* @package Xibo\Controller
*/
class StatusDashboard extends Base
{
/**
* @var StorageServiceInterface
*/
private $store;
/**
* @var PoolInterface
*/
private $pool;
/**
* @var UserFactory
*/
private $userFactory;
/**
* @var DisplayFactory
*/
private $displayFactory;
/**
* @var DisplayGroupFactory
*/
private $displayGroupFactory;
/**
* @var MediaFactory
*/
private $mediaFactory;
/**
* Set common dependencies.
* @param StorageServiceInterface $store
* @param PoolInterface $pool
* @param UserFactory $userFactory
* @param DisplayFactory $displayFactory
* @param DisplayGroupFactory $displayGroupFactory
* @param MediaFactory $mediaFactory
*/
public function __construct($store, $pool, $userFactory, $displayFactory, $displayGroupFactory, $mediaFactory)
{
$this->store = $store;
$this->pool = $pool;
$this->userFactory = $userFactory;
$this->displayFactory = $displayFactory;
$this->displayGroupFactory = $displayGroupFactory;
$this->mediaFactory = $mediaFactory;
}
/**
* Displays
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function displays(Request $request, Response $response)
{
$parsedRequestParams = $this->getSanitizer($request->getParams());
// Get a list of displays
$displays = $this->displayFactory->query($this->gridRenderSort($parsedRequestParams), $this->gridRenderFilter([], $parsedRequestParams));
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = $this->displayFactory->countLast();
$this->getState()->setData($displays);
return $this->render($request, $response);
}
/**
* View
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function displayPage(Request $request, Response $response)
{
$data = [];
// Set up some suffixes
$suffixes = array('B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB');
try {
// Get some data for a bandwidth chart
$dbh = $this->store->getConnection();
$params = ['month' => Carbon::now()->subSeconds(86400 * 365)->format('U')];
$sql = '
SELECT month,
SUM(size) AS size
FROM (
SELECT MAX(FROM_UNIXTIME(month)) AS month,
IFNULL(SUM(Size), 0) AS size,
MIN(month) AS month_order
FROM `bandwidth`
INNER JOIN `lkdisplaydg`
ON lkdisplaydg.displayID = bandwidth.displayId
INNER JOIN `displaygroup`
ON displaygroup.DisplayGroupID = lkdisplaydg.DisplayGroupID
AND displaygroup.isDisplaySpecific = 1
WHERE month > :month
';
// Permissions
$this->displayFactory->viewPermissionSql('Xibo\Entity\DisplayGroup', $sql, $params, '`lkdisplaydg`.displayGroupId');
$sql .= ' GROUP BY MONTH(FROM_UNIXTIME(month)) ';
// Include deleted displays?
if ($this->getUser()->isSuperAdmin()) {
$sql .= '
UNION ALL
SELECT MAX(FROM_UNIXTIME(month)) AS month,
IFNULL(SUM(Size), 0) AS size,
MIN(month) AS month_order
FROM `bandwidth`
WHERE bandwidth.displayId NOT IN (SELECT displayId FROM `display`)
AND month > :month
GROUP BY MONTH(FROM_UNIXTIME(month))
';
}
$sql .= '
) grp
GROUP BY month
ORDER BY MIN(month_order)
';
// Run the SQL
$results = $this->store->select($sql, $params);
// Monthly bandwidth - optionally tested against limits
$xmdsLimit = $this->getConfig()->getSetting('MONTHLY_XMDS_TRANSFER_LIMIT_KB');
$maxSize = 0;
foreach ($results as $row) {
$maxSize = ($row['size'] > $maxSize) ? $row['size'] : $maxSize;
}
// Decide what our units are going to be, based on the size
$base = ($maxSize == 0) ? 0 : floor(log($maxSize) / log(1024));
if ($xmdsLimit > 0) {
// Convert to appropriate size (xmds limit is in KB)
$xmdsLimit = ($xmdsLimit * 1024) / (pow(1024, $base));
$data['xmdsLimit'] = round($xmdsLimit, 2) . ' ' . $suffixes[$base];
}
$labels = [];
$usage = [];
$limit = [];
foreach ($results as $row) {
$sanitizedRow = $this->getSanitizer($row);
$labels[] = Carbon::createFromTimeString($sanitizedRow->getString('month'))->format('F');
$size = ((double)$row['size']) / (pow(1024, $base));
$usage[] = round($size, 2);
$limit[] = round($xmdsLimit - $size, 2);
}
// What if we are empty?
if (count($results) == 0) {
$labels[] = Carbon::now()->format('F');
$usage[] = 0;
$limit[] = 0;
}
// Organise our datasets
$dataSets = [
[
'label' => __('Used'),
'backgroundColor' => ($xmdsLimit > 0) ? 'rgb(255, 0, 0)' : 'rgb(11, 98, 164)',
'data' => $usage
]
];
if ($xmdsLimit > 0) {
$dataSets[] = [
'label' => __('Available'),
'backgroundColor' => 'rgb(0, 204, 0)',
'data' => $limit
];
}
// Set the data
$data['xmdsLimitSet'] = ($xmdsLimit > 0);
$data['bandwidthSuffix'] = $suffixes[$base];
$data['bandwidthWidget'] = json_encode([
'labels' => $labels,
'datasets' => $dataSets
]);
// We would also like a library usage pie chart!
if ($this->getUser()->libraryQuota != 0) {
$libraryLimit = $this->getUser()->libraryQuota * 1024;
} else {
$libraryLimit = $this->getConfig()->getSetting('LIBRARY_SIZE_LIMIT_KB') * 1024;
}
// Library Size in Bytes
$params = [];
$sql = 'SELECT IFNULL(SUM(FileSize), 0) AS SumSize, type FROM `media` WHERE 1 = 1 ';
$this->mediaFactory->viewPermissionSql('Xibo\Entity\Media', $sql, $params, '`media`.mediaId', '`media`.userId', [], 'media.permissionsFolderId');
$sql .= ' GROUP BY type ';
$sth = $dbh->prepare($sql);
$sth->execute($params);
$results = $sth->fetchAll();
// add any dependencies fonts, player software etc to the results
$event = new \Xibo\Event\DependencyFileSizeEvent($results);
$this->getDispatcher()->dispatch($event, $event::$NAME);
$results = $event->getResults();
// Do we base the units on the maximum size or the library limit
$maxSize = 0;
if ($libraryLimit > 0) {
$maxSize = $libraryLimit;
} else {
// Find the maximum sized chunk of the items in the library
foreach ($results as $library) {
$maxSize = ($library['SumSize'] > $maxSize) ? $library['SumSize'] : $maxSize;
}
}
// Decide what our units are going to be, based on the size
$base = ($maxSize == 0) ? 0 : floor(log($maxSize) / log(1024));
$libraryUsage = [];
$libraryLabels = [];
$totalSize = 0;
foreach ($results as $library) {
$libraryUsage[] = round((double)$library['SumSize'] / (pow(1024, $base)), 2);
$libraryLabels[] = ucfirst($library['type']) . ' ' . $suffixes[$base];
$totalSize = $totalSize + $library['SumSize'];
}
// Do we need to add the library remaining?
if ($libraryLimit > 0) {
$remaining = round(($libraryLimit - $totalSize) / (pow(1024, $base)), 2);
$libraryUsage[] = $remaining;
$libraryLabels[] = __('Free') . ' ' . $suffixes[$base];
}
// What if we are empty?
if (count($results) == 0 && $libraryLimit <= 0) {
$libraryUsage[] = 0;
$libraryLabels[] = __('Empty');
}
$data['libraryLimitSet'] = ($libraryLimit > 0);
$data['libraryLimit'] = (round((double)$libraryLimit / (pow(1024, $base)), 2)) . ' ' . $suffixes[$base];
$data['librarySize'] = ByteFormatter::format($totalSize, 1);
$data['librarySuffix'] = $suffixes[$base];
$data['libraryWidgetLabels'] = json_encode($libraryLabels);
$data['libraryWidgetData'] = json_encode($libraryUsage);
// Get a count of users
$data['countUsers'] = $this->userFactory->count();
// Get a count of active layouts, only for display groups we have permission for
$params = ['now' => Carbon::now()->format('U')];
$sql = '
SELECT IFNULL(COUNT(*), 0) AS count_scheduled
FROM `schedule`
WHERE (
:now BETWEEN FromDT AND ToDT
OR `schedule`.recurrence_range >= :now
OR (
IFNULL(`schedule`.recurrence_range, 0) = 0 AND IFNULL(`schedule`.recurrence_type, \'\') <> \'\'
)
)
AND eventId IN (
SELECT eventId
FROM `lkscheduledisplaygroup`
WHERE 1 = 1
';
$this->displayFactory->viewPermissionSql('Xibo\Entity\DisplayGroup', $sql, $params, '`lkscheduledisplaygroup`.displayGroupId');
$sql .= ' ) ';
$sth = $dbh->prepare($sql);
$sth->execute($params);
$data['nowShowing'] = $sth->fetchColumn(0);
// Latest news
if ($this->getConfig()->getSetting('DASHBOARD_LATEST_NEWS_ENABLED') == 1
&& !empty($this->getConfig()->getSetting('LATEST_NEWS_URL'))
) {
// Make sure we have the cache location configured
MediaService::ensureLibraryExists($this->getConfig()->getSetting('LIBRARY_LOCATION'));
try {
$feedUrl = $this->getConfig()->getSetting('LATEST_NEWS_URL');
$cache = $this->pool->getItem('rss/' . md5($feedUrl));
$latestNews = $cache->get();
// Check the cache
if ($cache->isMiss()) {
// Create a Guzzle Client to get the Feed XML
$client = new Client();
$responseGuzzle = $client->get($feedUrl, $this->getConfig()->getGuzzleProxy());
// Pull out the content type and body
$result = explode('charset=', $responseGuzzle->getHeaderLine('Content-Type'));
$document['encoding'] = $result[1] ?? '';
$document['xml'] = $responseGuzzle->getBody();
$this->getLog()->debug($document['xml']);
// Get the feed parser
$reader = new Reader();
$parser = $reader->getParser($feedUrl, $document['xml'], $document['encoding']);
// Get a feed object
$feed = $parser->execute();
// Parse the items in the feed
$latestNews = [];
foreach ($feed->getItems() as $item) {
// Try to get the description tag
$desc = $item->getTag('description');
if (!$desc) {
// use content with tags stripped
$content = strip_tags($item->getContent());
} else {
// use description
$content = ($desc[0] ?? strip_tags($item->getContent()));
}
$latestNews[] = [
'title' => $item->getTitle(),
'description' => $content,
'link' => $item->getUrl(),
'date' => Carbon::instance($item->getDate())->format(DateFormatHelper::getSystemFormat()),
];
}
// Store in the cache for 1 day
$cache->set($latestNews);
$cache->expiresAfter(86400);
$this->pool->saveDeferred($cache);
}
$data['latestNews'] = $latestNews;
} catch (PicoFeedException $e) {
$this->getLog()->error('Unable to get feed: %s', $e->getMessage());
$this->getLog()->debug($e->getTraceAsString());
$data['latestNews'] = array(array('title' => __('Latest news not available.'), 'description' => '', 'link' => ''));
}
} else {
$data['latestNews'] = array(array('title' => __('Latest news not enabled.'), 'description' => '', 'link' => ''));
}
// Display Status and Media Inventory data - Level one
$displays = $this->displayFactory->query();
$displayLoggedIn = [];
$displayMediaStatus = [];
$displaysOnline = 0;
$displaysOffline = 0;
$displaysMediaUpToDate = 0;
$displaysMediaNotUpToDate = 0;
foreach ($displays as $display) {
$displayLoggedIn[] = $display->loggedIn;
$displayMediaStatus[] = $display->mediaInventoryStatus;
}
foreach ($displayLoggedIn as $status) {
if ($status == 1) {
$displaysOnline++;
} else {
$displaysOffline++;
}
}
foreach ($displayMediaStatus as $statusMedia) {
if ($statusMedia == 1) {
$displaysMediaUpToDate++;
} else {
$displaysMediaNotUpToDate++;
}
}
$data['displayStatus'] = json_encode([$displaysOnline, $displaysOffline]);
$data['displayMediaStatus'] = json_encode([$displaysMediaUpToDate, $displaysMediaNotUpToDate]);
} catch (Exception $e) {
$this->getLog()->error($e->getMessage());
$this->getLog()->debug($e->getTraceAsString());
// Show the error in place of the bandwidth chart
$data['widget-error'] = 'Unable to get widget details';
}
// Do we have an embedded widget?
$data['embeddedWidget'] = html_entity_decode($this->getConfig()->getSetting('EMBEDDED_STATUS_WIDGET'));
// Render the Theme and output
$this->getState()->template = 'dashboard-status-page';
$this->getState()->setData($data);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function displayGroups(Request $request, Response $response)
{
$parsedQueryParams = $this->getSanitizer($request->getQueryParams());
$status = null;
$inventoryStatus = null;
$params = [];
$label = $parsedQueryParams->getString('status');
$labelContent = $parsedQueryParams->getString('inventoryStatus');
$displayGroupIds = [];
$displayGroupNames = [];
$displaysAssigned = [];
$data = [];
if (isset($label)) {
if ($label == 'Online') {
$status = 1;
} else {
$status = 0;
}
}
if (isset($labelContent)) {
if ($labelContent == 'Up to Date') {
$inventoryStatus = 1;
} else {
$inventoryStatus = -1;
}
}
try {
$sql = 'SELECT DISTINCT displaygroup.DisplayGroupID, displaygroup.displayGroup
FROM displaygroup
INNER JOIN `lkdisplaydg`
ON lkdisplaydg.DisplayGroupID = displaygroup.DisplayGroupID
INNER JOIN `display`
ON display.displayid = lkdisplaydg.DisplayID
WHERE
displaygroup.IsDisplaySpecific = 0 ';
if ($status !== null) {
$sql .= ' AND display.loggedIn = :status ';
$params = ['status' => $status];
}
if ($inventoryStatus != null) {
if ($inventoryStatus === -1) {
$sql .= ' AND display.MediaInventoryStatus <> 1';
} else {
$sql .= ' AND display.MediaInventoryStatus = :inventoryStatus';
$params = ['inventoryStatus' => $inventoryStatus];
}
}
$this->displayFactory->viewPermissionSql('Xibo\Entity\DisplayGroup', $sql, $params, '`lkdisplaydg`.displayGroupId', null, [], 'permissionsFolderId');
$sql .= ' ORDER BY displaygroup.DisplayGroup ';
$results = $this->store->select($sql, $params);
foreach ($results as $row) {
$displayGroupNames[] = $row['displayGroup'];
$displayGroupIds[] = $row['DisplayGroupID'];
$displaysAssigned[] = count($this->displayFactory->query(['displayGroup'], ['displayGroupId' => $row['DisplayGroupID'], 'mediaInventoryStatus' => $inventoryStatus, 'loggedIn' => $status]));
}
$data['displayGroupNames'] = json_encode($displayGroupNames);
$data['displayGroupIds'] = json_encode($displayGroupIds);
$data['displayGroupMembers'] = json_encode($displaysAssigned);
$this->getState()->setData($data);
} catch (Exception $e) {
$this->getLog()->error($e->getMessage());
$this->getLog()->debug($e->getTraceAsString());
}
return $this->render($request, $response);
}
}

722
lib/Controller/SyncGroup.php Executable file
View File

@@ -0,0 +1,722 @@
<?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\Controller;
use Psr\Http\Message\ResponseInterface;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Factory\FolderFactory;
use Xibo\Factory\SyncGroupFactory;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\ControllerNotImplemented;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
/**
* Class SyncGroup
* @package Xibo\Controller
*/
class SyncGroup extends Base
{
private SyncGroupFactory $syncGroupFactory;
private FolderFactory $folderFactory;
public function __construct(
SyncGroupFactory $syncGroupFactory,
FolderFactory $folderFactory
) {
$this->syncGroupFactory = $syncGroupFactory;
$this->folderFactory = $folderFactory;
}
/**
* Sync Group Page Render
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function displayPage(Request $request, Response $response)
{
$this->getState()->template = 'syncgroup-page';
return $this->render($request, $response);
}
/**
* @SWG\Get(
* path="/syncgroups",
* summary="Get Sync Groups",
* tags={"syncGroup"},
* operationId="syncGroupSearch",
* @SWG\Parameter(
* name="syncGroupId",
* in="query",
* description="Filter by syncGroup Id",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="name",
* in="query",
* description="Filter by syncGroup Name",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="ownerId",
* in="query",
* description="Filter by Owner ID",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="folderId",
* in="query",
* description="Filter by Folder ID",
* type="integer",
* required=false
* ),
* @SWG\Response(
* response=200,
* description="a successful response",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="#/definitions/SyncGroup")
* ),
* @SWG\Header(
* header="X-Total-Count",
* description="The total number of records",
* type="integer"
* )
* )
* )
* @param Request $request
* @param Response $response
* @return Response|ResponseInterface
* @throws AccessDeniedException
* @throws GeneralException
* @throws ControllerNotImplemented
* @throws InvalidArgumentException
*/
public function grid(Request $request, Response $response): Response|\Psr\Http\Message\ResponseInterface
{
$parsedQueryParams = $this->getSanitizer($request->getQueryParams());
$filter = [
'syncGroupId' => $parsedQueryParams->getInt('syncGroupId'),
'name' => $parsedQueryParams->getString('name'),
'folderId' => $parsedQueryParams->getInt('folderId'),
'ownerId' => $parsedQueryParams->getInt('ownerId'),
'leadDisplayId' => $parsedQueryParams->getInt('leadDisplayId')
];
$syncGroups = $this->syncGroupFactory->query(
$this->gridRenderSort($parsedQueryParams),
$this->gridRenderFilter($filter, $parsedQueryParams)
);
foreach ($syncGroups as $syncGroup) {
if (!empty($syncGroup->leadDisplayId)) {
try {
$display = $this->syncGroupFactory->getLeadDisplay($syncGroup->leadDisplayId);
$syncGroup->leadDisplay = $display->display;
} catch (NotFoundException $exception) {
$this->getLog()->error(
sprintf(
'Lead Display %d not found for %s',
$syncGroup->leadDisplayId,
$syncGroup->name
)
);
}
}
if ($this->isApi($request)) {
continue;
}
$syncGroup->includeProperty('buttons');
if ($this->getUser()->featureEnabled('display.syncModify')
&& $this->getUser()->checkEditable($syncGroup)
) {
// Edit
$syncGroup->buttons[] = [
'id' => 'syncgroup_button_group_edit',
'url' => $this->urlFor($request, 'syncgroup.form.edit', ['id' => $syncGroup->syncGroupId]),
'text' => __('Edit')
];
// Group Members
$syncGroup->buttons[] = [
'id' => 'syncgroup_button_group_members',
'url' => $this->urlFor($request, 'syncgroup.form.members', ['id' => $syncGroup->syncGroupId]),
'text' => __('Members')
];
$syncGroup->buttons[] = ['divider' => true];
// Delete
$syncGroup->buttons[] = [
'id' => 'syncgroup_button_group_delete',
'url' => $this->urlFor($request, 'syncgroup.form.delete', ['id' => $syncGroup->syncGroupId]),
'text' => __('Delete')
];
}
}
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = $this->syncGroupFactory->countLast();
$this->getState()->setData($syncGroups);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @return Response|ResponseInterface
* @throws ControllerNotImplemented
* @throws GeneralException
*/
public function addForm(Request $request, Response $response): Response|ResponseInterface
{
$this->getState()->template = 'syncgroup-form-add';
return $this->render($request, $response);
}
/**
* Adds a Sync Group
* @SWG\Post(
* path="/syncgroup/add",
* operationId="syncGroupAdd",
* tags={"syncGroup"},
* summary="Add a Sync Group",
* description="Add a new Sync Group to the CMS",
* @SWG\Parameter(
* name="name",
* in="formData",
* description="The Sync Group Name",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="syncPublisherPort",
* in="formData",
* description="The publisher port number on which sync group members will communicate - default 9590",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="folderId",
* in="formData",
* description="Folder ID to which this object should be assigned to",
* type="integer",
* required=false
* ),
* @SWG\Response(
* response=201,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/DisplayGroup"),
* @SWG\Header(
* header="Location",
* description="Location of the new DisplayGroup",
* type="string"
* )
* )
* )
* @param Request $request
* @param Response $response
* @return Response|ResponseInterface
* @throws AccessDeniedException
* @throws ControllerNotImplemented
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
*/
public function add(Request $request, Response $response): Response|ResponseInterface
{
if (!$this->getUser()->featureEnabled('display.syncAdd')) {
throw new AccessDeniedException();
}
$params = $this->getSanitizer($request->getParams());
// Folders
$folderId = $params->getInt('folderId');
if ($folderId === 1) {
$this->checkRootFolderAllowSave();
}
if (empty($folderId) || !$this->getUser()->featureEnabled('folder.view')) {
$folderId = $this->getUser()->homeFolderId;
}
$folder = $this->folderFactory->getById($folderId, 0);
$syncGroup = $this->syncGroupFactory->createEmpty();
$syncGroup->name = $params->getString('name');
$syncGroup->ownerId = $this->getUser()->userId;
$syncGroup->syncPublisherPort = $params->getInt('syncPublisherPort');
$syncGroup->syncSwitchDelay = $params->getInt('syncSwitchDelay');
$syncGroup->syncVideoPauseDelay = $params->getInt('syncVideoPauseDelay');
$syncGroup->folderId = $folder->getId();
$syncGroup->permissionsFolderId = $folder->getPermissionFolderIdOrThis();
$syncGroup->save();
// Return
$this->getState()->hydrate([
'httpStatus' => 201,
'message' => sprintf(__('Added %s'), $syncGroup->name),
'id' => $syncGroup->syncGroupId,
'data' => $syncGroup
]);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @param $id
* @return Response|ResponseInterface
* @throws AccessDeniedException
* @throws ControllerNotImplemented
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
*/
public function membersForm(Request $request, Response $response, $id): Response|ResponseInterface
{
$syncGroup = $this->syncGroupFactory->getById($id);
if (!$this->getUser()->checkEditable($syncGroup)) {
throw new AccessDeniedException();
}
// Displays in Group
$displaysAssigned = $syncGroup->getSyncGroupMembers();
$this->getState()->template = 'syncgroup-form-members';
$this->getState()->setData([
'syncGroup' => $syncGroup,
'extra' => [
'displaysAssigned' => $displaysAssigned,
],
]);
return $this->render($request, $response);
}
/**
* @SWG\Post(
* path="/syncgroup/{syncGroupId}/members",
* operationId="syncGroupMembers",
* tags={"syncGroup"},
* summary="Assign one or more Displays to a Sync Group",
* description="Adds the provided Displays to the Sync Group",
* @SWG\Parameter(
* name="syncGroupId",
* type="integer",
* in="path",
* description="The Sync Group to assign to",
* required=true
* ),
* @SWG\Parameter(
* name="displayId",
* type="array",
* in="formData",
* description="The Display Ids to assign",
* required=true,
* @SWG\Items(
* type="integer"
* )
* ),
* @SWG\Parameter(
* name="unassignDisplayId",
* in="formData",
* description="An optional array of Display IDs to unassign",
* type="array",
* required=false,
* @SWG\Items(type="integer")
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
* @param Request $request
* @param Response $response
* @param $id
* @return Response|ResponseInterface
* @throws AccessDeniedException
* @throws ControllerNotImplemented
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
*/
public function members(Request $request, Response $response, $id): Response|ResponseInterface
{
$syncGroup = $this->syncGroupFactory->getById($id);
$sanitizedParams = $this->getSanitizer($request->getParams());
if (!$this->getUser()->checkEditable($syncGroup)) {
throw new AccessDeniedException();
}
// Support both an array and a single int.
$displays = $sanitizedParams->getParam('displayId');
if (is_numeric($displays)) {
$displays = [$sanitizedParams->getInt('displayId')];
} else {
$displays = $sanitizedParams->getIntArray('displayId', ['default' => []]);
}
$syncGroup->setMembers($displays);
// Have we been provided with unassign id's as well?
$unSetDisplays = $sanitizedParams->getParam('unassignDisplayId');
if (is_numeric($unSetDisplays)) {
$unSetDisplays = [$sanitizedParams->getInt('unassignDisplayId')];
} else {
$unSetDisplays = $sanitizedParams->getIntArray('unassignDisplayId', ['default' => []]);
}
$syncGroup->unSetMembers($unSetDisplays);
$syncGroup->modifiedBy = $this->getUser()->userId;
if (empty($syncGroup->getSyncGroupMembers()) ||
in_array($syncGroup->leadDisplayId, $unSetDisplays)
) {
$syncGroup->leadDisplayId = null;
}
$syncGroup->save(['validate' => false]);
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Displays assigned to %s'), $syncGroup->name),
'id' => $syncGroup->syncGroupId
]);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @param $id
* @return Response|ResponseInterface
* @throws AccessDeniedException
* @throws ControllerNotImplemented
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
*/
public function editForm(Request $request, Response $response, $id): Response|ResponseInterface
{
$syncGroup = $this->syncGroupFactory->getById($id);
if (!$this->getUser()->checkEditable($syncGroup)) {
throw new AccessDeniedException();
}
$leadDisplay = null;
if (!empty($syncGroup->leadDisplayId)) {
$leadDisplay = $this->syncGroupFactory->getLeadDisplay($syncGroup->leadDisplayId);
}
$this->getState()->template = 'syncgroup-form-edit';
$this->getState()->setData([
'syncGroup' => $syncGroup,
'leadDisplay' => $leadDisplay,
]);
return $this->render($request, $response);
}
/**
* Edits a Sync Group
* @SWG\Post(
* path="/syncgroup/{syncGroupId}/edit",
* operationId="syncGroupEdit",
* tags={"syncGroup"},
* summary="Edit a Sync Group",
* description="Edit an existing Sync Group",
* @SWG\Parameter(
* name="syncGroupId",
* type="integer",
* in="path",
* description="The Sync Group to assign to",
* required=true
* ),
* @SWG\Parameter(
* name="name",
* in="formData",
* description="The Sync Group Name",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="syncPublisherPort",
* in="formData",
* description="The publisher port number on which sync group members will communicate - default 9590",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="syncSwitchDelay",
* in="formData",
* description="The delay (in ms) when displaying the changes in content - default 750",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="syncVideoPauseDelay",
* in="formData",
* description="The delay (in ms) before unpausing the video on start - default 100",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="leadDisplayId",
* in="formData",
* description="The ID of the Display that belongs to this Sync Group and should act as a Lead Display",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="folderId",
* in="formData",
* description="Folder ID to which this object should be assigned to",
* type="integer",
* required=false
* ),
* @SWG\Response(
* response=201,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/DisplayGroup"),
* @SWG\Header(
* header="Location",
* description="Location of the new DisplayGroup",
* type="string"
* )
* )
* )
* @param Request $request
* @param Response $response
* @param $id
* @return Response|ResponseInterface
* @throws AccessDeniedException
* @throws ControllerNotImplemented
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
*/
public function edit(Request $request, Response $response, $id): Response|ResponseInterface
{
$syncGroup = $this->syncGroupFactory->getById($id);
$params = $this->getSanitizer($request->getParams());
if (!$this->getUser()->checkEditable($syncGroup)) {
throw new AccessDeniedException();
}
// Folders
$folderId = $params->getInt('folderId');
if ($folderId === 1) {
$this->checkRootFolderAllowSave();
}
if (empty($folderId) || !$this->getUser()->featureEnabled('folder.view')) {
$folderId = $this->getUser()->homeFolderId;
}
$folder = $this->folderFactory->getById($folderId, 0);
$syncGroup->name = $params->getString('name');
$syncGroup->syncPublisherPort = $params->getInt('syncPublisherPort');
$syncGroup->syncSwitchDelay = $params->getInt('syncSwitchDelay');
$syncGroup->syncVideoPauseDelay = $params->getInt('syncVideoPauseDelay');
$syncGroup->leadDisplayId = $params->getInt('leadDisplayId');
$syncGroup->modifiedBy = $this->getUser()->userId;
$syncGroup->folderId = $folder->getId();
$syncGroup->permissionsFolderId = $folder->getPermissionFolderIdOrThis();
$syncGroup->save();
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Edited %s'), $syncGroup->name),
'id' => $syncGroup->syncGroupId,
'data' => $syncGroup
]);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @param $id
* @return Response|ResponseInterface
* @throws AccessDeniedException
* @throws ControllerNotImplemented
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
*/
public function deleteForm(Request $request, Response $response, $id): Response|ResponseInterface
{
$syncGroup = $this->syncGroupFactory->getById($id);
if (!$this->getUser()->checkDeleteable($syncGroup)) {
throw new AccessDeniedException();
}
// Set the form
$this->getState()->template = 'syncgroup-form-delete';
$this->getState()->setData([
'syncGroup' => $syncGroup,
]);
return $this->render($request, $response);
}
/**
* @SWG\Delete(
* path="/syncgroup/{syncGroupId}/delete",
* operationId="syncGroupDelete",
* tags={"syncGroup"},
* summary="Delete a Sync Group",
* description="Delete an existing Sync Group identified by its Id",
* @SWG\Parameter(
* name="syncGroupId",
* type="integer",
* in="path",
* description="The syncGroupId to delete",
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
* @param Request $request
* @param Response $response
* @param $id
* @return Response|ResponseInterface
* @throws AccessDeniedException
* @throws ControllerNotImplemented
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
*/
public function delete(Request $request, Response $response, $id): Response|ResponseInterface
{
$syncGroup = $this->syncGroupFactory->getById($id);
if (!$this->getUser()->checkDeleteable($syncGroup)) {
throw new AccessDeniedException();
}
$syncGroup->delete();
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Deleted %s'), $syncGroup->name)
]);
return $this->render($request, $response);
}
/**
* @SWG\Get(
* path="/syncgroup/{syncGroupId}/displays",
* summary="Get members of this sync group",
* tags={"syncGroup"},
* operationId="syncGroupDisplays",
* @SWG\Parameter(
* name="syncGroupId",
* type="integer",
* in="path",
* description="The syncGroupId to delete",
* required=true
* ),
* @SWG\Parameter(
* name="eventId",
* in="query",
* description="Filter by event ID - return will include Layouts Ids scheduled against each group member",
* type="integer",
* required=false
* ),
* @SWG\Response(
* response=200,
* description="a successful response",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="#/definitions/SyncGroup")
* ),
* )
* )
* @param Request $request
* @param Response $response
* @param $id
* @return Response|ResponseInterface
* @throws ControllerNotImplemented
* @throws GeneralException
* @throws NotFoundException
*/
public function fetchDisplays(Request $request, Response $response, $id): Response|ResponseInterface
{
$syncGroup = $this->syncGroupFactory->getById($id);
$params = $this->getSanitizer($request->getParams());
$displays = [];
if (!empty($params->getInt('eventId'))) {
$syncGroupMembers = $syncGroup->getGroupMembersForForm();
foreach ($syncGroupMembers as $display) {
$layoutId = $syncGroup->getLayoutIdForDisplay(
$params->getInt('eventId'),
$display['displayId']
);
$display['layoutId'] = $layoutId;
$displays[] = $display;
}
} else {
$displays = $syncGroup->getGroupMembersForForm();
}
$this->getState()->setData([
'displays' => $displays
]);
return $this->render($request, $response);
}
}

766
lib/Controller/Tag.php Normal file
View File

@@ -0,0 +1,766 @@
<?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\Controller;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Event\DisplayGroupLoadEvent;
use Xibo\Event\TagAddEvent;
use Xibo\Event\TagDeleteEvent;
use Xibo\Event\TagEditEvent;
use Xibo\Event\TriggerTaskEvent;
use Xibo\Factory\CampaignFactory;
use Xibo\Factory\DisplayFactory;
use Xibo\Factory\DisplayGroupFactory;
use Xibo\Factory\LayoutFactory;
use Xibo\Factory\MediaFactory;
use Xibo\Factory\PlaylistFactory;
use Xibo\Factory\ScheduleFactory;
use Xibo\Factory\TagFactory;
use Xibo\Factory\UserFactory;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
/**
* Class Tag
* @package Xibo\Controller
*/
class Tag extends Base
{
/** @var CampaignFactory */
private $campaignFactory;
/**
* @var DisplayFactory
*/
private $displayFactory;
/**
* @var DisplayGroupFactory
*/
private $displayGroupFactory;
/**
* @var LayoutFactory
*/
private $layoutFactory;
/**
* @var MediaFactory
*/
private $mediaFactory;
/** @var PlaylistFactory */
private $playlistFactory;
/**
* @var ScheduleFactory
*/
private $scheduleFactory;
/**
* @var TagFactory
*/
private $tagFactory;
/** @var UserFactory */
private $userFactory;
/**
* Set common dependencies.
* @param DisplayGroupFactory $displayGroupFactory
* @param LayoutFactory $layoutFactory
* @param TagFactory $tagFactory
* @param UserFactory $userFactory
* @param DisplayFactory $displayFactory
* @param MediaFactory $mediaFactory
* @param ScheduleFactory $scheduleFactory
* @param CampaignFactory $campaignFactory
* @param PlaylistFactory $playlistFactory
*/
public function __construct($displayGroupFactory, $layoutFactory, $tagFactory, $userFactory, $displayFactory, $mediaFactory, $scheduleFactory, $campaignFactory, $playlistFactory)
{
$this->displayGroupFactory = $displayGroupFactory;
$this->layoutFactory = $layoutFactory;
$this->tagFactory = $tagFactory;
$this->userFactory = $userFactory;
$this->displayFactory = $displayFactory;
$this->mediaFactory = $mediaFactory;
$this->scheduleFactory = $scheduleFactory;
$this->campaignFactory = $campaignFactory;
$this->playlistFactory = $playlistFactory;
}
/**
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function displayPage(Request $request, Response $response)
{
$this->getState()->template = 'tag-page';
$this->getState()->setData([
'users' => $this->userFactory->query()
]);
return $this->render($request, $response);
}
/**
* Tag Search
*
* @SWG\Get(
* path="/tag",
* operationId="tagSearch",
* tags={"tags"},
* summary="Search Tags",
* description="Search for Tags viewable by this user",
* @SWG\Parameter(
* name="tagId",
* in="query",
* description="Filter by Tag Id",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="tag",
* in="query",
* description="Filter by partial Tag",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="exactTag",
* in="query",
* description="Filter by exact Tag",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="isSystem",
* in="query",
* description="Filter by isSystem flag",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="isRequired",
* in="query",
* description="Filter by isRequired flag",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="haveOptions",
* in="query",
* description="Set to 1 to show only results that have options set",
* type="integer",
* required=false
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="#/definitions/Tag")
* )
* )
* )
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
function grid(Request $request, Response $response)
{
$sanitizedQueryParams = $this->getSanitizer($request->getQueryParams());
$filter = [
'tagId' => $sanitizedQueryParams->getInt('tagId'),
'tag' => $sanitizedQueryParams->getString('tag'),
'useRegexForName' => $sanitizedQueryParams->getCheckbox('useRegexForName'),
'isSystem' => $sanitizedQueryParams->getCheckbox('isSystem'),
'isRequired' => $sanitizedQueryParams->getCheckbox('isRequired'),
'haveOptions' => $sanitizedQueryParams->getCheckbox('haveOptions'),
'allTags' => $sanitizedQueryParams->getInt('allTags'),
'logicalOperatorName' => $sanitizedQueryParams->getString('logicalOperatorName'),
];
$tags = $this->tagFactory->query($this->gridRenderSort($sanitizedQueryParams), $this->gridRenderFilter($filter, $sanitizedQueryParams));
foreach ($tags as $tag) {
/* @var \Xibo\Entity\Tag $tag */
if ($this->isApi($request)) {
continue;
}
$tag->includeProperty('buttons');
$tag->buttons = [];
//Show buttons for non system tags
if ($tag->isSystem === 0) {
// Edit the Tag
$tag->buttons[] = [
'id' => 'tag_button_edit',
'url' => $this->urlFor($request,'tag.edit.form', ['id' => $tag->tagId]),
'text' => __('Edit')
];
// Delete Tag
$tag->buttons[] = [
'id' => 'tag_button_delete',
'url' => $this->urlFor($request,'tag.delete.form', ['id' => $tag->tagId]),
'text' => __('Delete'),
'multi-select' => true,
'dataAttributes' => [
['name' => 'commit-url', 'value' => $this->urlFor($request,'tag.delete', ['id' => $tag->tagId])],
['name' => 'commit-method', 'value' => 'delete'],
['name' => 'id', 'value' => 'tag_button_delete'],
['name' => 'text', 'value' => __('Delete')],
['name' => 'sort-group', 'value' => 1],
['name' => 'rowtitle', 'value' => $tag->tag]
]
];
}
$tag->buttons[] = [
'id' => 'tag_button_usage',
'url' => $this->urlFor($request, 'tag.usage.form', ['id' => $tag->tagId]),
'text' => __('Usage')
];
}
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = $this->tagFactory->countLast();
$this->getState()->setData($tags);
return $this->render($request, $response);
}
/**
* Tag Add Form
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function addForm(Request $request, Response $response)
{
$this->getState()->template = 'tag-form-add';
return $this->render($request, $response);
}
/**
* Add a Tag
*
* @SWG\Post(
* path="/tag",
* operationId="tagAdd",
* tags={"tags"},
* summary="Add a new Tag",
* description="Add a new Tag",
* @SWG\Parameter(
* name="name",
* in="formData",
* description="Tag name",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="isRequired",
* in="formData",
* description="A flag indicating whether value selection on assignment is required",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="options",
* in="formData",
* description="A comma separated string of Tag options",
* type="string",
* required=false
* ),
* @SWG\Response(
* response=201,
* description="successful operation",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="#/definitions/Tag")
* )
* )
* )
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\DuplicateEntityException
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\InvalidArgumentException
*/
public function add(Request $request, Response $response)
{
if (!$this->getUser()->isSuperAdmin()) {
throw new AccessDeniedException();
}
$sanitizedParams = $this->getSanitizer($request->getParams());
$values = [];
$tag = $this->tagFactory->create($sanitizedParams->getString('name'));
$tag->options = [];
$tag->isRequired = $sanitizedParams->getCheckbox('isRequired');
$optionValues = $sanitizedParams->getString('options');
if ($optionValues != '') {
$optionValuesArray = explode(',', $optionValues);
foreach ($optionValuesArray as $options) {
$values[] = $options;
}
$tag->options = json_encode($values);
} else {
$tag->options = null;
}
$tag->save();
// dispatch Tag add event
$event = new TagAddEvent($tag->tagId);
$this->getDispatcher()->dispatch($event, $event::$NAME);
// Return
$this->getState()->hydrate([
'httpStatus' => 201,
'message' => sprintf(__('Added %s'), $tag->tag),
'id' => $tag->tagId,
'data' => $tag
]);
return $this->render($request, $response);
}
/**
* Edit a Tag
*
* @SWG\Put(
* path="/tag/{tagId}",
* operationId="tagEdit",
* tags={"tags"},
* summary="Edit existing Tag",
* description="Edit existing Tag",
* @SWG\Parameter(
* name="name",
* in="formData",
* description="Tag name",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="isRequired",
* in="formData",
* description="A flag indicating whether value selection on assignment is required",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="options",
* in="formData",
* description="A comma separated string of Tag options",
* type="string",
* required=false
* ),
* @SWG\Response(
* response=201,
* description="successful operation",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="#/definitions/Tag")
* )
* )
* )
*
*
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function editForm(Request $request, Response $response, $id)
{
$tag = $this->tagFactory->getById($id);
$tagOptions = '';
if (isset($tag->options)) {
$tagOptions = implode(',', json_decode($tag->options));
}
$this->getState()->template = 'tag-form-edit';
$this->getState()->setData([
'tag' => $tag,
'options' => $tagOptions,
]);
return $this->render($request, $response);
}
public function usageForm(Request $request, Response $response, $id)
{
$tag = $this->tagFactory->getById($id);
$this->getState()->template = 'tag-usage-form';
$this->getState()->setData([
'tag' => $tag
]);
return $this->render($request, $response);
}
public function usage(Request $request, Response $response, $id)
{
$sanitizedParams = $this->getSanitizer($request->getParams());
$sanitizedQueryParams = $this->getSanitizer($request->getQueryParams());
$filter = [
'tagId' => $id,
];
$entries = $this->tagFactory->getAllLinks(
$this->gridRenderSort($sanitizedParams),
$this->gridRenderFilter($filter, $sanitizedQueryParams)
);
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = $this->tagFactory->countLast();
$this->getState()->setData($entries);
return $this->render($request, $response);
}
/**
* Edit a Tag
*
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\DuplicateEntityException
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\InvalidArgumentException
*/
public function edit(Request $request, Response $response, $id)
{
if (!$this->getUser()->isSuperAdmin()) {
throw new AccessDeniedException();
}
$sanitizedParams = $this->getSanitizer($request->getParams());
$tag = $this->tagFactory->getById($id);
if ($tag->isSystem === 1) {
throw new AccessDeniedException(__('Access denied System tags cannot be edited'));
}
if(isset($tag->options)) {
$tagOptionsCurrent = implode(',', json_decode($tag->options));
$tagOptionsArrayCurrent = explode(',', $tagOptionsCurrent);
}
$values = [];
$oldTag = $tag->tag;
$tag->tag = $sanitizedParams->getString('name');
$tag->isRequired = $sanitizedParams->getCheckbox('isRequired');
$optionValues = $sanitizedParams->getString('options');
if ($optionValues != '') {
$optionValuesArray = explode(',', $optionValues);
foreach ($optionValuesArray as $option) {
$values[] = trim($option);
}
$tag->options = json_encode($values);
} else {
$tag->options = null;
}
// if option were changed, we need to compare the array of options before and after edit
if($tag->hasPropertyChanged('options')) {
if (isset($tagOptionsArrayCurrent)) {
if(isset($tag->options)) {
$tagOptions = implode(',', json_decode($tag->options));
$tagOptionsArray = explode(',', $tagOptions);
} else {
$tagOptionsArray = [];
}
// compare array of options before and after the Tag edit was made
$tagValuesToRemove = array_diff($tagOptionsArrayCurrent, $tagOptionsArray);
// go through every element of the new array and set the value to null if removed value was assigned to one of the lktag tables
$tag->updateTagValues($tagValuesToRemove);
}
}
$tag->save();
// dispatch Tag edit event
$event = new TagEditEvent($tag->tagId, $oldTag, $tag->tag);
$this->getDispatcher()->dispatch($event, $event::$NAME);
// Return
$this->getState()->hydrate([
'httpStatus' => 201,
'message' => sprintf(__('Edited %s'), $tag->tag),
'id' => $tag->tagId,
'data' => $tag
]);
return $this->render($request,$response);
}
/**
* Shows the Delete Group Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
function deleteForm(Request $request, Response $response, $id)
{
$tag = $this->tagFactory->getById($id);
$this->getState()->template = 'tag-form-delete';
$this->getState()->setData([
'tag' => $tag,
]);
return $this->render($request, $response);
}
/**
* Delete Tag
*
* @SWG\Delete(
* path="/tag/{tagId}",
* operationId="tagDelete",
* tags={"tags"},
* summary="Delete Tag",
* description="Delete a Tag",
* @SWG\Parameter(
* name="tagId",
* in="path",
* description="The Tag ID to delete",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ConfigurationException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\DuplicateEntityException
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\InvalidArgumentException
*/
public function delete(Request $request, Response $response, $id)
{
if (!$this->getUser()->isSuperAdmin()) {
throw new AccessDeniedException();
}
$tag = $this->tagFactory->getById($id);
if ($tag->isSystem === 1) {
throw new AccessDeniedException(__('Access denied System tags cannot be deleted'));
}
// Dispatch delete event, remove this tag links in all lktag tables.
$event = new TagDeleteEvent($tag->tagId);
$this->getDispatcher()->dispatch($event, $event::$NAME);
// tag delete, remove the record from tag table
$tag->delete();
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Deleted %s'), $tag->tag)
]);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function loadTagOptions(Request $request, Response $response)
{
$tagName = $this->getSanitizer($request->getParams())->getString('name');
try {
$tag = $this->tagFactory->getByTag($tagName);
} catch (NotFoundException $e) {
// User provided new tag, which is fine
$tag = null;
}
$this->getState()->setData([
'tag' => ($tag === null) ? null : $tag
]);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ConfigurationException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\DuplicateEntityException
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\InvalidArgumentException
*/
public function editMultiple(Request $request, Response $response)
{
// Handle permissions
if (!$this->getUser()->featureEnabled('tag.tagging')) {
throw new AccessDeniedException();
}
$sanitizedParams = $this->getSanitizer($request->getParams());
$targetType = $sanitizedParams->getString('targetType');
$targetIds = $sanitizedParams->getString('targetIds');
$tagsToAdd = $sanitizedParams->getString('addTags');
$tagsToRemove = $sanitizedParams->getString('removeTags');
// check if we need to do anything first
if ($tagsToAdd != '' || $tagsToRemove != '') {
// covert comma separated string of ids into array
$targetIdsArray = explode(',', $targetIds);
// get tags to assign and unassign
$tags = $this->tagFactory->tagsFromString($tagsToAdd);
$untags = $this->tagFactory->tagsFromString($tagsToRemove);
// depending on the type we need different factory
switch ($targetType){
case 'layout':
$entityFactory = $this->layoutFactory;
break;
case 'playlist':
$entityFactory = $this->playlistFactory;
break;
case 'media':
$entityFactory = $this->mediaFactory;
break;
case 'campaign':
$entityFactory = $this->campaignFactory;
break;
case 'displayGroup':
case 'display':
$entityFactory = $this->displayGroupFactory;
break;
default:
throw new InvalidArgumentException(__('Edit multiple tags is not supported on this item'), 'targetType');
}
foreach ($targetIdsArray as $id) {
// get the entity by provided id, for display we need different function
$this->getLog()->debug('editMultiple: lookup using id: ' . $id . ' for type: ' . $targetType);
if ($targetType === 'display') {
$entity = $entityFactory->getDisplaySpecificByDisplayId($id);
} else {
$entity = $entityFactory->getById($id);
}
if ($targetType === 'display' || $targetType === 'displaygroup') {
$this->getDispatcher()->dispatch(new DisplayGroupLoadEvent($entity), DisplayGroupLoadEvent::$NAME);
}
foreach ($untags as $untag) {
$entity->unassignTag($untag);
}
// go through tags and adjust assignments.
foreach ($tags as $tag) {
$entity->assignTag($tag);
}
$entity->save(['isTagEdit' => true]);
}
// Once we're done, and if we're a Display entity, we need to calculate the dynamic display groups
if ($targetType === 'display') {
// Background update.
$this->getDispatcher()->dispatch(
new TriggerTaskEvent('\Xibo\XTR\MaintenanceRegularTask', 'DYNAMIC_DISPLAY_GROUP_ASSESSED'),
TriggerTaskEvent::$NAME
);
}
} else {
$this->getLog()->debug('Tags were not changed');
}
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => __('Tags Edited')
]);
return $this->render($request, $response);
}
}

621
lib/Controller/Task.php Normal file
View File

@@ -0,0 +1,621 @@
<?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\Controller;
use Carbon\Carbon;
use Cron\CronExpression;
use Illuminate\Support\Str;
use Psr\Container\ContainerInterface;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Stash\Interfaces\PoolInterface;
use Xibo\Factory\TaskFactory;
use Xibo\Helper\DateFormatHelper;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Storage\TimeSeriesStoreInterface;
use Xibo\Support\Exception\NotFoundException;
use Xibo\XTR\TaskInterface;
/**
* Class Task
* @package Xibo\Controller
*/
class Task extends Base
{
/** @var TaskFactory */
private $taskFactory;
/** @var StorageServiceInterface */
private $store;
/** @var TimeSeriesStoreInterface */
private $timeSeriesStore;
/** @var PoolInterface */
private $pool;
/** ContainerInterface */
private $container;
/**
* Set common dependencies.
* @param StorageServiceInterface $store
* @param TimeSeriesStoreInterface $timeSeriesStore
* @param PoolInterface $pool
* @param TaskFactory $taskFactory
* @param ContainerInterface $container
*/
public function __construct($store, $timeSeriesStore, $pool, $taskFactory, ContainerInterface $container)
{
$this->taskFactory = $taskFactory;
$this->store = $store;
$this->timeSeriesStore = $timeSeriesStore;
$this->pool = $pool;
$this->container = $container;
}
/**
* Display Page
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function displayPage(Request $request, Response $response)
{
$this->getState()->template = 'task-page';
return $this->render($request, $response);
}
/**
* Grid
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function grid(Request $request, Response $response)
{
$sanitizedParams = $this->getSanitizer($request->getParams());
$tasks = $this->taskFactory->query(
$this->gridRenderSort($sanitizedParams),
$this->gridRenderFilter([], $sanitizedParams)
);
foreach ($tasks as $task) {
/** @var \Xibo\Entity\Task $task */
$task->setUnmatchedProperty('nextRunDt', $task->nextRunDate());
if ($this->isApi($request)) {
continue;
}
$task->includeProperty('buttons');
$task->buttons[] = array(
'id' => 'task_button_run.now',
'url' => $this->urlFor($request, 'task.runNow.form', ['id' => $task->taskId]),
'text' => __('Run Now'),
'dataAttributes' => [
['name' => 'auto-submit', 'value' => true],
[
'name' => 'commit-url',
'value' => $this->urlFor($request, 'task.runNow', ['id' => $task->taskId]),
],
['name' => 'commit-method', 'value' => 'POST']
]
);
// Don't show any edit buttons if the config is locked.
if ($this->getConfig()->getSetting('TASK_CONFIG_LOCKED_CHECKB') == 1
|| $this->getConfig()->getSetting('TASK_CONFIG_LOCKED_CHECKB') == 'Checked'
) {
continue;
}
// Edit Button
$task->buttons[] = array(
'id' => 'task_button_edit',
'url' => $this->urlFor($request, 'task.edit.form', ['id' => $task->taskId]),
'text' => __('Edit')
);
// Delete Button
$task->buttons[] = array(
'id' => 'task_button_delete',
'url' => $this->urlFor($request, 'task.delete.form', ['id' => $task->taskId]),
'text' => __('Delete')
);
}
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = $this->taskFactory->countLast();
$this->getState()->setData($tasks);
return $this->render($request, $response);
}
/**
* Add form
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function addForm(Request $request, Response $response)
{
// Provide a list of possible task classes by searching for .task file in /tasks and /custom
$data = ['tasksAvailable' => []];
// Do we have any modules to install?!
if ($this->getConfig()->getSetting('TASK_CONFIG_LOCKED_CHECKB') != 1 && $this->getConfig()->getSetting('TASK_CONFIG_LOCKED_CHECKB') != 'Checked') {
// Get a list of matching files in the modules folder
$files = array_merge(glob(PROJECT_ROOT . '/tasks/*.task'), glob(PROJECT_ROOT . '/custom/*.task'));
// Add to the list of available tasks
foreach ($files as $file) {
$config = json_decode(file_get_contents($file));
$config->file = Str::replaceFirst(PROJECT_ROOT, '', $file);
$data['tasksAvailable'][] = $config;
}
}
$this->getState()->template = 'task-form-add';
$this->getState()->setData($data);
return $this->render($request, $response);
}
/**
* Add
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\InvalidArgumentException
*/
public function add(Request $request, Response $response)
{
$sanitizedParams = $this->getSanitizer($request->getParams());
$task = $this->taskFactory->create();
$task->name = $sanitizedParams->getString('name');
$task->configFile = $sanitizedParams->getString('file');
$task->schedule = $sanitizedParams->getString('schedule');
$task->status = \Xibo\Entity\Task::$STATUS_IDLE;
$task->lastRunStatus = 0;
$task->isActive = 0;
$task->runNow = 0;
$task->setClassAndOptions();
$task->save();
// Return
$this->getState()->hydrate([
'httpStatus' => 201,
'message' => sprintf(__('Added %s'), $task->name),
'id' => $task->taskId,
'data' => $task
]);
return $this->render($request, $response);
}
/**
* Edit Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function editForm(Request $request, Response $response, $id)
{
$task = $this->taskFactory->getById($id);
$task->setClassAndOptions();
$this->getState()->template = 'task-form-edit';
$this->getState()->setData([
'task' => $task
]);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\InvalidArgumentException
*/
public function edit(Request $request, Response $response, $id)
{
$task = $this->taskFactory->getById($id);
$sanitizedParams = $this->getSanitizer($request->getParams());
$task->setClassAndOptions();
$task->name = $sanitizedParams->getString('name');
$task->schedule = $sanitizedParams->getString('schedule');
$task->isActive = $sanitizedParams->getCheckbox('isActive');
// Loop through each option and see if a new value is provided
foreach ($task->options as $option => $value) {
$provided = $sanitizedParams->getString($option);
if ($provided !== null) {
$this->getLog()->debug('Setting ' . $option . ' to ' . $provided);
$task->options[$option] = $provided;
}
}
$this->getLog()->debug('New options = ' . var_export($task->options, true));
$task->save();
// Return
$this->getState()->hydrate([
'httpStatus' => 200,
'message' => sprintf(__('Edited %s'), $task->name),
'id' => $task->taskId,
'data' => $task
]);
return $this->render($request, $response);
}
/**
* Delete Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function deleteForm(Request $request, Response $response, $id)
{
$task = $this->taskFactory->getById($id);
$this->getState()->template = 'task-form-delete';
$this->getState()->setData([
'task' => $task
]);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function delete(Request $request, Response $response, $id)
{
$task = $this->taskFactory->getById($id);
$task->delete();
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Deleted %s'), $task->name)
]);
return $this->render($request, $response);
}
/**
* Delete Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function runNowForm(Request $request, Response $response, $id)
{
$task = $this->taskFactory->getById($id);
$this->getState()->template = 'task-form-run-now';
$this->getState()->autoSubmit = $this->getAutoSubmit('taskRunNowForm');
$this->getState()->setData([
'task' => $task
]);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\InvalidArgumentException
*/
public function runNow(Request $request, Response $response, $id)
{
$task = $this->taskFactory->getById($id);
$task->runNow = 1;
$task->save();
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Run Now set on %s'), $task->name)
]);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\InvalidArgumentException
*/
public function run(Request $request, Response $response, $id)
{
// Get this task
if (is_numeric($id)) {
$task = $this->taskFactory->getById($id);
} else {
$task = $this->taskFactory->getByName($id);
}
// Set to running
$this->getLog()->debug('run: Running Task ' . $task->name
. ' [' . $task->taskId . '], Class = ' . $task->class);
// Run
$task->setStarted();
try {
// Instantiate
if (!class_exists($task->class)) {
throw new NotFoundException(sprintf(__('Task with class name %s not found'), $task->class));
}
/** @var TaskInterface $taskClass */
$taskClass = new $task->class();
// Record the start time
$start = Carbon::now()->format('U');
$taskClass
->setSanitizer($this->getSanitizer($request->getParams()))
->setUser($this->getUser())
->setConfig($this->getConfig())
->setLogger($this->getLog())
->setPool($this->pool)
->setStore($this->store)
->setTimeSeriesStore($this->timeSeriesStore)
->setDispatcher($this->getDispatcher())
->setFactories($this->container)
->setTask($task)
->run();
// We should commit anything this task has done
$this->store->commitIfNecessary();
// Collect results
$task->lastRunDuration = Carbon::now()->format('U') - $start;
$task->lastRunMessage = $taskClass->getRunMessage();
$task->lastRunStatus = \Xibo\Entity\Task::$STATUS_SUCCESS;
$task->lastRunExitCode = 0;
} catch (\Exception $e) {
$this->getLog()->error('run: ' . $e->getMessage() . ' Exception Type: ' . get_class($e));
$this->getLog()->debug($e->getTraceAsString());
// We should roll back anything we've done so far
if ($this->store->getConnection()->inTransaction()) {
$this->store->getConnection()->rollBack();
}
// Set the results to error
$task->lastRunMessage = $e->getMessage();
$task->lastRunStatus = \Xibo\Entity\Task::$STATUS_ERROR;
$task->lastRunExitCode = 1;
}
$task->lastRunDt = Carbon::now()->format('U');
$task->runNow = 0;
$task->status = \Xibo\Entity\Task::$STATUS_IDLE;
// lastRunMessage columns has a limit of 254 characters, if the message is longer, we need to truncate it.
if (strlen($task->lastRunMessage) >= 255) {
$task->lastRunMessage = substr($task->lastRunMessage, 0, 249) . '(...)';
}
// Finished
$task->setFinished();
$this->getLog()->debug('run: Finished Task ' . $task->name . ' [' . $task->taskId . '] Run Dt: '
. Carbon::now()->format(DateFormatHelper::getSystemFormat()));
$this->setNoOutput();
return $this->render($request, $response);
}
/**
* Poll for tasks to run
* continue polling until there aren't anymore to run
* allow for multiple polls to run at the same time
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function poll(Request $request, Response $response)
{
$this->getLog()->debug('poll: XTR poll started');
// Process timeouts
$this->pollProcessTimeouts();
// Keep track of tasks we've run during this poll period
// we will use this as a catch-all so that we do not run a task more than once.
$tasksRun = [];
// We loop until we have gone through without running a task
// each loop we are expecting to run ONE task only, to allow for multiple runs of XTR at the
// same time.
while (true) {
// Get tasks that aren't running currently
// we have to get them all here because we can't calculate the CRON schedule with SQL,
// therefore we return them all and process one and a time.
$tasks = $this->store->select('
SELECT taskId, `schedule`, runNow, lastRunDt
FROM `task`
WHERE isActive = 1
AND `status` <> :status
ORDER BY lastRunDuration
', ['status' => \Xibo\Entity\Task::$STATUS_RUNNING], 'xtr', true);
// Assume we won't run anything
$taskRun = false;
foreach ($tasks as $task) {
/** @var \Xibo\Entity\Task $task */
$taskId = $task['taskId'];
// Skip tasks that have already been run
if (in_array($taskId, $tasksRun)) {
continue;
}
try {
$cron = new CronExpression($task['schedule']);
} catch (\Exception $e) {
$this->getLog()->info('run: CRON syntax error for taskId ' . $taskId
. ', e: ' . $e->getMessage());
// Try and take the first X characters instead.
try {
$cron = new CronExpression(substr($task['schedule'], 0, strlen($task['schedule']) - 2));
} catch (\Exception) {
$this->getLog()->error('run: cannot fix CRON syntax error ' . $taskId);
continue;
}
}
// Is the next run date of this event earlier than now, or is the task set to runNow
$nextRunDt = $cron->getNextRunDate(\DateTime::createFromFormat('U', $task['lastRunDt']))
->format('U');
if ($task['runNow'] == 1 || $nextRunDt <= Carbon::now()->format('U')) {
$this->getLog()->info('poll: Running Task ' . $taskId);
try {
// Pass to run.
$this->run($request, $response, $taskId);
} catch (\Exception $exception) {
// The only thing which can fail inside run is core code,
// so it is reasonable here to disable the task.
$this->getLog()->error('poll: Task run error for taskId ' . $taskId
. '. E = ' . $exception->getMessage());
$this->getLog()->debug($exception->getTraceAsString());
// Set to error and disable.
$this->store->update('
UPDATE `task` SET status = :status, isActive = :isActive, lastRunMessage = :lastRunMessage
WHERE taskId = :taskId
', [
'taskId' => $taskId,
'status' => \Xibo\Entity\Task::$STATUS_ERROR,
'isActive' => 0,
'lastRunMessage' => 'Fatal Error: ' . $exception->getMessage()
], 'xtr', true, false);
}
// We have run a task
$taskRun = true;
// We've run this task during this polling period
$tasksRun[] = $taskId;
// As mentioned above, we only run 1 task at a time to allow for concurrent runs of XTR.
break;
}
}
// If we haven't run a task, then stop
if (!$taskRun) {
break;
}
}
$this->getLog()->debug('XTR poll stopped');
$this->setNoOutput();
return $this->render($request, $response);
}
private function pollProcessTimeouts()
{
$count = $this->store->update('
UPDATE `task` SET `status` = :newStatus
WHERE `isActive` = 1
AND `status` = :currentStatus
AND `lastRunStartDt` < :timeout
', [
'timeout' => Carbon::now()->subHours(12)->format('U'),
'currentStatus' => \Xibo\Entity\Task::$STATUS_RUNNING,
'newStatus' => \Xibo\Entity\Task::$STATUS_TIMEOUT,
], 'xtr', false, false);
if ($count > 0) {
$this->getLog()->error($count . ' timed out tasks.');
} else {
$this->getLog()->debug('No timed out tasks.');
}
}
}

806
lib/Controller/Template.php Normal file
View File

@@ -0,0 +1,806 @@
<?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\Controller;
use Parsedown;
use Psr\Http\Message\ResponseInterface;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Entity\SearchResult;
use Xibo\Entity\SearchResults;
use Xibo\Event\TemplateProviderEvent;
use Xibo\Event\TemplateProviderListEvent;
use Xibo\Factory\LayoutFactory;
use Xibo\Factory\TagFactory;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
/**
* Class Template
* @package Xibo\Controller
*/
class Template extends Base
{
/**
* @var LayoutFactory
*/
private $layoutFactory;
/**
* @var TagFactory
*/
private $tagFactory;
/**
* @var \Xibo\Factory\ResolutionFactory
*/
private $resolutionFactory;
/**
* Set common dependencies.
* @param LayoutFactory $layoutFactory
* @param TagFactory $tagFactory
* @param \Xibo\Factory\ResolutionFactory $resolutionFactory
*/
public function __construct($layoutFactory, $tagFactory, $resolutionFactory)
{
$this->layoutFactory = $layoutFactory;
$this->tagFactory = $tagFactory;
$this->resolutionFactory = $resolutionFactory;
}
/**
* Display page logic
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
function displayPage(Request $request, Response $response)
{
// Call to render the template
$this->getState()->template = 'template-page';
return $this->render($request, $response);
}
/**
* Data grid
*
* @SWG\Get(
* path="/template",
* operationId="templateSearch",
* tags={"template"},
* summary="Template Search",
* description="Search templates this user has access to",
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="#/definitions/Layout")
* )
* )
* )
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function grid(Request $request, Response $response)
{
$sanitizedQueryParams = $this->getSanitizer($request->getQueryParams());
// Embed?
$embed = ($sanitizedQueryParams->getString('embed') != null)
? explode(',', $sanitizedQueryParams->getString('embed'))
: [];
$templates = $this->layoutFactory->query($this->gridRenderSort($sanitizedQueryParams), $this->gridRenderFilter([
'excludeTemplates' => 0,
'tags' => $sanitizedQueryParams->getString('tags'),
'layoutId' => $sanitizedQueryParams->getInt('templateId'),
'layout' => $sanitizedQueryParams->getString('template'),
'useRegexForName' => $sanitizedQueryParams->getCheckbox('useRegexForName'),
'folderId' => $sanitizedQueryParams->getInt('folderId'),
'logicalOperator' => $sanitizedQueryParams->getString('logicalOperator'),
'logicalOperatorName' => $sanitizedQueryParams->getString('logicalOperatorName'),
], $sanitizedQueryParams));
foreach ($templates as $template) {
/* @var \Xibo\Entity\Layout $template */
if (in_array('regions', $embed)) {
$template->load([
'loadPlaylists' => in_array('playlists', $embed),
'loadCampaigns' => in_array('campaigns', $embed),
'loadPermissions' => in_array('permissions', $embed),
'loadTags' => in_array('tags', $embed),
'loadWidgets' => in_array('widgets', $embed)
]);
}
if ($this->isApi($request)) {
continue;
}
$template->includeProperty('buttons');
// Thumbnail
$template->setUnmatchedProperty('thumbnail', '');
if (file_exists($template->getThumbnailUri())) {
$template->setUnmatchedProperty(
'thumbnail',
$this->urlFor($request, 'layout.download.thumbnail', ['id' => $template->layoutId])
);
}
// Parse down for description
$template->setUnmatchedProperty(
'descriptionWithMarkup',
Parsedown::instance()->setSafeMode(true)->text($template->description),
);
if ($this->getUser()->featureEnabled('template.modify')
&& $this->getUser()->checkEditable($template)
) {
// Design Button
$template->buttons[] = [
'id' => 'layout_button_design',
'linkType' => '_self', 'external' => true,
'url' => $this->urlFor(
$request,
'layout.designer',
['id' => $template->layoutId]
) . '?isTemplateEditor=1',
'text' => __('Alter Template')
];
if ($template->isEditable()) {
$template->buttons[] = ['divider' => true];
$template->buttons[] = array(
'id' => 'layout_button_publish',
'url' => $this->urlFor($request, 'layout.publish.form', ['id' => $template->layoutId]),
'text' => __('Publish')
);
$template->buttons[] = array(
'id' => 'layout_button_discard',
'url' => $this->urlFor($request, 'layout.discard.form', ['id' => $template->layoutId]),
'text' => __('Discard')
);
$template->buttons[] = ['divider' => true];
} else {
$template->buttons[] = ['divider' => true];
// Checkout Button
$template->buttons[] = array(
'id' => 'layout_button_checkout',
'url' => $this->urlFor($request, 'layout.checkout.form', ['id' => $template->layoutId]),
'text' => __('Checkout'),
'dataAttributes' => [
['name' => 'auto-submit', 'value' => true],
[
'name' => 'commit-url',
'value' => $this->urlFor(
$request,
'layout.checkout',
['id' => $template->layoutId]
)
],
['name' => 'commit-method', 'value' => 'PUT']
]
);
$template->buttons[] = ['divider' => true];
}
// Edit Button
$template->buttons[] = array(
'id' => 'layout_button_edit',
'url' => $this->urlFor($request, 'template.edit.form', ['id' => $template->layoutId]),
'text' => __('Edit')
);
// Select Folder
if ($this->getUser()->featureEnabled('folder.view')) {
$template->buttons[] = [
'id' => 'campaign_button_selectfolder',
'url' => $this->urlFor($request, 'campaign.selectfolder.form', ['id' => $template->campaignId]),
'text' => __('Select Folder'),
'multi-select' => true,
'dataAttributes' => [
[
'name' => 'commit-url',
'value' => $this->urlFor(
$request,
'campaign.selectfolder',
['id' => $template->campaignId]
)
],
['name' => 'commit-method', 'value' => 'put'],
['name' => 'id', 'value' => 'campaign_button_selectfolder'],
['name' => 'text', 'value' => __('Move to Folder')],
['name' => 'rowtitle', 'value' => $template->layout],
['name' => 'form-callback', 'value' => 'moveFolderMultiSelectFormOpen']
]
];
}
// Copy Button
$template->buttons[] = array(
'id' => 'layout_button_copy',
'url' => $this->urlFor($request, 'layout.copy.form', ['id' => $template->layoutId]),
'text' => __('Copy')
);
}
// Extra buttons if have delete permissions
if ($this->getUser()->featureEnabled('template.modify')
&& $this->getUser()->checkDeleteable($template)) {
// Delete Button
$template->buttons[] = [
'id' => 'layout_button_delete',
'url' => $this->urlFor($request, 'layout.delete.form', ['id' => $template->layoutId]),
'text' => __('Delete'),
'multi-select' => true,
'dataAttributes' => [
[
'name' => 'commit-url',
'value' => $this->urlFor(
$request,
'layout.delete',
['id' => $template->layoutId]
)
],
['name' => 'commit-method', 'value' => 'delete'],
['name' => 'id', 'value' => 'layout_button_delete'],
['name' => 'text', 'value' => __('Delete')],
['name' => 'sort-group', 'value' => 1],
['name' => 'rowtitle', 'value' => $template->layout]
]
];
}
$template->buttons[] = ['divider' => true];
// Extra buttons if we have modify permissions
if ($this->getUser()->featureEnabled('template.modify')
&& $this->getUser()->checkPermissionsModifyable($template)) {
// Permissions button
$template->buttons[] = [
'id' => 'layout_button_permissions',
'url' => $this->urlFor(
$request,
'user.permissions.form',
['entity' => 'Campaign', 'id' => $template->campaignId]
) . '?nameOverride=' . __('Template'),
'text' => __('Share'),
'multi-select' => true,
'dataAttributes' => [
[
'name' => 'commit-url',
'value' => $this->urlFor(
$request,
'user.permissions.multi',
['entity' => 'Campaign', 'id' => $template->campaignId]
)
],
['name' => 'commit-method', 'value' => 'post'],
['name' => 'id', 'value' => 'layout_button_permissions'],
['name' => 'text', 'value' => __('Share')],
['name' => 'rowtitle', 'value' => $template->layout],
['name' => 'sort-group', 'value' => 2],
['name' => 'custom-handler', 'value' => 'XiboMultiSelectPermissionsFormOpen'],
[
'name' => 'custom-handler-url',
'value' => $this->urlFor(
$request,
'user.permissions.multi.form',
['entity' => 'Campaign']
)
],
['name' => 'content-id-name', 'value' => 'campaignId']
]
];
}
if ($this->getUser()->featureEnabled('layout.export')) {
$template->buttons[] = ['divider' => true];
// Export Button
$template->buttons[] = array(
'id' => 'layout_button_export',
'linkType' => '_self',
'external' => true,
'url' => $this->urlFor($request, 'layout.export', ['id' => $template->layoutId]),
'text' => __('Export')
);
}
}
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = $this->layoutFactory->countLast();
$this->getState()->setData($templates);
return $this->render($request, $response);
}
/**
* Data grid
*
* @SWG\Get(
* path="/template/search",
* operationId="templateSearchAll",
* tags={"template"},
* summary="Template Search All",
* description="Search all templates from local and connectors",
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="#/definitions/SearchResult")
* )
* )
* )
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\GeneralException
*/
public function search(Request $request, Response $response)
{
$sanitizedQueryParams = $this->getSanitizer($request->getQueryParams());
$provider = $sanitizedQueryParams->getString('provider', ['default' => 'both']);
$searchResults = new SearchResults();
if ($provider === 'both' || $provider === 'local') {
$templates = $this->layoutFactory->query(['layout'], $this->gridRenderFilter([
'excludeTemplates' => 0,
'layout' => $sanitizedQueryParams->getString('template'),
'folderId' => $sanitizedQueryParams->getInt('folderId'),
'orientation' => $sanitizedQueryParams->getString('orientation', ['defaultOnEmptyString' => true]),
'publishedStatusId' => 1
], $sanitizedQueryParams));
foreach ($templates as $template) {
$searchResult = new SearchResult();
$searchResult->id = $template->layoutId;
$searchResult->source = 'local';
$searchResult->title = $template->layout;
// Handle the description
$searchResult->description = '';
if (!empty($template->description)) {
$searchResult->description = Parsedown::instance()->setSafeMode(true)->line($template->description);
}
$searchResult->orientation = $template->orientation;
$searchResult->width = $template->width;
$searchResult->height = $template->height;
if (!empty($template->tags)) {
foreach ($template->getTags() as $tag) {
if ($tag->tag === 'template') {
continue;
}
$searchResult->tags[] = $tag->tag;
}
}
// Thumbnail
$searchResult->thumbnail = '';
if (file_exists($template->getThumbnailUri())) {
$searchResult->thumbnail = $this->urlFor(
$request,
'layout.download.thumbnail',
['id' => $template->layoutId]
);
}
$searchResults->data[] = $searchResult;
}
}
if ($provider === 'both' || $provider === 'remote') {
// Hand off to any other providers that may want to provide results.
$event = new TemplateProviderEvent(
$searchResults,
$sanitizedQueryParams->getInt('start', ['default' => 0]),
$sanitizedQueryParams->getInt('length', ['default' => 15]),
$sanitizedQueryParams->getString('template'),
$sanitizedQueryParams->getString('orientation'),
);
$this->getLog()->debug('Dispatching event. ' . $event->getName());
try {
$this->getDispatcher()->dispatch($event, $event->getName());
} catch (\Exception $exception) {
$this->getLog()->error('Template search: Exception in dispatched event: ' . $exception->getMessage());
$this->getLog()->debug($exception->getTraceAsString());
}
}
return $response->withJson($searchResults);
}
/**
* Template Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\NotFoundException
*/
function addTemplateForm(Request $request, Response $response, $id)
{
// Get the layout
$layout = $this->layoutFactory->getById($id);
// Check Permissions
if (!$this->getUser()->checkViewable($layout)) {
throw new AccessDeniedException(__('You do not have permissions to view this layout'));
}
$this->getState()->template = 'template-form-add-from-layout';
$this->getState()->setData([
'layout' => $layout,
]);
return $this->render($request, $response);
}
/**
* Add a Template
* @SWG\Post(
* path="/template",
* operationId="templateAdd",
* tags={"template"},
* summary="Add a Template",
* description="Add a new Template to the CMS",
* @SWG\Parameter(
* name="name",
* in="formData",
* description="The layout name",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="description",
* in="formData",
* description="The layout description",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="resolutionId",
* in="formData",
* description="If a Template is not provided, provide the resolutionId for this Layout.",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="returnDraft",
* in="formData",
* description="Should we return the Draft Layout or the Published Layout on Success?",
* type="boolean",
* required=false
* ),
* @SWG\Response(
* response=201,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Layout"),
* @SWG\Header(
* header="Location",
* description="Location of the new record",
* type="string"
* )
* )
* )
*
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
function add(Request $request, Response $response)
{
$sanitizedParams = $this->getSanitizer($request->getParams());
$name = $sanitizedParams->getString('name');
$description = $sanitizedParams->getString('description');
$resolutionId = $sanitizedParams->getInt('resolutionId');
$enableStat = $sanitizedParams->getCheckbox('enableStat');
$autoApplyTransitions = $sanitizedParams->getCheckbox('autoApplyTransitions');
$folderId = $sanitizedParams->getInt('folderId');
if ($folderId === 1) {
$this->checkRootFolderAllowSave();
}
if (empty($folderId) || !$this->getUser()->featureEnabled('folder.view')) {
$folderId = $this->getUser()->homeFolderId;
}
// Tags
if ($this->getUser()->featureEnabled('tag.tagging')) {
$tags = $this->tagFactory->tagsFromString($sanitizedParams->getString('tags'));
} else {
$tags = [];
}
$tags[] = $this->tagFactory->tagFromString('template');
$layout = $this->layoutFactory->createFromResolution($resolutionId,
$this->getUser()->userId,
$name,
$description,
$tags,
null
);
// Set layout enableStat flag
$layout->enableStat = $enableStat;
// Set auto apply transitions flag
$layout->autoApplyTransitions = $autoApplyTransitions;
// Set folderId
$layout->folderId = $folderId;
// Save
$layout->save();
// Automatically checkout the new layout for edit
$layout = $this->layoutFactory->checkoutLayout($layout, $sanitizedParams->getCheckbox('returnDraft'));
// Return
$this->getState()->hydrate([
'httpStatus' => 201,
'message' => sprintf(__('Added %s'), $layout->layout),
'id' => $layout->layoutId,
'data' => $layout
]);
return $this->render($request, $response);
}
/**
* Add template
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\NotFoundException
* @SWG\Post(
* path="/template/{layoutId}",
* operationId="template.add.from.layout",
* tags={"template"},
* summary="Add a template from a Layout",
* description="Use the provided layout as a base for a new template",
* @SWG\Parameter(
* name="layoutId",
* in="path",
* description="The Layout ID",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="includeWidgets",
* in="formData",
* description="Flag indicating whether to include the widgets in the Template",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="name",
* in="formData",
* description="The Template Name",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="tags",
* in="formData",
* description="Comma separated list of Tags for the template",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="description",
* in="formData",
* description="A description of the Template",
* type="string",
* required=false
* ),
* @SWG\Response(
* response=201,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Layout"),
* @SWG\Header(
* header="Location",
* description="Location of the new record",
* type="string"
* )
* )
* )
*/
public function addFromLayout(Request $request, Response $response, $id): Response
{
// Get the layout
$layout = $this->layoutFactory->getById($id);
// Check Permissions
if (!$this->getUser()->checkViewable($layout)) {
throw new AccessDeniedException(__('You do not have permissions to view this layout'));
}
$sanitizedParams = $this->getSanitizer($request->getParams());
// Should the copy include the widgets
$includeWidgets = ($sanitizedParams->getCheckbox('includeWidgets') == 1);
// Load without anything
$layout->load([
'loadPlaylists' => true,
'loadWidgets' => $includeWidgets,
'playlistIncludeRegionAssignments' => false,
'loadTags' => false,
'loadPermissions' => false,
'loadCampaigns' => false
]);
$originalLayout = $layout;
$layout = clone $layout;
$layout->layout = $sanitizedParams->getString('name');
if ($this->getUser()->featureEnabled('tag.tagging')) {
$layout->updateTagLinks($this->tagFactory->tagsFromString($sanitizedParams->getString('tags')));
} else {
$layout->tags = [];
}
$layout->assignTag($this->tagFactory->tagFromString('template'));
$layout->description = $sanitizedParams->getString('description');
$layout->folderId = $sanitizedParams->getInt('folderId');
if ($layout->folderId === 1) {
$this->checkRootFolderAllowSave();
}
// When saving a layout as a template, we should not include the empty canva region as that requires
// a widget to be inside it.
// https://github.com/xibosignage/xibo/issues/3574
if (!$includeWidgets) {
$this->getLog()->debug('addFromLayout: widgets have not been included, checking for empty regions');
$regionsWithWidgets = [];
foreach ($layout->regions as $region) {
if ($region->type === 'canvas') {
$this->getLog()->debug('addFromLayout: Canvas region excluded from export');
} else {
$regionsWithWidgets[] = $region;
}
}
$layout->regions = $regionsWithWidgets;
}
$layout->setOwner($this->getUser()->userId, true);
$layout->save();
if ($includeWidgets) {
// Sub-Playlist
foreach ($layout->regions as $region) {
// Match our original region id to the id in the parent layout
$original = $originalLayout->getRegion($region->getOriginalValue('regionId'));
// Make sure Playlist closure table from the published one are copied over
$original->getPlaylist()->cloneClosureTable($region->getPlaylist()->playlistId);
}
}
// Return
$this->getState()->hydrate([
'httpStatus' => 201,
'message' => sprintf(__('Saved %s'), $layout->layout),
'id' => $layout->layoutId,
'data' => $layout
]);
return $this->render($request, $response);
}
/**
* Displays an Add/Edit form
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
function addForm(Request $request, Response $response)
{
$this->getState()->template = 'template-form-add';
$this->getState()->setData([
'resolutions' => $this->resolutionFactory->query(['resolution']),
]);
return $this->render($request, $response);
}
/**
* Edit form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function editForm(Request $request, Response $response, $id)
{
// Get the layout
$template = $this->layoutFactory->getById($id);
// Check Permissions
if (!$this->getUser()->checkEditable($template)) {
throw new AccessDeniedException();
}
$this->getState()->template = 'template-form-edit';
$this->getState()->setData([
'layout' => $template,
]);
return $this->render($request, $response);
}
/**
* Get list of Template providers with their details.
*
* @param Request $request
* @param Response $response
* @return Response|ResponseInterface
*/
public function providersList(Request $request, Response $response): Response|\Psr\Http\Message\ResponseInterface
{
$event = new TemplateProviderListEvent();
$this->getDispatcher()->dispatch($event, $event->getName());
$providers = $event->getProviders();
return $response->withJson($providers);
}
}

View File

@@ -0,0 +1,165 @@
<?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\Controller;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Factory\TransitionFactory;
use Xibo\Support\Exception\AccessDeniedException;
/**
* Class Transition
* @package Xibo\Controller
*/
class Transition extends Base
{
/**
* @var TransitionFactory
*/
private $transitionFactory;
/**
* Set common dependencies.
* @param TransitionFactory $transitionFactory
*/
public function __construct($transitionFactory)
{
$this->transitionFactory = $transitionFactory;
}
/**
* No display page functionaility
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
function displayPage(Request $request, Response $response)
{
$this->getState()->template = 'transition-page';
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function grid(Request $request, Response $response)
{
$sanitizedQueryParams = $this->getSanitizer($request->getQueryParams());
$filter = [
'transition' => $sanitizedQueryParams->getString('transition'),
'code' => $sanitizedQueryParams->getString('code'),
'availableAsIn' => $sanitizedQueryParams->getInt('availableAsIn'),
'availableAsOut' => $sanitizedQueryParams->getInt('availableAsOut')
];
$transitions = $this->transitionFactory->query($this->gridRenderSort($sanitizedQueryParams), $this->gridRenderFilter($filter, $sanitizedQueryParams));
foreach ($transitions as $transition) {
/* @var \Xibo\Entity\Transition $transition */
// If the module config is not locked, present some buttons
if ($this->getConfig()->getSetting('TRANSITION_CONFIG_LOCKED_CHECKB') != 1 && $this->getConfig()->getSetting('TRANSITION_CONFIG_LOCKED_CHECKB') != 'Checked' ) {
// Edit button
$transition->buttons[] = array(
'id' => 'transition_button_edit',
'url' => $this->urlFor($request,'transition.edit.form', ['id' => $transition->transitionId]),
'text' => __('Edit')
);
}
}
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = $this->transitionFactory->countLast();
$this->getState()->setData($transitions);
return $this->render($request, $response);
}
/**
* Transition Edit Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function editForm(Request $request, Response $response, $id)
{
if ($this->getConfig()->getSetting('TRANSITION_CONFIG_LOCKED_CHECKB') == 1 || $this->getConfig()->getSetting('TRANSITION_CONFIG_LOCKED_CHECKB') == 'Checked') {
throw new AccessDeniedException(__('Transition Config Locked'));
}
$transition = $this->transitionFactory->getById($id);
$this->getState()->template = 'transition-form-edit';
$this->getState()->setData([
'transition' => $transition,
]);
return $this->render($request, $response);
}
/**
* Edit Transition
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function edit(Request $request, Response $response, $id)
{
if ($this->getConfig()->getSetting('TRANSITION_CONFIG_LOCKED_CHECKB') == 1 || $this->getConfig()->getSetting('TRANSITION_CONFIG_LOCKED_CHECKB') == 'Checked') {
throw new AccessDeniedException(__('Transition Config Locked'));
}
$sanitizedParams = $this->getSanitizer($request->getParams());
$transition = $this->transitionFactory->getById($id);
$transition->availableAsIn = $sanitizedParams->getCheckbox('availableAsIn');
$transition->availableAsOut = $sanitizedParams->getCheckbox('availableAsOut');
$transition->save();
$this->getState()->hydrate([
'message' => sprintf(__('Edited %s'), $transition->transition),
'id' => $transition->transitionId,
'data' => $transition
]);
return $this->render($request, $response);
}
}

2567
lib/Controller/User.php Normal file

File diff suppressed because it is too large Load Diff

1097
lib/Controller/UserGroup.php Normal file

File diff suppressed because it is too large Load Diff

2033
lib/Controller/Widget.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,414 @@
<?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\Controller;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Factory\ModuleFactory;
use Xibo\Factory\WidgetDataFactory;
use Xibo\Factory\WidgetFactory;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\InvalidArgumentException;
/**
* Controller for managing Widget Data
*/
class WidgetData extends Base
{
public function __construct(
private readonly WidgetDataFactory $widgetDataFactory,
private readonly WidgetFactory $widgetFactory,
private readonly ModuleFactory $moduleFactory
) {
}
// phpcs:disable
/**
* @SWG\Get(
* path="/playlist/widget/data/{id}",
* operationId="getWidgetData",
* tags={"widget"},
* summary="Get data for Widget",
* description="Return all of the fallback data currently assigned to this Widget",
* @SWG\Parameter(
* name="id",
* in="path",
* description="The Widget ID that this data should be added to",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=201,
* description="successful operation",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="#/definitions/WidgetData")
* )
* )
* )
* @throws \Xibo\Support\Exception\GeneralException
*/
// phpcs:enable
public function get(Request $request, Response $response, int $id): Response
{
$widget = $this->widgetFactory->getById($id);
if (!$this->getUser()->checkEditable($widget)) {
throw new AccessDeniedException(__('This Widget is not shared with you with edit permission'));
}
return $response->withJson($this->widgetDataFactory->getByWidgetId($widget->widgetId));
}
// phpcs:disable
/**
* @SWG\Post(
* path="/playlist/widget/data/{id}",
* operationId="addWidgetData",
* tags={"widget"},
* summary="Add a data to a Widget",
* description="Add fallback data to a data Widget",
* @SWG\Parameter(
* name="id",
* in="path",
* description="The Widget ID that this data should be added to",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="data",
* in="path",
* description="A JSON formatted string containing a single data item for this widget's data type",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="displayOrder",
* in="formData",
* description="Optional integer to say which position this data should appear if there is more than one data item",
* type="integer",
* required=false
* ),
* @SWG\Response(
* response=201,
* description="successful operation",
* @SWG\Header(
* header="Location",
* description="Location of the new record",
* type="string"
* )
* )
* )
* @throws \Xibo\Support\Exception\GeneralException
*/
// phpcs:enable
public function add(Request $request, Response $response, int $id): Response
{
// Check that we have permission to edit this widget
$widget = $this->widgetFactory->getById($id);
if (!$this->getUser()->checkEditable($widget)) {
throw new AccessDeniedException(__('This Widget is not shared with you with edit permission'));
}
// Get the other params.
$params = $this->getSanitizer($request->getParams());
$widgetData = $this->widgetDataFactory
->create(
$widget->widgetId,
$this->parseAndValidate($widget, $params->getArray('data')),
$params->getInt('displayOrder', ['default' => 1]),
)
->save();
// Update the widget modified dt
$widget->modifiedDt =
// Successful
$this->getState()->hydrate([
'httpStatus' => 201,
'message' => __('Added data for Widget'),
'id' => $widgetData->id,
'data' => $widgetData,
]);
return $this->render($request, $response);
}
// phpcs:disable
/**
* @SWG\Put(
* path="/playlist/widget/data/{id}/{dataId}",
* operationId="editWidgetData",
* tags={"widget"},
* summary="Edit data on a Widget",
* description="Edit fallback data on a data Widget",
* @SWG\Parameter(
* name="id",
* in="path",
* description="The Widget ID that this data is attached to",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="dataId",
* in="path",
* description="The ID of the data to be edited",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="data",
* in="path",
* description="A JSON formatted string containing a single data item for this widget's data type",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="displayOrder",
* in="formData",
* description="Optional integer to say which position this data should appear if there is more than one data item",
* type="integer",
* required=false
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
* @throws \Xibo\Support\Exception\GeneralException
*/
// phpcs:enable
public function edit(Request $request, Response $response, int $id, int $dataId): Response
{
// Check that we have permission to edit this widget
$widget = $this->widgetFactory->getById($id);
if (!$this->getUser()->checkEditable($widget)) {
throw new AccessDeniedException(__('This Widget is not shared with you with edit permission'));
}
// Make sure this dataId is for this widget
$widgetData = $this->widgetDataFactory->getById($dataId);
if ($id !== $widgetData->widgetId) {
throw new AccessDeniedException(__('This widget data does not belong to this widget'));
}
// Get params and process the edit
$params = $this->getSanitizer($request->getParams());
$widgetData->data = $this->parseAndValidate($widget, $params->getArray('data'));
$widgetData->displayOrder = $params->getInt('displayOrder', ['default' => 1]);
$widgetData->save();
// Successful
$this->getState()->hydrate([
'message' => __('Edited data for Widget'),
'id' => $widgetData->id,
'data' => $widgetData,
'httpStatus' => 204,
]);
return $this->render($request, $response);
}
// phpcs:disable
/**
* @SWG\Delete(
* path="/playlist/widget/data/{id}/{dataId}",
* operationId="deleteWidgetData",
* tags={"widget"},
* summary="Delete data on a Widget",
* description="Delete fallback data on a data Widget",
* @SWG\Parameter(
* name="id",
* in="path",
* description="The Widget ID that this data is attached to",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="dataId",
* in="path",
* description="The ID of the data to be deleted",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
* @throws \Xibo\Support\Exception\GeneralException
*/
// phpcs:enable
public function delete(Request $request, Response $response, int $id, int $dataId): Response
{
// Check that we have permission to edit this widget
$widget = $this->widgetFactory->getById($id);
if (!$this->getUser()->checkEditable($widget)) {
throw new AccessDeniedException(__('This Widget is not shared with you with edit permission'));
}
// Make sure this dataId is for this widget
$widgetData = $this->widgetDataFactory->getById($dataId);
if ($id !== $widgetData->widgetId) {
throw new AccessDeniedException(__('This widget data does not belong to this widget'));
}
// Delete it.
$widgetData->delete();
// Successful
$this->getState()->hydrate(['message' => __('Deleted'), 'httpStatus' => 204]);
return $this->render($request, $response);
}
// phpcs:disable
/**
* @SWG\Definition(
* definition="WidgetDataOrder",
* @SWG\Property(
* property="dataId",
* type="integer",
* description="Data ID"
* ),
* @SWG\Property(
* property="displayOrder",
* type="integer",
* description="Desired display order"
* )
* )
*
* @SWG\Post(
* path="/playlist/widget/data/{id}/order",
* operationId="orderWidgetData",
* tags={"widget"},
* summary="Update the order of data on a Widget",
* description="Provide all data to be ordered on a widget",
* @SWG\Parameter(
* name="id",
* in="path",
* description="The Widget ID that this data is attached to",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="dataId",
* in="path",
* description="The ID of the data to be deleted",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="order",
* in="body",
* description="An array of any widget data records that should be re-ordered",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="WidgetDataOrder")
* ),
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
* @throws \Xibo\Support\Exception\GeneralException
*/
// phpcs:enable
public function setOrder(Request $request, Response $response, int $id): Response
{
// Check that we have permission to edit this widget
$widget = $this->widgetFactory->getById($id);
if (!$this->getUser()->checkEditable($widget)) {
throw new AccessDeniedException(__('This Widget is not shared with you with edit permission'));
}
// Expect an array of `id` in order.
$params = $this->getSanitizer($request->getParams());
foreach ($params->getArray('order', ['default' => []]) as $item) {
$itemParams = $this->getSanitizer($item);
// Make sure this dataId is for this widget
$widgetData = $this->widgetDataFactory->getById($itemParams->getInt('dataId'));
$widgetData->displayOrder = $itemParams->getInt('displayOrder');
if ($id !== $widgetData->widgetId) {
throw new AccessDeniedException(__('This widget data does not belong to this widget'));
}
// Save it
$widgetData->save();
}
// Successful
$this->getState()->hydrate([
'message' => __('Updated the display order for data on Widget'),
'id' => $widget->widgetId,
'httpStatus' => 204,
]);
return $this->render($request, $response);
}
/**
* Parse and validate the data provided in params.
* @throws \Xibo\Support\Exception\InvalidArgumentException
* @throws \Xibo\Support\Exception\NotFoundException
*/
private function parseAndValidate(\Xibo\Entity\Widget $widget, array $item): array
{
// Check that this module is a data widget
$module = $this->moduleFactory->getByType($widget->type);
if (!$module->isDataProviderExpected()) {
throw new InvalidArgumentException(__('This is not a data widget'));
}
if ($module->fallbackData !== 1) {
throw new InvalidArgumentException(__('Fallback data is not expected for this Widget'));
}
// Parse out the data string we've been given and make sure it's valid according to this widget's datatype
$data = [];
$params = $this->getSanitizer($item);
$dataType = $this->moduleFactory->getDataTypeById($module->dataType);
foreach ($dataType->fields as $field) {
if ($field->isRequired && !$params->hasParam($field->id)) {
throw new InvalidArgumentException(sprintf(
'Data is missing a field called %s',
$field->title
));
}
$value = match ($field->type) {
'number' => $params->getDouble($field->id),
default => $params->getString($field->id),
};
$data[$field->id] = $value;
}
return $data;
}
}

View File

@@ -0,0 +1,647 @@
<?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\Dependencies;
use Psr\Container\ContainerInterface;
/**
* Helper class to add controllers to DI
*/
class Controllers
{
/**
* Register controllers with DI
*/
public static function registerControllersWithDi()
{
return [
'\Xibo\Controller\Action' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Action(
$c->get('actionFactory'),
$c->get('layoutFactory'),
$c->get('regionFactory'),
$c->get('widgetFactory'),
$c->get('moduleFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\Applications' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Applications(
$c->get('session'),
$c->get('applicationFactory'),
$c->get('applicationRedirectUriFactory'),
$c->get('applicationScopeFactory'),
$c->get('userFactory'),
$c->get('pool'),
$c->get('connectorFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\AuditLog' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\AuditLog(
$c->get('auditLogFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\Campaign' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Campaign(
$c->get('campaignFactory'),
$c->get('layoutFactory'),
$c->get('tagFactory'),
$c->get('folderFactory'),
$c->get('displayGroupFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\Connector' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Connector(
$c->get('connectorFactory'),
$c->get('widgetFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\Clock' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Clock(
$c->get('session')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\Command' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Command(
$c->get('commandFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\DataSet' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\DataSet(
$c->get('dataSetFactory'),
$c->get('dataSetColumnFactory'),
$c->get('userFactory'),
$c->get('folderFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\DataSetColumn' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\DataSetColumn(
$c->get('dataSetFactory'),
$c->get('dataSetColumnFactory'),
$c->get('dataSetColumnTypeFactory'),
$c->get('dataTypeFactory'),
$c->get('pool')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\DataSetData' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\DataSetData(
$c->get('dataSetFactory'),
$c->get('mediaFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\DataSetRss' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\DataSetRss(
$c->get('dataSetRssFactory'),
$c->get('dataSetFactory'),
$c->get('dataSetColumnFactory'),
$c->get('pool'),
$c->get('store')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\DayPart' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\DayPart(
$c->get('dayPartFactory'),
$c->get('scheduleFactory'),
$c->get('displayNotifyService')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\Developer' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Developer(
$c->get('moduleFactory'),
$c->get('moduleTemplateFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\Display' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Display(
$c->get('store'),
$c->get('pool'),
$c->get('playerActionService'),
$c->get('displayFactory'),
$c->get('displayGroupFactory'),
$c->get('displayTypeFactory'),
$c->get('layoutFactory'),
$c->get('displayProfileFactory'),
$c->get('displayEventFactory'),
$c->get('requiredFileFactory'),
$c->get('tagFactory'),
$c->get('notificationFactory'),
$c->get('userGroupFactory'),
$c->get('playerVersionFactory'),
$c->get('dayPartFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\DisplayGroup' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\DisplayGroup(
$c->get('playerActionService'),
$c->get('displayFactory'),
$c->get('displayGroupFactory'),
$c->get('layoutFactory'),
$c->get('moduleFactory'),
$c->get('mediaFactory'),
$c->get('commandFactory'),
$c->get('tagFactory'),
$c->get('campaignFactory'),
$c->get('folderFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\DisplayProfile' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\DisplayProfile(
$c->get('pool'),
$c->get('displayProfileFactory'),
$c->get('commandFactory'),
$c->get('playerVersionFactory'),
$c->get('dayPartFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\Fault' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Fault(
$c->get('store'),
$c->get('logFactory'),
$c->get('displayFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\Folder' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Folder(
$c->get('folderFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\Font' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Font(
$c->get('fontFactory')
);
$controller->useMediaService($c->get('mediaService'));
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\IconDashboard' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\IconDashboard();
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\Layout' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Layout(
$c->get('session'),
$c->get('userFactory'),
$c->get('resolutionFactory'),
$c->get('layoutFactory'),
$c->get('moduleFactory'),
$c->get('userGroupFactory'),
$c->get('tagFactory'),
$c->get('mediaFactory'),
$c->get('dataSetFactory'),
$c->get('campaignFactory'),
$c->get('displayGroupFactory'),
$c->get('pool'),
$c->get('mediaService'),
$c->get('widgetFactory'),
$c->get('widgetDataFactory'),
$c->get('playlistFactory'),
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\Library' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Library(
$c->get('userFactory'),
$c->get('moduleFactory'),
$c->get('tagFactory'),
$c->get('mediaFactory'),
$c->get('widgetFactory'),
$c->get('permissionFactory'),
$c->get('layoutFactory'),
$c->get('playlistFactory'),
$c->get('userGroupFactory'),
$c->get('displayFactory'),
$c->get('scheduleFactory'),
$c->get('folderFactory')
);
$controller->useMediaService($c->get('mediaService'));
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\Logging' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Logging(
$c->get('store'),
$c->get('logFactory'),
$c->get('userFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\Login' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Login(
$c->get('session'),
$c->get('userFactory'),
$c->get('pool')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
if ($c->has('flash')) {
$controller->setFlash($c->get('flash'));
}
return $controller;
},
'\Xibo\Controller\Maintenance' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Maintenance(
$c->get('store'),
$c->get('mediaFactory'),
$c->get('mediaService')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\MediaManager' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\MediaManager(
$c->get('store'),
$c->get('moduleFactory'),
$c->get('mediaFactory'),
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\MenuBoard' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\MenuBoard(
$c->get('menuBoardFactory'),
$c->get('folderFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\MenuBoardCategory' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\MenuBoardCategory(
$c->get('menuBoardFactory'),
$c->get('menuBoardCategoryFactory'),
$c->get('mediaFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\MenuBoardProduct' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\MenuBoardProduct(
$c->get('menuBoardFactory'),
$c->get('menuBoardCategoryFactory'),
$c->get('menuBoardProductOptionFactory'),
$c->get('mediaFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\PlaylistDashboard' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\PlaylistDashboard(
$c->get('playlistFactory'),
$c->get('moduleFactory'),
$c->get('widgetFactory'),
$c->get('mediaFactory'),
$c
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\Module' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Module(
$c->get('moduleFactory'),
$c->get('moduleTemplateFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\Notification' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Notification(
$c->get('notificationFactory'),
$c->get('userNotificationFactory'),
$c->get('displayGroupFactory'),
$c->get('userGroupFactory'),
$c->get('displayNotifyService')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\PlayerFault' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\PlayerFault(
$c->get('playerFaultFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\PlayerSoftware' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\PlayerSoftware(
$c->get('pool'),
$c->get('playerVersionFactory'),
$c->get('displayProfileFactory'),
$c->get('displayFactory')
);
$controller->useMediaService($c->get('mediaService'));
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\Playlist' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Playlist(
$c->get('playlistFactory'),
$c->get('mediaFactory'),
$c->get('widgetFactory'),
$c->get('moduleFactory'),
$c->get('userGroupFactory'),
$c->get('userFactory'),
$c->get('tagFactory'),
$c->get('layoutFactory'),
$c->get('displayFactory'),
$c->get('scheduleFactory'),
$c->get('folderFactory'),
$c->get('regionFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\Preview' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Preview(
$c->get('layoutFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\Pwa' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Pwa(
$c->get('displayFactory'),
$c,
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\Region' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Region(
$c->get('regionFactory'),
$c->get('widgetFactory'),
$c->get('transitionFactory'),
$c->get('moduleFactory'),
$c->get('layoutFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\Report' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Report(
$c->get('reportService')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\SavedReport' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\SavedReport(
$c->get('reportService'),
$c->get('reportScheduleFactory'),
$c->get('savedReportFactory'),
$c->get('mediaFactory'),
$c->get('userFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\ScheduleReport' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\ScheduleReport(
$c->get('reportService'),
$c->get('reportScheduleFactory'),
$c->get('savedReportFactory'),
$c->get('mediaFactory'),
$c->get('userFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\SyncGroup' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\SyncGroup(
$c->get('syncGroupFactory'),
$c->get('folderFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\Resolution' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Resolution(
$c->get('resolutionFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\Schedule' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Schedule(
$c->get('session'),
$c->get('scheduleFactory'),
$c->get('displayGroupFactory'),
$c->get('campaignFactory'),
$c->get('commandFactory'),
$c->get('displayFactory'),
$c->get('layoutFactory'),
$c->get('dayPartFactory'),
$c->get('scheduleReminderFactory'),
$c->get('scheduleExclusionFactory'),
$c->get('syncGroupFactory'),
$c->get('scheduleCriteriaFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\CypressTest' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\CypressTest(
$c->get('store'),
$c->get('session'),
$c->get('scheduleFactory'),
$c->get('displayGroupFactory'),
$c->get('campaignFactory'),
$c->get('displayFactory'),
$c->get('layoutFactory'),
$c->get('dayPartFactory'),
$c->get('folderFactory'),
$c->get('commandFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\Sessions' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Sessions(
$c->get('store'),
$c->get('sessionFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\Settings' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Settings(
$c->get('layoutFactory'),
$c->get('userGroupFactory'),
$c->get('transitionFactory'),
$c->get('userFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\Stats' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Stats(
$c->get('store'),
$c->get('timeSeriesStore'),
$c->get('reportService'),
$c->get('displayFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\StatusDashboard' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\StatusDashboard(
$c->get('store'),
$c->get('pool'),
$c->get('userFactory'),
$c->get('displayFactory'),
$c->get('displayGroupFactory'),
$c->get('mediaFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\Task' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Task(
$c->get('store'),
$c->get('timeSeriesStore'),
$c->get('pool'),
$c->get('taskFactory'),
$c
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\Tag' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Tag(
$c->get('displayGroupFactory'),
$c->get('layoutFactory'),
$c->get('tagFactory'),
$c->get('userFactory'),
$c->get('displayFactory'),
$c->get('mediaFactory'),
$c->get('scheduleFactory'),
$c->get('campaignFactory'),
$c->get('playlistFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\Template' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Template(
$c->get('layoutFactory'),
$c->get('tagFactory'),
$c->get('resolutionFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\Transition' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Transition(
$c->get('transitionFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\User' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\User(
$c->get('userFactory'),
$c->get('userTypeFactory'),
$c->get('userGroupFactory'),
$c->get('permissionFactory'),
$c->get('applicationFactory'),
$c->get('sessionFactory'),
$c->get('mediaService')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\UserGroup' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\UserGroup(
$c->get('userGroupFactory'),
$c->get('permissionFactory'),
$c->get('userFactory')
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\Widget' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\Widget(
$c->get('moduleFactory'),
$c->get('moduleTemplateFactory'),
$c->get('playlistFactory'),
$c->get('mediaFactory'),
$c->get('permissionFactory'),
$c->get('widgetFactory'),
$c->get('transitionFactory'),
$c->get('regionFactory'),
$c->get('widgetAudioFactory'),
$c->get('widgetDataFactory'),
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
'\Xibo\Controller\WidgetData' => function (ContainerInterface $c) {
$controller = new \Xibo\Controller\WidgetData(
$c->get('widgetDataFactory'),
$c->get('widgetFactory'),
$c->get('moduleFactory'),
);
$controller->useBaseDependenciesService($c->get('ControllerBaseDependenciesService'));
return $controller;
},
];
}
}

View File

@@ -0,0 +1,536 @@
<?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\Dependencies;
use Psr\Container\ContainerInterface;
/**
* Helper class to add factories to DI.
*/
class Factories
{
/**
* Register Factories with DI
*/
public static function registerFactoriesWithDi()
{
return [
'actionFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\ActionFactory(
$c->get('user'),
$c->get('userFactory')
);
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'apiRequestsFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\ApplicationRequestsFactory();
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'applicationFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\ApplicationFactory(
$c->get('user'),
$c->get('applicationRedirectUriFactory'),
$c->get('applicationScopeFactory')
);
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'applicationRedirectUriFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\ApplicationRedirectUriFactory();
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'applicationScopeFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\ApplicationScopeFactory();
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'auditLogFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\AuditLogFactory();
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'bandwidthFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\BandwidthFactory(
$c->get('pool'),
);
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'campaignFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\CampaignFactory(
$c->get('user'),
$c->get('userFactory'),
$c->get('permissionFactory'),
$c->get('scheduleFactory'),
$c->get('displayNotifyService')
);
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'commandFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\CommandFactory(
$c->get('user'),
$c->get('userFactory')
);
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'connectorFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\ConnectorFactory(
$c->get('pool'),
$c->get('configService'),
$c->get('jwtService'),
$c->get('playerActionService'),
$c
);
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'dataSetColumnFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\DataSetColumnFactory(
$c->get('dataTypeFactory'),
$c->get('dataSetColumnTypeFactory')
);
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'dataSetColumnTypeFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\DataSetColumnTypeFactory();
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'dataSetFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\DataSetFactory(
$c->get('user'),
$c->get('userFactory'),
$c->get('configService'),
$c->get('pool'),
$c->get('dataSetColumnFactory'),
$c->get('permissionFactory'),
$c->get('displayNotifyService')
);
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'dataSetRssFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\DataSetRssFactory(
$c->get('user'),
$c->get('userFactory')
);
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'dataTypeFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\DataTypeFactory();
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'dayPartFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\DayPartFactory(
$c->get('user'),
$c->get('userFactory')
);
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'displayFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\DisplayFactory(
$c->get('user'),
$c->get('userFactory'),
$c->get('displayNotifyService'),
$c->get('configService'),
$c->get('displayGroupFactory'),
$c->get('displayProfileFactory'),
$c->get('folderFactory')
);
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'displayEventFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\DisplayEventFactory();
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'displayGroupFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\DisplayGroupFactory(
$c->get('user'),
$c->get('userFactory'),
$c->get('permissionFactory')
);
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'displayTypeFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\DisplayTypeFactory();
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'displayProfileFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\DisplayProfileFactory(
$c->get('configService'),
$c->get('commandFactory')
);
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'folderFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\FolderFactory(
$c->get('permissionFactory'),
$c->get('user'),
$c->get('userFactory')
);
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'fontFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\FontFactory(
$c->get('configService')
);
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'layoutFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\LayoutFactory(
$c->get('user'),
$c->get('userFactory'),
$c->get('configService'),
$c->get('permissionFactory'),
$c->get('regionFactory'),
$c->get('tagFactory'),
$c->get('campaignFactory'),
$c->get('mediaFactory'),
$c->get('moduleFactory'),
$c->get('moduleTemplateFactory'),
$c->get('resolutionFactory'),
$c->get('widgetFactory'),
$c->get('widgetOptionFactory'),
$c->get('playlistFactory'),
$c->get('widgetAudioFactory'),
$c->get('actionFactory'),
$c->get('folderFactory'),
$c->get('fontFactory'),
$c->get('widgetDataFactory'),
);
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
if ($c->has('pool')) {
$repository->usePool($c->get('pool'));
}
return $repository;
},
'logFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\LogFactory();
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'mediaFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\MediaFactory(
$c->get('user'),
$c->get('userFactory'),
$c->get('configService'),
$c->get('permissionFactory'),
$c->get('playlistFactory')
);
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'menuBoardCategoryFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\MenuBoardCategoryFactory(
$c->get('menuBoardProductOptionFactory')
);
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'menuBoardProductOptionFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\MenuBoardProductOptionFactory();
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'menuBoardFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\MenuBoardFactory(
$c->get('user'),
$c->get('userFactory'),
$c->get('configService'),
$c->get('pool'),
$c->get('permissionFactory'),
$c->get('menuBoardCategoryFactory'),
$c->get('displayNotifyService')
);
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'moduleFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\ModuleFactory(
$c->get('configService')->getSetting('LIBRARY_LOCATION') . 'widget',
$c->get('pool'),
$c->get('view'),
$c->get('configService')
);
$repository
->setAclDependencies(
$c->get('user'),
$c->get('userFactory')
)
->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'moduleTemplateFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\ModuleTemplateFactory(
$c->get('pool'),
$c->get('view'),
);
$repository
->setAclDependencies(
$c->get('user'),
$c->get('userFactory')
)
->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'notificationFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\NotificationFactory(
$c->get('user'),
$c->get('userFactory'),
$c->get('userGroupFactory'),
$c->get('displayGroupFactory')
);
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'permissionFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\PermissionFactory();
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'playerFaultFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\PlayerFaultFactory();
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'playerVersionFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\PlayerVersionFactory(
$c->get('user'),
$c->get('userFactory'),
$c->get('configService')
);
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'playlistFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\PlaylistFactory(
$c->get('configService'),
$c->get('user'),
$c->get('userFactory'),
$c->get('permissionFactory'),
$c->get('widgetFactory')
);
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'regionFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\RegionFactory(
$c->get('permissionFactory'),
$c->get('regionOptionFactory'),
$c->get('playlistFactory'),
$c->get('actionFactory'),
$c->get('campaignFactory')
);
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'regionOptionFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\RegionOptionFactory();
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'requiredFileFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\RequiredFileFactory();
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'reportScheduleFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\ReportScheduleFactory(
$c->get('user'),
$c->get('userFactory')
);
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'resolutionFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\ResolutionFactory();
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'savedReportFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\SavedReportFactory(
$c->get('user'),
$c->get('userFactory'),
$c->get('configService'),
$c->get('mediaFactory')
);
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'scheduleFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\ScheduleFactory(
$c->get('configService'),
$c->get('pool'),
$c->get('displayGroupFactory'),
$c->get('dayPartFactory'),
$c->get('userFactory'),
$c->get('scheduleReminderFactory'),
$c->get('scheduleExclusionFactory'),
$c->get('user'),
$c->get('scheduleCriteriaFactory')
);
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'scheduleReminderFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\ScheduleReminderFactory(
$c->get('user'),
$c->get('userFactory'),
$c->get('configService')
);
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'scheduleExclusionFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\ScheduleExclusionFactory();
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'scheduleCriteriaFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\ScheduleCriteriaFactory();
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'sessionFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\SessionFactory();
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'syncGroupFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\SyncGroupFactory(
$c->get('user'),
$c->get('userFactory'),
$c->get('permissionFactory'),
$c->get('displayFactory'),
$c->get('scheduleFactory')
);
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'tagFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\TagFactory();
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'taskFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\TaskFactory();
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'transitionFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\TransitionFactory();
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'userFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\UserFactory(
$c->get('configService'),
$c->get('permissionFactory'),
$c->get('userOptionFactory'),
$c->get('applicationScopeFactory')
);
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'userGroupFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\UserGroupFactory(
$c->get('user'),
$c->get('userFactory')
);
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'userNotificationFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\UserNotificationFactory(
$c->get('user'),
$c->get('userFactory')
);
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'userOptionFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\UserOptionFactory();
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'userTypeFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\UserTypeFactory();
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'widgetFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\WidgetFactory(
$c->get('user'),
$c->get('userFactory'),
$c->get('widgetOptionFactory'),
$c->get('widgetMediaFactory'),
$c->get('widgetAudioFactory'),
$c->get('permissionFactory'),
$c->get('displayNotifyService'),
$c->get('actionFactory'),
$c->get('moduleTemplateFactory')
);
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'widgetMediaFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\WidgetMediaFactory();
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'widgetAudioFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\WidgetAudioFactory();
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'widgetOptionFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\WidgetOptionFactory();
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
'widgetDataFactory' => function (ContainerInterface $c) {
$repository = new \Xibo\Factory\WidgetDataFactory();
$repository->useBaseDependenciesService($c->get('RepositoryBaseDependenciesService'));
return $repository;
},
];
}
}

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

@@ -0,0 +1,300 @@
<?php
/*
* Copyright (C) 2022 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Entity;
use Carbon\Carbon;
use Xibo\Helper\DateFormatHelper;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\InvalidArgumentException;
/**
* Class Action
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class Action implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The Action Id")
* @var int
*/
public $actionId;
/**
* @SWG\Property(description="The Owner Id")
* @var int
*/
public $ownerId;
/**
* @SWG\Property(description="The Action trigger type")
* @var string
*/
public $triggerType;
/**
* @SWG\Property(description="The Action trigger code")
* @var string
*/
public $triggerCode;
/**
* @SWG\Property(description="The Action type")
* @var string
*/
public $actionType;
/**
* @SWG\Property(description="The Action source (layout, region or widget)")
* @var string
*/
public $source;
/**
* @SWG\Property(description="The Action source Id (layoutId, regionId or widgetId)")
* @var int
*/
public $sourceId;
/**
* @SWG\Property(description="The Action target (region)")
* @var string
*/
public $target;
/**
* @SWG\Property(description="The Action target Id (regionId)")
* @var int
*/
public $targetId;
/**
* @SWG\Property(description="Widget ID that will be loaded as a result of navigate to Widget Action type")
* @var int
*/
public $widgetId;
/**
* @SWG\Property(description="Layout Code identifier")
* @var string
*/
public $layoutCode;
/**
* @SWG\Property(description="Layout Id associated with this Action")
* @var int
*/
public $layoutId;
/** @var \Xibo\Factory\PermissionFactory */
private $permissionFactory;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
public function __clone()
{
$this->hash = null;
$this->actionId = null;
}
/**
* Get the Id
* @return int
*/
public function getId()
{
return $this->actionId;
}
/**
* Get the OwnerId
* @return int
*/
public function getOwnerId()
{
return $this->ownerId;
}
/**
* Sets the Owner
* @param int $ownerId
*/
public function setOwner($ownerId)
{
$this->ownerId = $ownerId;
}
/**
* @return string
*/
public function __toString()
{
return sprintf('ActionId %d, Trigger Type %s, Trigger Code %s, Action Type %s, Source %s, SourceId %s, Target %s, TargetId %d', $this->actionId, $this->triggerType, $this->triggerCode, $this->actionType, $this->source, $this->sourceId, $this->target, $this->targetId);
}
/**
* @throws InvalidArgumentException
*/
public function validate()
{
// on add we expect only layoutId, actionType, target and targetId
if ($this->layoutId == null) {
throw new InvalidArgumentException(__('No layoutId specified'), 'layoutId');
}
if (!in_array($this->actionType, ['next', 'previous', 'navLayout', 'navWidget'])) {
throw new InvalidArgumentException(__('Invalid action type'), 'actionType');
}
if (!in_array(strtolower($this->source), ['layout', 'region', 'widget'])) {
throw new InvalidArgumentException(__('Invalid source'), 'source');
}
if (!in_array(strtolower($this->target), ['region', 'screen'])) {
throw new InvalidArgumentException(__('Invalid target'), 'target');
}
if ($this->target == 'region' && $this->targetId == null) {
throw new InvalidArgumentException(__('Please select a Region'), 'targetId');
}
if ($this->triggerType === 'webhook' && $this->triggerCode === null) {
throw new InvalidArgumentException(__('Please provide trigger code'), 'triggerCode');
}
if ($this->triggerType === 'keyPress' && $this->triggerCode === null) {
throw new InvalidArgumentException(__('Please provide trigger key'), 'triggerKey');
}
if (!in_array($this->triggerType, ['touch', 'webhook', 'keyPress'])) {
throw new InvalidArgumentException(__('Invalid trigger type'), 'triggerType');
}
if ($this->actionType === 'navLayout' && $this->layoutCode == '') {
throw new InvalidArgumentException(__('Please enter Layout code'), 'layoutCode');
}
if ($this->actionType === 'navWidget' && $this->widgetId == null) {
throw new InvalidArgumentException(__('Please create a Widget to be loaded'), 'widgetId');
}
}
/**
* @param array $options
* @throws InvalidArgumentException
*/
public function save($options = [])
{
$options = array_merge([
'validate' => true,
'notifyLayout' => false
], $options);
$this->getLog()->debug('Saving ' . $this);
if ($options['validate']) {
$this->validate();
}
if ($this->actionId == null || $this->actionId == 0) {
$this->add();
$this->loaded = true;
} else {
$this->update();
}
if ($options['notifyLayout'] && $this->layoutId != null) {
$this->notifyLayout($this->layoutId);
}
}
public function add()
{
$this->actionId = $this->getStore()->insert('INSERT INTO `action` (ownerId, triggerType, triggerCode, actionType, source, sourceId, target, targetId, widgetId, layoutCode, layoutId) VALUES (:ownerId, :triggerType, :triggerCode, :actionType, :source, :sourceId, :target, :targetId, :widgetId, :layoutCode, :layoutId)', [
'ownerId' => $this->ownerId,
'triggerType' => $this->triggerType,
'triggerCode' => $this->triggerCode,
'actionType' => $this->actionType,
'source' => $this->source,
'sourceId' => $this->sourceId,
'target' => $this->target,
'targetId' => $this->targetId,
'widgetId' => $this->widgetId,
'layoutCode' => $this->layoutCode,
'layoutId' => $this->layoutId
]);
}
public function update()
{
$this->getStore()->update('UPDATE `action` SET ownerId = :ownerId, triggerType = :triggerType, triggerCode = :triggerCode, actionType = :actionType, source = :source, sourceId = :sourceId, target = :target, targetId = :targetId, widgetId = :widgetId, layoutCode = :layoutCode, layoutId = :layoutId WHERE actionId = :actionId', [
'ownerId' => $this->ownerId,
'triggerType' => $this->triggerType,
'triggerCode' => $this->triggerCode,
'actionType' => $this->actionType,
'source' => $this->source,
'sourceId' => $this->sourceId,
'target' => $this->target,
'targetId' => $this->targetId,
'actionId' => $this->actionId,
'widgetId' => $this->widgetId,
'layoutCode' => $this->layoutCode,
'layoutId' => $this->layoutId
]);
}
public function delete()
{
$this->getStore()->update('DELETE FROM `action` WHERE actionId = :actionId', ['actionId' => $this->actionId]);
}
/**
* Notify the Layout (set to building)
* @param $layoutId
*/
public function notifyLayout($layoutId)
{
$this->getLog()->debug(sprintf('Saving Interactive Action ID %d triggered layout ID %d build', $this->actionId, $layoutId));
$this->getStore()->update('
UPDATE `layout` SET `status` = 3, `modifiedDT` = :modifiedDt WHERE layoutId = :layoutId
', [
'layoutId' => $layoutId,
'modifiedDt' => Carbon::now()->format(DateFormatHelper::getSystemFormat())
]);
}
}

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

@@ -0,0 +1,458 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Entity;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use Xibo\Factory\ApplicationRedirectUriFactory;
use Xibo\Factory\ApplicationScopeFactory;
use Xibo\Helper\Random;
use Xibo\OAuth\ScopeEntity;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class Application
* @package Xibo\Entity
*
* @SWG\Definition
*/
class Application implements \JsonSerializable, ClientEntityInterface
{
use EntityTrait;
/**
* @SWG\Property(
* description="Application Key"
* )
* @var string
*/
public $key;
/**
* @SWG\Property(
* description="Private Secret Key"
* )
* @var string
*/
public $secret;
/**
* @SWG\Property(
* description="Application Name"
* )
* @var string
*/
public $name;
/**
* @SWG\Property(
* description="Application Owner"
* )
* @var string
*/
public $owner;
/**
* @SWG\Property(
* description="Application Session Expiry"
* )
* @var int
*/
public $expires;
/**
* @SWG\Property(
* description="The Owner of this Application"
* )
* @var int
*/
public $userId;
/**
* @SWG\Property(description="Flag indicating whether to allow the authorizationCode Grant Type")
* @var int
*/
public $authCode = 0;
/**
* @SWG\Property(description="Flag indicating whether to allow the clientCredentials Grant Type")
* @var int
*/
public $clientCredentials = 0;
/**
* @SWG\Property(description="Flag indicating whether this Application will be confidential or not (can it keep a secret?)")
* @var int
*/
public $isConfidential = 1;
/** * @var ApplicationRedirectUri[] */
public $redirectUris = [];
/** * @var ApplicationScope[] */
public $scopes = [];
/**
* @SWG\Property(description="Application description")
* @var string
*/
public $description;
/**
* @SWG\Property(description="Path to Application logo")
* @var string
*/
public $logo;
/**
* @SWG\Property(description="Path to Application Cover Image")
* @var string
*/
public $coverImage;
/**
* @SWG\Property(description="Company name associated with this Application")
* @var string
*/
public $companyName;
/**
* @SWG\Property(description="URL to Application terms")
* @var string
*/
public $termsUrl;
/**
* @SWG\Property(description="URL to Application privacy policy")
* @var string
*/
public $privacyUrl;
/** @var ApplicationRedirectUriFactory */
private $applicationRedirectUriFactory;
/** @var ApplicationScopeFactory */
private $applicationScopeFactory;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
* @param ApplicationRedirectUriFactory $applicationRedirectUriFactory
* @param ApplicationScopeFactory $applicationScopeFactory
*/
public function __construct($store, $log, $dispatcher, $applicationRedirectUriFactory, $applicationScopeFactory)
{
$this->setCommonDependencies($store, $log, $dispatcher);
$this->applicationRedirectUriFactory = $applicationRedirectUriFactory;
$this->applicationScopeFactory = $applicationScopeFactory;
}
public function __serialize(): array
{
return $this->jsonSerialize();
}
public function __unserialize(array $data): void
{
foreach ($data as $key => $value) {
$this->{$key} = $value;
}
}
/**
* @param ApplicationRedirectUri $redirectUri
*/
public function assignRedirectUri($redirectUri)
{
$this->load();
// Assert client id
$redirectUri->clientId = $this->key;
if (!in_array($redirectUri, $this->redirectUris)) {
$this->redirectUris[] = $redirectUri;
}
}
/**
* Unassign RedirectUri
* @param ApplicationRedirectUri $redirectUri
*/
public function unassignRedirectUri($redirectUri)
{
$this->load();
$this->redirectUris = array_udiff($this->redirectUris, [$redirectUri], function($a, $b) {
/**
* @var ApplicationRedirectUri $a
* @var ApplicationRedirectUri $b
*/
return $a->getId() - $b->getId();
});
}
/**
* @param ApplicationScope $scope
*/
public function assignScope($scope)
{
if (!in_array($scope, $this->scopes)) {
$this->scopes[] = $scope;
}
return $this;
}
/**
* @param ApplicationScope $scope
*/
public function unassignScope($scope)
{
$this->scopes = array_udiff($this->scopes, [$scope], function ($a, $b) {
/**
* @var ApplicationScope $a
* @var ApplicationScope $b
*/
return $a->getId() !== $b->getId();
});
}
/**
* Get the hash for password verify
* @return string
*/
public function getHash()
{
return password_hash($this->secret, PASSWORD_DEFAULT);
}
/**
* Load
* @return $this
*/
public function load()
{
if ($this->loaded || empty($this->key)) {
return $this;
}
// Redirects
$this->redirectUris = $this->applicationRedirectUriFactory->getByClientId($this->key);
// Get scopes
$this->scopes = $this->applicationScopeFactory->getByClientId($this->key);
$this->loaded = true;
return $this;
}
/**
* @return $this
*/
public function save()
{
if ($this->key == null || $this->key == '') {
// Make a new secret.
$this->resetSecret();
// Add
$this->add();
} else {
// Edit
$this->edit();
}
$this->getLog()->debug('Saving redirect uris: ' . json_encode($this->redirectUris));
foreach ($this->redirectUris as $redirectUri) {
$redirectUri->save();
}
$this->manageScopeAssignments();
return $this;
}
/**
* Delete
*/
public function delete()
{
$this->load();
foreach ($this->redirectUris as $redirectUri) {
$redirectUri->delete();
}
// Clear link table for this Application
$this->getStore()->update('DELETE FROM `oauth_lkclientuser` WHERE clientId = :id', ['id' => $this->key]);
// Clear out everything owned by this client
$this->getStore()->update('DELETE FROM `oauth_client_scopes` WHERE `clientId` = :id', ['id' => $this->key]);
$this->getStore()->update('DELETE FROM `oauth_clients` WHERE `id` = :id', ['id' => $this->key]);
}
/**
* Reset Secret
*/
public function resetSecret()
{
$this->secret = Random::generateString(254);
}
private function add()
{
// Make an ID
$this->key = Random::generateString(40);
// Simple Insert for now
$this->getStore()->insert('
INSERT INTO `oauth_clients` (`id`, `secret`, `name`, `userId`, `authCode`, `clientCredentials`, `isConfidential`, `description`, `logo`, `coverImage`, `companyName`, `termsUrl`, `privacyUrl`)
VALUES (:id, :secret, :name, :userId, :authCode, :clientCredentials, :isConfidential, :description, :logo, :coverImage, :companyName, :termsUrl, :privacyUrl)
', [
'id' => $this->key,
'secret' => $this->secret,
'name' => $this->name,
'userId' => $this->userId,
'authCode' => $this->authCode,
'clientCredentials' => $this->clientCredentials,
'isConfidential' => $this->isConfidential,
'description' => $this->description,
'logo' => $this->logo,
'coverImage' => $this->coverImage,
'companyName' => $this->companyName,
'termsUrl' => $this->termsUrl,
'privacyUrl' => $this->privacyUrl
]);
}
private function edit()
{
$this->getStore()->update('
UPDATE `oauth_clients` SET
`id` = :id,
`secret` = :secret,
`name` = :name,
`userId` = :userId,
`authCode` = :authCode,
`clientCredentials` = :clientCredentials,
`isConfidential` = :isConfidential,
`description` = :description,
`logo` = :logo,
`coverImage` = :coverImage,
`companyName` = :companyName,
`termsUrl` = :termsUrl,
`privacyUrl` = :privacyUrl
WHERE `id` = :id
', [
'id' => $this->key,
'secret' => $this->secret,
'name' => $this->name,
'userId' => $this->userId,
'authCode' => $this->authCode,
'clientCredentials' => $this->clientCredentials,
'isConfidential' => $this->isConfidential,
'description' => $this->description,
'logo' => $this->logo,
'coverImage' => $this->coverImage,
'companyName' => $this->companyName,
'termsUrl' => $this->termsUrl,
'privacyUrl' => $this->privacyUrl
]);
}
/**
* Compare the original assignments with the current assignments and delete any that are missing, add any new ones
*/
private function manageScopeAssignments()
{
$i = 0;
$params = ['clientId' => $this->key];
$unassignIn = '';
foreach ($this->scopes as $link) {
$this->getStore()->update('
INSERT INTO `oauth_client_scopes` (clientId, scopeId) VALUES (:clientId, :scopeId)
ON DUPLICATE KEY UPDATE scopeId = scopeId', [
'clientId' => $this->key,
'scopeId' => $link->id
]);
$i++;
$unassignIn .= ',:scopeId' . $i;
$params['scopeId' . $i] = $link->id;
}
// Unlink any NOT in the collection
$sql = 'DELETE FROM `oauth_client_scopes` WHERE clientId = :clientId AND scopeId NOT IN (\'0\'' . $unassignIn . ')';
$this->getStore()->update($sql, $params);
}
/** @inheritDoc */
public function getIdentifier()
{
return $this->key;
}
/** @inheritDoc */
public function getName()
{
return $this->name;
}
/** @inheritDoc */
public function getRedirectUri()
{
$count = count($this->redirectUris);
if ($count <= 0) {
return null;
} else if (count($this->redirectUris) == 1) {
return $this->redirectUris[0]->redirectUri;
} else {
return array_map(function($el) {
return $el->redirectUri;
}, $this->redirectUris);
}
}
/**
* @return \League\OAuth2\Server\Entities\ScopeEntityInterface[]
*/
public function getScopes()
{
$scopes = [];
foreach ($this->scopes as $applicationScope) {
$scope = new ScopeEntity();
$scope->setIdentifier($applicationScope->getId());
$scopes[] = $scope;
}
return $scopes;
}
/** @inheritDoc */
public function isConfidential()
{
return $this->isConfidential === 1;
}
}

View File

@@ -0,0 +1,123 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Entity;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class ApplicationRedirectUri
* @package Xibo\Entity
*/
class ApplicationRedirectUri implements \JsonSerializable
{
use EntityTrait;
/**
* @var int
*/
public $id;
/**
* @var string
*/
public $clientId;
/**
* @var string
*/
public $redirectUri;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
public function __serialize(): array
{
return $this->jsonSerialize();
}
public function __unserialize(array $data): void
{
foreach ($data as $key => $value) {
$this->{$key} = $value;
}
}
/**
* Get Id
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* Save
*/
public function save()
{
if ($this->id == null)
$this->add();
else
$this->edit();
}
public function delete()
{
$this->getStore()->update('DELETE FROM `oauth_client_redirect_uris` WHERE `id` = :id', ['id' => $this->id]);
}
private function add()
{
$this->id = $this->getStore()->insert('
INSERT INTO `oauth_client_redirect_uris` (`client_id`, `redirect_uri`)
VALUES (:clientId, :redirectUri)
', [
'clientId' => $this->clientId,
'redirectUri' => $this->redirectUri
]);
}
private function edit()
{
$this->getStore()->update('
UPDATE `oauth_client_redirect_uris`
SET `redirect_uri` = :redirectUri
WHERE `id` = :id
',[
'id' => $this->id,
'redirectUri' => $this->redirectUri
]);
}
}

View File

@@ -0,0 +1,98 @@
<?php
/*
* Copyright (C) 2025 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Entity;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Application Request
* @SWG\Definition()
*/
class ApplicationRequest implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The request ID")
* @var int
*/
public $requestId;
/**
* @SWG\Property(description="The user ID")
* @var int
*/
public $userId;
/**
* @SWG\Property(description="The application ID")
* @var string
*/
public $applicationId;
/**
* @SWG\Property(description="The request route")
* @var string
*/
public $url;
/**
* @SWG\Property(description="The request method")
* @var string
*/
public $method;
/**
* @SWG\Property(description="The request start time")
* @var string
*/
public $startTime;
/**
* @SWG\Property(description="The request end time")
* @var string
*/
public $endTime;
/**
* @SWG\Property(description="The request duration")
* @var int
*/
public $duration;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param EventDispatcherInterface $dispatcher
*/
public function __construct(
StorageServiceInterface $store,
LogServiceInterface $log,
EventDispatcherInterface $dispatcher
) {
$this->setCommonDependencies($store, $log, $dispatcher);
}
}

View File

@@ -0,0 +1,111 @@
<?php
/*
* Copyright (C) 2025 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Entity;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class ApplicationScope
* @package Xibo\Entity
*/
class ApplicationScope implements \JsonSerializable
{
use EntityTrait;
/**
* @var string
*/
public $id;
/**
* @var string
*/
public $description;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
public function __serialize(): array
{
return $this->jsonSerialize();
}
public function __unserialize(array $data): void
{
foreach ($data as $key => $value) {
$this->{$key} = $value;
}
}
/**
* Get Id
* @return string
*/
public function getId()
{
return $this->id;
}
/**
* Check whether this scope has permission for this route
* @param string $method
* @param string $requestedRoute
* @return bool
*/
public function checkRoute(string $method, string $requestedRoute): bool
{
$routes = $this->getStore()->select('
SELECT `route`
FROM `oauth_scope_routes`
WHERE `scopeId` = :scope
AND `method` LIKE :method
', [
'scope' => $this->getId(),
'method' => '%' . $method . '%',
]);
$this->getLog()->debug('checkRoute: there are ' . count($routes) . ' potential routes for the scope '
. $this->getId() . ' with ' . $method);
// We need to look through each route and run the regex against our requested route.
$grantAccess = false;
foreach ($routes as $route) {
$regexResult = preg_match($route['route'], $requestedRoute);
if ($regexResult === 1) {
$grantAccess = true;
break;
}
}
return $grantAccess;
}
}

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

@@ -0,0 +1,108 @@
<?php
/*
* Copyright (C) 2022-2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Entity;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class AuditLog
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class AuditLog implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The Log Id")
* @var int
*/
public $logId;
/**
* @SWG\Property(description="The Log Date")
* @var int
*/
public $logDate;
/**
* @SWG\Property(description="The userId of the User that took this action")
* @var int
*/
public $userId;
/**
* @SWG\Property(description="Message describing the action taken")
* @var string
*/
public $message;
/**
* @SWG\Property(description="The effected entity")
* @var string
*/
public $entity;
/**
* @SWG\Property(description="The effected entityId")
* @var int
*/
public $entityId;
/**
* @SWG\Property(description="A JSON representation of the object after it was changed")
* @var string
*/
public $objectAfter;
/**
* @SWG\Property(description="The User Name of the User that took this action")
* @var string
*/
public $userName;
/**
* @SWG\Property(description="The IP Address of the User that took this action")
* @var string
*/
public $ipAddress;
/**
* @SWG\Property(description="Session history id.")
* @var int
*/
public $sessionHistoryId;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
}

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

@@ -0,0 +1,89 @@
<?php
/*
* Copyright (C) 2022 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Entity;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\DeadlockException;
/**
* Class Bandwidth
* @package Xibo\Entity
*
*/
class Bandwidth
{
use EntityTrait;
public static $REGISTER = 1;
public static $RF = 2;
public static $SCHEDULE = 3;
public static $GETFILE = 4;
public static $GETRESOURCE = 5;
public static $MEDIAINVENTORY = 6;
public static $NOTIFYSTATUS = 7;
public static $SUBMITSTATS = 8;
public static $SUBMITLOG = 9;
public static $REPORTFAULT = 10;
public static $SCREENSHOT = 11;
public static $GET_DATA = 12;
public static $GET_DEPENDENCY = 13;
public $displayId;
public $type;
public $size;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
public function save()
{
try {
// This runs on the "isolated" connection because we do not want a failure here to impact the
// main transaction we've just completed (we log bandwidth at the end).
// Running on a separate transaction is cleaner than committing what we already have (debatable)
$this->getStore()->updateWithDeadlockLoop('
INSERT INTO `bandwidth` (Month, Type, DisplayID, Size)
VALUES (:month, :type, :displayId, :size)
ON DUPLICATE KEY UPDATE Size = Size + :size2
', [
'month' => strtotime(date('m') . '/02/' . date('Y') . ' 00:00:00'),
'type' => $this->type,
'displayId' => $this->displayId,
'size' => $this->size,
'size2' => $this->size
], 'isolated', false, true);
} catch (DeadlockException $deadlockException) {
$this->getLog()->error('Deadlocked inserting bandwidth');
}
}
}

1031
lib/Entity/Campaign.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,56 @@
<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Entity;
/**
* Campaign Progress
*/
class CampaignProgress implements \JsonSerializable
{
/** @var int */
public $daysIn = 0;
/** @var int */
public $daysTotal = 0;
/** @var float */
public $targetPerDay = 0.0;
/** @var float */
public $progressTime = 0.0;
/** @var float */
public $progressTarget = 0.0;
/** @inheritDoc */
public function jsonSerialize(): array
{
return [
'daysIn' => $this->daysIn,
'daysTotal' => $this->daysTotal,
'targetPerDay' => $this->targetPerDay,
'progressTime' => $this->progressTime,
'progressTarget' => $this->progressTarget,
];
}
}

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

@@ -0,0 +1,353 @@
<?php
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Entity;
use Respect\Validation\Validator as v;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\InvalidArgumentException;
/**
* Class Command
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class Command implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(
* description="Command Id"
* )
* @var int
*/
public $commandId;
/**
* @SWG\Property(
* description="Command Name"
* )
* @var string
*/
public $command;
/**
* @SWG\Property(
* description="Unique Code"
* )
* @var string
*/
public $code;
/**
* @SWG\Property(
* description="Description"
* )
* @var string
*/
public $description;
/**
* @SWG\Property(
* description="User Id"
* )
* @var int
*/
public $userId;
/**
* @SWG\Property(
* description="Command String"
* )
* @var string
*/
public $commandString;
/**
* @SWG\Property(
* description="Validation String"
* )
* @var string
*/
public $validationString;
/**
* @SWG\Property(
* description="DisplayProfileId if specific to a Display Profile"
* )
* @var int
*/
public $displayProfileId;
/**
* @SWG\Property(
* description="Command String specific to the provided DisplayProfile"
* )
* @var string
*/
public $commandStringDisplayProfile;
/**
* @SWG\Property(
* description="Validation String specific to the provided DisplayProfile"
* )
* @var string
*/
public $validationStringDisplayProfile;
/**
* @SWG\Property(
* description="A comma separated list of player types this command is available on"
* )
* @var string
*/
public $availableOn;
/**
* @SWG\Property(
* description="Define if execution of this command should create an alert on success, failure, always or never."
* )
* @var string
*/
public $createAlertOn;
/**
* @SWG\Property(
* description="Create Alert On specific to the provided DisplayProfile."
* )
*/
public $createAlertOnDisplayProfile;
/**
* @SWG\Property(description="A comma separated list of groups/users with permissions to this Command")
* @var string
*/
public $groupsWithPermissions;
/**
* Command constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
/**
* Get Id
* @return int
*/
public function getId()
{
return $this->commandId;
}
/**
* Get OwnerId
* @return int
*/
public function getOwnerId()
{
return $this->userId;
}
/**
* @return string
*/
public function getCommandString()
{
return empty($this->commandStringDisplayProfile) ? $this->commandString : $this->commandStringDisplayProfile;
}
/**
* @return string
*/
public function getValidationString()
{
return empty($this->validationStringDisplayProfile)
? $this->validationString
: $this->validationStringDisplayProfile;
}
/**
* @return string
*/
public function getCreateAlertOn(): string
{
return empty($this->createAlertOnDisplayProfile)
? $this->createAlertOn
: $this->createAlertOnDisplayProfile;
}
/**
* @return array
*/
public function getAvailableOn()
{
return empty($this->availableOn) ? [] : explode(',', $this->availableOn);
}
/**
* @param string $type Player Type
* @return bool
*/
public function isAvailableOn($type)
{
$availableOn = $this->getAvailableOn();
return count($availableOn) <= 0 || in_array($type, $availableOn);
}
/**
* @return bool
*/
public function isReady()
{
return !empty($this->getCommandString());
}
/**
* Validate
* @throws InvalidArgumentException
*/
public function validate()
{
if (!v::stringType()->notEmpty()->length(1, 254)->validate($this->command)) {
throw new InvalidArgumentException(
__('Please enter a command name between 1 and 254 characters'),
'command'
);
}
if (!v::alpha('_')->NoWhitespace()->notEmpty()->length(1, 50)->validate($this->code)) {
throw new InvalidArgumentException(
__('Please enter a code between 1 and 50 characters containing only alpha characters and no spaces'),
'code'
);
}
if (!v::stringType()->length(0, 1000)->validate($this->description)) {
throw new InvalidArgumentException(
__('Please enter a description between 1 and 1000 characters'),
'description'
);
}
}
/**
* Save
* @param array $options
*
* @throws InvalidArgumentException
*/
public function save($options = [])
{
$options = array_merge($options, ['validate' => true]);
if ($options['validate']) {
$this->validate();
}
if ($this->commandId == null) {
$this->add();
} else {
$this->edit();
}
}
/**
* Delete
*/
public function delete()
{
$this->getStore()->update(
'DELETE FROM `command` WHERE `commandId` = :commandId',
['commandId' => $this->commandId]
);
}
private function add()
{
$this->commandId = $this->getStore()->insert('
INSERT INTO `command` (
`command`,
`code`,
`description`,
`userId`,
`commandString`,
`validationString`,
`availableOn`,
`createAlertOn`
)
VALUES (
:command,
:code,
:description,
:userId,
:commandString,
:validationString,
:availableOn,
:createAlertOn
)
', [
'command' => $this->command,
'code' => $this->code,
'description' => $this->description,
'userId' => $this->userId,
'commandString' => $this->commandString,
'validationString' => $this->validationString,
'availableOn' => $this->availableOn,
'createAlertOn' => $this->createAlertOn
]);
}
private function edit()
{
$this->getStore()->update('
UPDATE `command` SET
`command` = :command,
`code` = :code,
`description` = :description,
`userId` = :userId,
`commandString` = :commandString,
`validationString` = :validationString,
`availableOn` = :availableOn,
`createAlertOn` = :createAlertOn
WHERE `commandId` = :commandId
', [
'command' => $this->command,
'code' => $this->code,
'description' => $this->description,
'userId' => $this->userId,
'commandId' => $this->commandId,
'commandString' => $this->commandString,
'validationString' => $this->validationString,
'availableOn' => $this->availableOn,
'createAlertOn' => $this->createAlertOn
]);
}
}

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

@@ -0,0 +1,135 @@
<?php
/*
* Copyright (c) 2022 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Entity;
use Xibo\Connector\ConnectorInterface;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\InvalidArgumentException;
/**
* Represents the database object for a Connector
*
* @SWG\Definition()
*/
class Connector implements \JsonSerializable
{
use EntityTrait;
// Status properties
public $isInstalled = true;
public $isSystem = true;
// Database properties
public $connectorId;
public $className;
public $settings;
public $isEnabled;
public $isVisible;
// Decorated properties
public $title;
public $description;
public $thumbnail;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
/**
* @param \Xibo\Connector\ConnectorInterface $connector
* @return $this
*/
public function decorate(ConnectorInterface $connector): Connector
{
$this->title = $connector->getTitle();
$this->description = $connector->getDescription();
$this->thumbnail = $connector->getThumbnail();
if (empty($this->thumbnail)) {
$this->thumbnail = 'theme/default/img/connectors/placeholder.png';
}
return $this;
}
public function save()
{
if ($this->connectorId == null || $this->connectorId == 0) {
$this->add();
} else {
$this->edit();
}
}
private function add()
{
$this->connectorId = $this->getStore()->insert('
INSERT INTO `connectors` (`className`, `isEnabled`, `isVisible`, `settings`)
VALUES (:className, :isEnabled, :isVisible, :settings)
', [
'className' => $this->className,
'isEnabled' => $this->isEnabled,
'isVisible' => $this->isVisible,
'settings' => json_encode($this->settings)
]);
}
private function edit()
{
$this->getStore()->update('
UPDATE `connectors` SET
`className` = :className,
`isEnabled` = :isEnabled,
`isVisible` = :isVisible,
`settings` = :settings
WHERE connectorId = :connectorId
', [
'connectorId' => $this->connectorId,
'className' => $this->className,
'isEnabled' => $this->isEnabled,
'isVisible' => $this->isVisible,
'settings' => json_encode($this->settings)
]);
}
/**
* @return void
* @throws \Xibo\Support\Exception\InvalidArgumentException
*/
public function delete()
{
if ($this->isSystem) {
throw new InvalidArgumentException(__('Sorry we cannot delete a system connector.'), 'isSystem');
}
$this->getStore()->update('DELETE FROM `connectors` WHERE connectorId = :connectorId', [
'connectorId' => $this->connectorId
]);
}
}

1412
lib/Entity/DataSet.php Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,59 @@
<?php
/*
* Copyright (c) 2022 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Entity;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class DataSetColumnType
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class DataSetColumnType implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The ID for this DataSetColumnType")
* @var int
*/
public $dataSetColumnTypeId;
/**
* @SWG\Property(description="The name for this DataSetColumnType")
* @var string
*/
public $dataSetColumnType;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
}

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

@@ -0,0 +1,164 @@
<?php
/*
* Copyright (c) 2022 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Entity;
use Xibo\Helper\Random;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class DataSetRss
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class DataSetRss implements \JsonSerializable
{
use EntityTrait;
public $id;
public $dataSetId;
public $titleColumnId;
public $summaryColumnId;
public $contentColumnId;
public $publishedDateColumnId;
public $psk;
public $title;
public $author;
public $sort;
public $filter;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
/**
* @return array|mixed
*/
public function getFilter()
{
return ($this->filter == '') ? ['filter' => '', 'useFilteringClause' => 0, 'filterClauses' => []] : json_decode($this->filter, true);
}
/**
* @return array|mixed
*/
public function getSort()
{
return ($this->sort == '') ? ['sort' => '', 'useOrderingClause' => 0, 'orderClauses' => []] : json_decode($this->sort, true);
}
/**
* Save
*/
public function save()
{
if ($this->id == null) {
$this->add();
$this->audit($this->id, 'Added', []);
} else {
$this->edit();
$this->audit($this->id, 'Saved');
}
}
/**
* @return $this
* @throws \Exception
*/
public function setNewPsk()
{
$this->psk = Random::generateString(12);
return $this;
}
/**
* Delete
*/
public function delete()
{
$this->getStore()->update('DELETE FROM `datasetrss` WHERE id = :id', ['id' => $this->id]);
$this->audit($this->id, 'Deleted');
}
private function add()
{
$this->id = $this->getStore()->insert('
INSERT INTO datasetrss (dataSetId, psk, title, author, titleColumnId, summaryColumnId, contentColumnId, publishedDateColumnId, sort, filter) VALUES
(:dataSetId, :psk, :title, :author, :titleColumnId, :summaryColumnId, :contentColumnId, :publishedDateColumnId, :sort, :filter)
', [
'dataSetId' => $this->dataSetId,
'psk' => $this->psk,
'title' => $this->title,
'author' => $this->author,
'titleColumnId' => $this->titleColumnId,
'summaryColumnId' => $this->summaryColumnId,
'contentColumnId' => $this->contentColumnId,
'publishedDateColumnId' => $this->publishedDateColumnId,
'sort' => $this->sort,
'filter' => $this->filter
]);
}
private function edit()
{
$this->getStore()->update('
UPDATE `datasetrss` SET
psk = :psk,
title = :title,
author = :author,
titleColumnId = :titleColumnId,
summaryColumnId = :summaryColumnId,
contentColumnId = :contentColumnId,
publishedDateColumnId = :publishedDateColumnId,
sort = :sort,
filter = :filter
WHERE id = :id
', [
'id' => $this->id,
'psk' => $this->psk,
'title' => $this->title,
'author' => $this->author,
'titleColumnId' => $this->titleColumnId,
'summaryColumnId' => $this->summaryColumnId,
'contentColumnId' => $this->contentColumnId,
'publishedDateColumnId' => $this->publishedDateColumnId,
'sort' => $this->sort,
'filter' => $this->filter
]);
}
}

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

@@ -0,0 +1,61 @@
<?php
/*
* Copyright (c) 2022 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Entity;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class DataType
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class DataType implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The ID for this DataType")
* @var int
*/
public $dataTypeId;
/**
* @SWG\Property(description="The Name for this DataType")
* @var string
*/
public $dataType;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
}

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

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

1576
lib/Entity/Display.php Normal file

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,225 @@
<?php
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Entity;
use Carbon\Carbon;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class DisplayEvent
* @package Xibo\Entity
*/
class DisplayEvent implements \JsonSerializable
{
use EntityTrait;
public $displayEventId;
public $displayId;
public $eventDate;
public $start;
public $end;
public $eventTypeId;
public $refId;
public $detail;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param EventDispatcherInterface $dispatcher
*/
public function __construct(
StorageServiceInterface $store,
LogServiceInterface $log,
EventDispatcherInterface $dispatcher
) {
$this->setCommonDependencies($store, $log, $dispatcher);
}
/**
* Save displayevent
* @return void
*/
public function save(): void
{
if ($this->displayEventId == null) {
$this->add();
} else {
$this->edit();
}
}
/**
* Add a new displayevent
* @return void
*/
private function add(): void
{
$this->displayEventId = $this->getStore()->insert('
INSERT INTO `displayevent` (eventDate, start, end, displayID, eventTypeId, refId, detail)
VALUES (:eventDate, :start, :end, :displayId, :eventTypeId, :refId, :detail)
', [
'eventDate' => Carbon::now()->format('U'),
'start' => $this->start,
'end' => $this->end,
'displayId' => $this->displayId,
'eventTypeId' => $this->eventTypeId,
'refId' => $this->refId,
'detail' => $this->detail,
]);
}
/**
* Edit displayevent
* @return void
*/
private function edit(): void
{
$this->getStore()->update('
UPDATE displayevent
SET end = :end,
displayId = :displayId,
eventTypeId = :eventTypeId,
refId = :refId,
detail = :detail
WHERE displayEventId = :displayEventId
', [
'displayEventId' => $this->displayEventId,
'end' => $this->end,
'displayId' => $this->displayId,
'eventTypeId' => $this->eventTypeId,
'refId' => $this->refId,
'detail' => $this->detail,
]);
}
/**
* Record end date for specified display and event type.
* @param int $displayId
* @param int|null $date
* @param int $eventTypeId
* @return void
*/
public function eventEnd(int $displayId, int $eventTypeId = 1, string $detail = null, ?int $date = null): void
{
$this->getLog()->debug(
sprintf(
'displayEvent : end display alert for eventType %s and displayId %d',
$this->getEventNameFromId($eventTypeId),
$displayId
)
);
$this->getStore()->update(
"UPDATE `displayevent` SET `end` = :toDt, `detail` = CONCAT_WS('. ', NULLIF(`detail`, ''), :detail)
WHERE displayId = :displayId
AND `end` IS NULL
AND eventTypeId = :eventTypeId",
[
'toDt' => $date ?? Carbon::now()->format('U'),
'displayId' => $displayId,
'eventTypeId' => $eventTypeId,
'detail' => $detail,
]
);
}
/**
* Record end date for specified display, event type and refId
* @param int $displayId
* @param int $eventTypeId
* @param int $refId
* @param int|null $date
* @return void
*/
public function eventEndByReference(int $displayId, int $eventTypeId, int $refId, string $detail = null, ?int $date = null): void
{
$this->getLog()->debug(
sprintf(
'displayEvent : end display alert for refId %d, displayId %d and eventType %s',
$refId,
$displayId,
$this->getEventNameFromId($eventTypeId),
)
);
// When updating the event end, concatenate the end message to the current message
$this->getStore()->update(
"UPDATE `displayevent` SET
`end` = :toDt,
`detail` = CONCAT_WS('. ', NULLIF(`detail`, ''), :detail)
WHERE displayId = :displayId
AND `end` IS NULL
AND eventTypeId = :eventTypeId
AND refId = :refId",
[
'toDt' => $date ?? Carbon::now()->format('U'),
'displayId' => $displayId,
'eventTypeId' => $eventTypeId,
'refId' => $refId,
'detail' => $detail,
]
);
}
/**
* Match event type string from log to eventTypeId in database.
* @param string $eventType
* @return int
*/
public function getEventIdFromString(string $eventType): int
{
return match ($eventType) {
'Display Up/down' => 1,
'App Start' => 2,
'Power Cycle' => 3,
'Network Cycle' => 4,
'TV Monitoring' => 5,
'Player Fault' => 6,
'Command' => 7,
default => 8
};
}
/**
* Match eventTypeId from database to string event name.
* @param int $eventTypeId
* @return string
*/
public function getEventNameFromId(int $eventTypeId): string
{
return match ($eventTypeId) {
1 => __('Display Up/down'),
2 => __('App Start'),
3 => __('Power Cycle'),
4 => __('Network Cycle'),
5 => __('TV Monitoring'),
6 => __('Player Fault'),
7 => __('Command'),
default => __('Other')
};
}
}

1115
lib/Entity/DisplayGroup.php Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,61 @@
<?php
/*
* Copyright (c) 2022 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Entity;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class DisplayType
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class DisplayType implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(description="The ID for this DisplayType")
* @var int
*/
public $displayTypeId;
/**
* @SWG\Property(description="The Name for this DisplayType")
* @var string
*/
public $displayType;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
*/
public function __construct($store, $log, $dispatcher)
{
$this->setCommonDependencies($store, $log, $dispatcher);
}
}

Some files were not shown because too many files have changed in this diff Show More