Initial Upload

This commit is contained in:
Matt Batchelder
2025-12-02 10:32:59 -05:00
commit 05ce0da296
2240 changed files with 467811 additions and 0 deletions

150
lib/Middleware/Actions.php Normal file
View File

@@ -0,0 +1,150 @@
<?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\Middleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface as Middleware;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\App as App;
use Slim\Routing\RouteContext;
use Xibo\Entity\User;
use Xibo\Entity\UserNotification;
use Xibo\Factory\UserNotificationFactory;
use Xibo\Helper\Environment;
/**
* Class Actions
* Web Actions
* @package Xibo\Middleware
*/
class Actions implements Middleware
{
/* @var App $app */
private $app;
public function __construct($app)
{
$this->app = $app;
}
public function process(Request $request, RequestHandler $handler): Response
{
// Do not proceed unless we have completed an upgrade
if (Environment::migrationPending()) {
return $handler->handle($request);
}
$app = $this->app;
$container = $app->getContainer();
// Get the current route pattern
$routeContext = RouteContext::fromRequest($request);
$route = $routeContext->getRoute();
$resource = $route->getPattern();
$routeParser = $app->getRouteCollector()->getRouteParser();
// Do we have a user set?
/** @var User $user */
$user = $container->get('user');
// Only process notifications if we are a full request
if (!$this->isAjax($request)) {
if ($user->userId != null
&& $container->get('session')->isExpired() == 0
&& $user->featureEnabled('drawer')
) {
// Notifications
$notifications = [];
$extraNotifications = 0;
/** @var UserNotificationFactory $factory */
$factory = $container->get('userNotificationFactory');
// Is the CMS Docker stack in DEV mode? (this will be true for dev and test)
if (Environment::isDevMode()) {
$notifications[] = $factory->create('CMS IN DEV MODE');
$extraNotifications++;
} else {
// We're not in DEV mode and therefore install/index.php shouldn't be there.
if ($user->userTypeId == 1 && file_exists(PROJECT_ROOT . '/web/install/index.php')) {
$container->get('logger')->notice('Install.php exists and shouldn\'t');
$notifications[] = $factory->create(
__('There is a problem with this installation, the web/install folder should be deleted.')
);
$extraNotifications++;
// Test for web in the URL.
$url = $request->getUri();
if (!Environment::checkUrl($url)) {
$container->get('logger')->notice('Suspicious URL detected - it is very unlikely that /web/ should be in the URL. URL is ' . $url);
$notifications[] = $factory->create(__('CMS configuration warning, it is very unlikely that /web/ should be in the URL. This usually means that the DocumentRoot of the web server is wrong and may put your CMS at risk if not corrected.'));
$extraNotifications++;
}
}
}
// User notifications
$notifications = array_merge($notifications, $factory->getMine());
// If we aren't already in a notification interrupt, then check to see if we should be
if ($resource != '/drawer/notification/interrupt/{id}' && !$this->isAjax($request) && $container->get('session')->isExpired() != 1) {
foreach ($notifications as $notification) {
/** @var UserNotification $notification */
if ($notification->isInterrupt == 1 && $notification->read == 0) {
$container->get('flash')->addMessage('interruptedUrl', $resource);
return $handler->handle($request)
->withHeader('Location', $routeParser->urlFor('notification.interrupt', ['id' => $notification->notificationId]))
->withHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
->withHeader('Pragma',' no-cache')
->withHeader('Expires',' 0');
}
}
}
$container->get('view')->offsetSet('notifications', $notifications);
$container->get('view')->offsetSet('notificationCount', $factory->countMyUnread() + $extraNotifications);
}
}
if (!$this->isAjax($request) && $user->isPasswordChangeRequired == 1 && $resource != '/user/page/password') {
return $handler->handle($request)
->withStatus(302)
->withHeader('Location', $routeParser->urlFor('user.force.change.password.page'));
}
return $handler->handle($request);
}
/**
* Is the provided request from AJAX
* @param \Psr\Http\Message\ServerRequestInterface $request
* @return bool
*/
private function isAjax(Request $request)
{
return strtolower($request->getHeaderLine('X-Requested-With')) === 'xmlhttprequest';
}
}

View File

@@ -0,0 +1,110 @@
<?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\Middleware;
use League\OAuth2\Server\Grant\AuthCodeGrant;
use League\OAuth2\Server\Grant\RefreshTokenGrant;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface as Middleware;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\App as App;
use Xibo\OAuth\RefreshTokenRepository;
use Xibo\Support\Exception\ConfigurationException;
/**
* Class ApiAuthentication
* This middleware protects the AUTH entry point
* @package Xibo\Middleware
*/
class ApiAuthentication implements Middleware
{
/* @var App $app */
private $app;
/**
* ApiAuthorizationOAuth constructor.
* @param $app
*/
public function __construct($app)
{
$this->app = $app;
}
/**
* @param \Psr\Http\Message\ServerRequestInterface $request
* @param \Psr\Http\Server\RequestHandlerInterface $handler
* @return \Psr\Http\Message\ResponseInterface
* @throws \Xibo\Support\Exception\ConfigurationException
*/
public function process(Request $request, RequestHandler $handler): Response
{
$app = $this->app;
$container = $app->getContainer();
// DI in the server
$container->set('server', function(ContainerInterface $container) {
/** @var \Xibo\Service\LogServiceInterface $logger */
$logger = $container->get('logService');
// API Keys
$apiKeyPaths = $container->get('configService')->getApiKeyDetails();
$privateKey = $apiKeyPaths['privateKeyPath'];
$encryptionKey = $apiKeyPaths['encryptionKey'];
try {
$server = new \League\OAuth2\Server\AuthorizationServer(
$container->get('applicationFactory'),
new \Xibo\OAuth\AccessTokenRepository($logger, $container->get('pool'), $container->get('applicationFactory')),
$container->get('applicationScopeFactory'),
$privateKey,
$encryptionKey
);
// Grant Types
$server->enableGrantType(
new \League\OAuth2\Server\Grant\ClientCredentialsGrant(),
new \DateInterval('PT1H')
);
$server->enableGrantType(
new AuthCodeGrant(
new \Xibo\OAuth\AuthCodeRepository(),
new \Xibo\OAuth\RefreshTokenRepository($logger, $container->get('pool')),
new \DateInterval('PT10M')
),
new \DateInterval('PT1H')
);
$server->enableGrantType(new RefreshTokenGrant(new RefreshTokenRepository($logger, $container->get('pool'))));
return $server;
} catch (\LogicException $exception) {
$logger->error($exception->getMessage());
throw new ConfigurationException('API configuration problem, consult your administrator');
}
});
return $handler->handle($request->withAttribute('_entryPoint', 'auth'));
}
}

View File

@@ -0,0 +1,215 @@
<?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\Middleware;
use Carbon\Carbon;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\ResourceServer;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface as Middleware;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\App as App;
use Slim\Routing\RouteContext;
use Xibo\Factory\ApplicationScopeFactory;
use Xibo\Helper\UserLogProcessor;
use Xibo\OAuth\AccessTokenRepository;
use Xibo\Support\Exception\AccessDeniedException;
/**
* Class ApiAuthenticationOAuth
* This middleware protects the API entry point
* @package Xibo\Middleware
*/
class ApiAuthorization implements Middleware
{
/* @var App $app */
private $app;
/**
* ApiAuthenticationOAuth constructor.
* @param $app
*/
public function __construct($app)
{
$this->app = $app;
}
/**
* @param Request $request
* @param RequestHandler $handler
* @return Response
* @throws OAuthServerException
* @throws \Xibo\Support\Exception\AccessDeniedException
* @throws \Xibo\Support\Exception\ConfigurationException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function process(Request $request, RequestHandler $handler): Response
{
/* @var \Xibo\Entity\User $user */
$user = null;
/** @var \Xibo\Service\LogServiceInterface $logger */
$logger = $this->app->getContainer()->get('logService');
// Setup the authorization server
$this->app->getContainer()->set('server', function (ContainerInterface $container) use ($logger) {
// oAuth Resource
$apiKeyPaths = $container->get('configService')->getApiKeyDetails();
$accessTokenRepository = new AccessTokenRepository(
$logger,
$container->get('pool'),
$container->get('applicationFactory')
);
return new ResourceServer(
$accessTokenRepository,
$apiKeyPaths['publicKeyPath']
);
});
/** @var ResourceServer $server */
$server = $this->app->getContainer()->get('server');
$validatedRequest = $server->validateAuthenticatedRequest($request);
// We have a valid JWT/token
// get our user from it.
$userFactory = $this->app->getContainer()->get('userFactory');
// What type of Access Token to we have? Client Credentials or AuthCode
// client_credentials grants are issued with the correct oauth_user_id in the token, so we don't need to
// distinguish between them here! nice!
$userId = $validatedRequest->getAttribute('oauth_user_id');
$user = $userFactory->getById($userId);
$user->setChildAclDependencies($this->app->getContainer()->get('userGroupFactory'));
$user->load();
// Block access by retired users.
if ($user->retired === 1) {
throw new AccessDeniedException(__('Sorry this account does not exist or cannot be authenticated.'));
}
// We must check whether this user has access to the route they have requested.
// Get the current route pattern
$routeContext = RouteContext::fromRequest($request);
$route = $routeContext->getRoute();
$resource = $route->getPattern();
// Allow public routes
if (!in_array($resource, $validatedRequest->getAttribute('publicRoutes', []))) {
$request = $request->withAttribute('public', false);
// Check that the Scopes granted to this token are allowed access to the route/method of this request
/** @var ApplicationScopeFactory $applicationScopeFactory */
$applicationScopeFactory = $this->app->getContainer()->get('applicationScopeFactory');
$scopes = $validatedRequest->getAttribute('oauth_scopes');
// If there are no scopes in the JWT, we should not authorise
// An older client which makes a request with no scopes should get an access token with
// all scopes configured for the application
if (!is_array($scopes) || count($scopes) <= 0) {
throw new AccessDeniedException();
}
$logger->debug('Scopes provided with request: ' . count($scopes));
// Check all scopes in the request before we deny access.
$grantAccess = false;
foreach ($scopes as $scope) {
// If I have the "all" scope granted then there isn't any need to test further
if ($scope === 'all') {
$grantAccess = true;
break;
}
$logger->debug(
sprintf(
'Test authentication for %s %s against scope %s',
$resource,
$request->getMethod(),
$scope
)
);
// Check the route and request method
if ($applicationScopeFactory->getById($scope)->checkRoute($request->getMethod(), $resource)) {
$grantAccess = true;
break;
}
}
if (!$grantAccess) {
throw new AccessDeniedException(__('Access to this route is denied for this scope'));
}
} else {
// Public request
$validatedRequest = $validatedRequest->withAttribute('public', true);
}
$requestId = $this->app->getContainer()->get('store')->insert('
INSERT INTO `application_requests_history` (
userId,
applicationId,
url,
method,
startTime,
endTime,
duration
)
VALUES (
:userId,
:applicationId,
:url,
:method,
:startTime,
:endTime,
:duration
)
', [
'userId' => $user->userId,
'applicationId' => $validatedRequest->getAttribute('oauth_client_id'),
'url' => htmlspecialchars($request->getUri()->getPath()),
'method' => $request->getMethod(),
'startTime' => Carbon::now(),
'endTime' => Carbon::now(),
'duration' => 0
], 'api_requests_history');
$this->app->getContainer()->get('store')->commitIfNecessary('api_requests_history');
$logger->setUserId($user->userId);
$this->app->getContainer()->set('user', $user);
$logger->setRequestId($requestId);
// Add this request information to the logger.
$logger->getLoggerInterface()->pushProcessor(new UserLogProcessor(
$user->userId,
null,
$requestId,
));
return $handler->handle($validatedRequest->withAttribute('name', 'API'));
}
}

129
lib/Middleware/ApiView.php Normal file
View File

@@ -0,0 +1,129 @@
<?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\Middleware;
use Slim\Slim;
use Slim\View;
use Xibo\Helper\HttpsDetect;
class ApiView extends View
{
public function render($template = '', $data = NULL)
{
$app = Slim::getInstance();
// JSONP Callback?
$jsonPCallback = $app->request->get('callback', null);
// Don't envelope unless requested
if ($jsonPCallback != null || $app->request()->get('envelope', 0) == 1 || $app->getName() == 'test') {
// Envelope
$response = $this->all();
// append error bool
if (!$this->has('success') || !$this->get('success')) {
$response['success'] = false;
}
// append status code
$response['status'] = $app->response()->getStatus();
// add flash messages
if (isset($this->data->flash) && is_object($this->data->flash)){
$flash = $this->data->flash->getMessages();
if (count($flash)) {
$response['flash'] = $flash;
} else {
unset($response['flash']);
}
}
// Enveloped responses always return 200
$app->status(200);
} else {
// Don't envelope
// Set status
$app->status(intval($this->get('status')));
// Are we successful?
if (!$this->has('success') || !$this->get('success')) {
// Error condition
$response = [
'error' => [
'message' => $this->get('message'),
'code' => intval($this->get('status')),
'data' => $this->get('data')
]
];
}
else {
// Are we a grid?
if ($this->get('grid') == true) {
// Set the response to our data['data'] object
$grid = $this->get('data');
$response = $grid['data'];
// Total Number of Rows
$totalRows = $grid['recordsTotal'];
// Set some headers indicating our next/previous pages
$start = $app->sanitizerService->getInt('start', 0);
$size = $app->sanitizerService->getInt('length', 10);
$linkHeader = '';
$url = (new HttpsDetect())->getRootUrl() . $app->request()->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"';
$app->response()->header('X-Total-Count', $totalRows);
$app->response()->header('Link', $linkHeader);
} else {
// Set the response to our data object
$response = $this->get('data');
}
}
}
// JSON header
$app->response()->header('Content-Type', 'application/json');
if ($jsonPCallback !== null) {
$app->response()->body($jsonPCallback.'('.json_encode($response).')');
} else {
$app->response()->body(json_encode($response, JSON_PRETTY_PRINT));
}
$app->stop();
}
}

View File

@@ -0,0 +1,116 @@
<?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\Middleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface as Middleware;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
/**
* Class AuthenticationBase
* @package Xibo\Middleware
*/
abstract class AuthenticationBase implements Middleware, AuthenticationInterface
{
use AuthenticationTrait;
/**
* Uses a Hook to check every call for authorization
* Will redirect to the login route if the user is unauthorized
*
* @param Request $request
* @param RequestHandler $handler
* @return Response
* @throws \Xibo\Support\Exception\AccessDeniedException
* @throws \Xibo\Support\Exception\ConfigurationException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function process(Request $request, RequestHandler $handler): Response
{
// This Middleware protects the Web Route, so we update the request with that name
$request = $request->withAttribute('_entryPoint', 'web');
// Add any authentication specific request modifications
$request = $this->addToRequest($request);
// Start with an empty user.
$user = $this->getEmptyUser();
// Get the current route pattern
$resource = $this->getRoutePattern($request);
// Check to see if this is a public resource (there are only a few, so we have them in an array)
if (!in_array($resource, $this->getPublicRoutes($request))) {
$request = $request->withAttribute('public', false);
// Need to check
if ($user->hasIdentity() && !$this->getSession()->isExpired()) {
// Replace our user with a fully loaded one
$user = $this->getUser(
$user->userId,
$request->getAttribute('ip_address'),
$this->getSession()->get('sessionHistoryId')
);
// We are authenticated, override with the populated user object
$this->setUserForRequest($user);
// Handle the rest of the Middleware stack and return
return $handler->handle($request);
} else {
// Session has expired or the user is already logged out.
// in either case, capture the route
$this->rememberRoute($request->getUri()->getPath());
$this->getLog()->debug('not in public routes, expired, should redirect to login');
// We update the last accessed date on the user here, if there was one logged in at this point
if ($user->hasIdentity()) {
$user->touch();
}
// Issue appropriate logout depending on the type of web request
return $this->redirectToLogin($request);
}
} else {
// This is a public route.
$request = $request->withAttribute('public', true);
// If we are expired and come from ping/clock, then we redirect
if ($this->shouldRedirectPublicRoute($resource)) {
$this->getLog()->debug('should redirect to login , resource is ' . $resource);
if ($user->hasIdentity()) {
$user->touch();
}
// Issue appropriate logout depending on the type of web request
return $this->redirectToLogin($request);
} else {
// We handle the rest of the request, unauthenticated.
return $handler->handle($request);
}
}
}
}

View File

@@ -0,0 +1,69 @@
<?php
/**
* Copyright (C) 2020 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Middleware;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\App;
/**
* Interface AuthenticationInterface
* @package Xibo\Middleware
*/
interface AuthenticationInterface
{
/**
* @param \Slim\App $app
* @return mixed
*/
public function setDependencies(App $app);
/**
* @return $this
*/
public function addRoutes();
/**
* @param Request $request
* @return \Psr\Http\Message\ResponseInterface|\Slim\Http\Response
*/
public function redirectToLogin(Request $request);
/**
* @param Request $request
* @return array
*/
public function getPublicRoutes(Request $request);
/**
* Should this public route be redirected to login when the session is expired?
* @param string $route
* @return bool
*/
public function shouldRedirectPublicRoute($route);
/**
* @param \Psr\Http\Message\ServerRequestInterface $request
* @return Request
*/
public function addToRequest(Request $request);
}

View File

@@ -0,0 +1,209 @@
<?php
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Middleware;
use Nyholm\Psr7\Factory\Psr17Factory;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\App;
use Slim\Http\Factory\DecoratedResponseFactory;
use Slim\Routing\RouteContext;
use Xibo\Entity\User;
use Xibo\Helper\HttpsDetect;
use Xibo\Helper\UserLogProcessor;
/**
* Trait AuthenticationTrait
* @package Xibo\Middleware
*/
trait AuthenticationTrait
{
/* @var App $app */
protected $app;
/**
* Set dependencies
* @param App $app
* @return $this
*/
public function setDependencies(App $app)
{
$this->app = $app;
return $this;
}
/**
* @return \Xibo\Service\ConfigServiceInterface
*/
protected function getConfig()
{
return $this->app->getContainer()->get('configService');
}
/**
* @return \Xibo\Helper\Session
*/
protected function getSession()
{
return $this->app->getContainer()->get('session');
}
/**
* @return \Xibo\Service\LogServiceInterface
*/
protected function getLog()
{
return $this->app->getContainer()->get('logService');
}
/**
* @param $array
* @return \Xibo\Support\Sanitizer\SanitizerInterface
*/
protected function getSanitizer($array)
{
return $this->app->getContainer()->get('sanitizerService')->getSanitizer($array);
}
/**
* @return \Xibo\Factory\UserFactory
*/
protected function getUserFactory()
{
return $this->app->getContainer()->get('userFactory');
}
/**
* @return \Xibo\Factory\UserGroupFactory
*/
protected function getUserGroupFactory()
{
return $this->app->getContainer()->get('userGroupFactory');
}
/**
* @return \Xibo\Entity\User
*/
protected function getEmptyUser()
{
$container = $this->app->getContainer();
/** @var User $user */
$user = $container->get('userFactory')->create();
$user->setChildAclDependencies($container->get('userGroupFactory'));
return $user;
}
/**
* @param int $userId
* @param string $ip
* @param int $sessionHistoryId
* @return \Xibo\Entity\User
*/
protected function getUser($userId, $ip, $sessionHistoryId): User
{
$container = $this->app->getContainer();
$user = $container->get('userFactory')->getById($userId);
// Pass the page factory into the user object, so that it can check its page permissions
$user->setChildAclDependencies($container->get('userGroupFactory'));
// Load the user
$user->load(false);
// Configure the log service with the logged in user id
$container->get('logService')->setUserId($user->userId);
$container->get('logService')->setIpAddress($ip);
$container->get('logService')->setSessionHistoryId($sessionHistoryId);
return $user;
}
/**
* @param \Xibo\Entity\User $user
*/
protected function setUserForRequest($user)
{
$container = $this->app->getContainer();
$container->set('user', $user);
// Add this users information to the logger
$this->getLog()->getLoggerInterface()->pushProcessor(new UserLogProcessor(
$user->userId,
$this->getLog()->getSessionHistoryId(),
null,
));
}
/**
* @param Request $request
* @return string
*/
protected function getRoutePattern($request)
{
$routeContext = RouteContext::fromRequest($request);
$route = $routeContext->getRoute();
return $route->getPattern();
}
/**
* @return \Slim\Interfaces\RouteParserInterface
*/
protected function getRouteParser()
{
return $this->app->getRouteCollector()->getRouteParser();
}
/**
* @param \Psr\Http\Message\ServerRequestInterface $request
* @return bool
*/
protected function isAjax(Request $request)
{
return strtolower($request->getHeaderLine('X-Requested-With')) === 'xmlhttprequest';
}
/**
* @param \Psr\Http\Message\ServerRequestInterface $request
* @return \Psr\Http\Message\ResponseInterface
*/
protected function createResponse(Request $request)
{
// Create a new response
$nyholmFactory = new Psr17Factory();
$decoratedResponseFactory = new DecoratedResponseFactory($nyholmFactory, $nyholmFactory);
return HttpsDetect::decorateWithStsIfNecessary(
$this->getConfig(),
$request,
$decoratedResponseFactory->createResponse()
);
}
/**
* @param string $route
*/
protected function rememberRoute($route)
{
$this->app->getContainer()->get('flash')->addMessage('priorRoute', $route);
}
}

View File

@@ -0,0 +1,175 @@
<?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\Middleware;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Http\Response;
use Slim\Http\ServerRequest;
use Xibo\Helper\ApplicationState;
use Xibo\Helper\LogoutTrait;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\NotFoundException;
/**
* Class CASAuthentication
* @package Xibo\Middleware
*
* Provide CAS authentication to Xibo configured via settings.php.
*
* This class was originally contributed by Emmanuel Blindauer
*/
class CASAuthentication extends AuthenticationBase
{
use LogoutTrait;
/**
* @return $this
*/
public function addRoutes()
{
$app = $this->app;
$app->getContainer()->set('logoutRoute', 'cas.logout');
$app->map(['GET', 'POST'], '/cas/login', function (ServerRequest $request, Response $response) {
// Initiate CAS SSO
$this->initCasClient();
\phpCAS::setNoCasServerValidation();
// Login happens here
\phpCAS::forceAuthentication();
$username = \phpCAS::getUser();
try {
$user = $this->getUserFactory()->getByName($username);
} catch (NotFoundException $e) {
throw new AccessDeniedException('Unable to authenticate');
}
if ($user->retired === 1) {
throw new AccessDeniedException('Sorry this account does not exist or cannot be authenticated.');
}
if (isset($user) && $user->userId > 0) {
// Load User
$this->getUser(
$user->userId,
$request->getAttribute('ip_address'),
$this->getSession()->get('sessionHistoryId')
);
// Overwrite our stored user with this new object.
$this->setUserForRequest($user);
// Switch Session ID's
$this->getSession()->setIsExpired(0);
$this->getSession()->regenerateSessionId();
$this->getSession()->setUser($user->userId);
$user->touch();
// Audit Log
// Set the userId on the log object
$this->getLog()->audit('User', $user->userId, 'Login Granted via CAS', [
'UserAgent' => $request->getHeader('User-Agent')
]);
}
return $response->withRedirect($this->getRouteParser()->urlFor('home'));
})->setName('cas.login');
// Service for the logout of the user.
// End the CAS session and the application session
$app->get('/cas/logout', function (ServerRequest $request, Response $response) {
// The order is first: local session to destroy, second the cas session
// because phpCAS::logout() redirects to CAS server
$this->completeLogoutFlow(
$this->getUser(
$_SESSION['userid'],
$request->getAttribute('ip_address'),
$_SESSION['sessionHistoryId']
),
$this->getSession(),
$this->getLog(),
$request
);
$this->initCasClient();
\phpCAS::logout();
})->setName('cas.logout');
return $this;
}
/**
* Initialise the CAS client
*/
private function initCasClient()
{
$settings = $this->getConfig()->casSettings['config'];
\phpCAS::client(
CAS_VERSION_2_0,
$settings['server'],
intval($settings['port']),
$settings['uri'],
$settings['service_base_url']
);
}
/** @inheritDoc */
public function redirectToLogin(Request $request)
{
if ($this->isAjax($request)) {
return $this->createResponse($request)
->withJson(ApplicationState::asRequiresLogin());
} else {
return $this->createResponse($request)
->withRedirect($this->getRouteParser()->urlFor('login'));
}
}
/** @inheritDoc */
public function getPublicRoutes(Request $request)
{
return array_merge($request->getAttribute('publicRoutes', []), [
'/cas/login',
'/cas/logout',
]);
}
/** @inheritDoc */
public function shouldRedirectPublicRoute($route)
{
return $this->getSession()->isExpired() && ($route == '/login/ping' || $route == 'clock');
}
/** @inheritDoc */
public function addToRequest(Request $request)
{
return $request->withAttribute(
'excludedCsrfRoutes',
array_merge($request->getAttribute('excludedCsrfRoutes', []), ['/cas/login', '/cas/logout'])
);
}
}

View File

@@ -0,0 +1,82 @@
<?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\Middleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\App;
/**
* This middleware is used to register connectors.
*/
class ConnectorMiddleware implements MiddlewareInterface
{
/* @var App $app */
private $app;
public function __construct($app)
{
$this->app = $app;
}
/**
* @param Request $request
* @param RequestHandler $handler
* @return Response
*/
public function process(Request $request, RequestHandler $handler): Response
{
$app = $this->app;
// Set connectors
self::setConnectors($app);
// Next middleware
return $handler->handle($request);
}
/**
* Set connectors
* @param \Slim\App $app
* @return void
*/
public static function setConnectors(App $app)
{
// Dynamically load any connectors?
$container = $app->getContainer();
/** @var \Xibo\Factory\ConnectorFactory $connectorFactory */
$connectorFactory = $container->get('connectorFactory');
foreach ($connectorFactory->query(['isEnabled' => 1, 'isVisible' => 1]) as $connector) {
try {
// Create a connector, add in platform settings and register it with the dispatcher.
$connectorFactory->create($connector)->registerWithDispatcher($container->get('dispatcher'));
} catch (\Exception $exception) {
// Log and ignore.
$container->get('logger')->error('Incorrectly configured connector. e=' . $exception->getMessage());
}
}
}
}

74
lib/Middleware/Csp.php Normal file
View File

@@ -0,0 +1,74 @@
<?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\Middleware;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface as Middleware;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Xibo\Helper\Random;
/**
* CSP middleware to output CSP headers and add a CSP nonce to the view layer.
*/
class Csp implements Middleware
{
public function __construct(private readonly ContainerInterface $container)
{
}
/**
* Call middleware
* @param Request $request
* @param RequestHandler $handler
* @return Response
* @throws \Psr\Container\ContainerExceptionInterface
* @throws \Psr\Container\NotFoundExceptionInterface
*/
public function process(Request $request, RequestHandler $handler): Response
{
// Generate a nonce
$nonce = Random::generateString(8);
// Create CSP header
$csp = 'object-src \'none\'; script-src \'nonce-' . $nonce . '\'';
$csp .= ' \'unsafe-inline\' \'unsafe-eval\' \'strict-dynamic\' https: http:;';
$csp .= ' base-uri \'self\';';
$csp .= ' frame-ancestors \'self\';';
// Store it for use in the stack if needed
$request = $request->withAttribute('cspNonce', $nonce);
// Assign it to our view
$this->container->get('view')->offsetSet('cspNonce', $nonce);
// Call next middleware.
$response = $handler->handle($request);
// Add our header
return $response
->withAddedHeader('X-Frame-Options', 'SAMEORIGIN')
->withAddedHeader('Content-Security-Policy', $csp);
}
}

View File

@@ -0,0 +1,123 @@
<?php
/**
* Copyright (C) 2020 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Middleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface as Middleware;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\App as App;
use Slim\Routing\RouteContext;
use Xibo\Helper\Environment;
use Xibo\Support\Exception\ExpiredException;
class CsrfGuard implements Middleware
{
/**
* CSRF token key name.
*
* @var string
*/
protected $key;
/* @var App $app */
private $app;
/**
* Constructor.
*
* @param App $app
* @param string $key The CSRF token key name.
*/
public function __construct($app, $key = 'csrfToken')
{
if (! is_string($key) || empty($key) || preg_match('/[^a-zA-Z0-9\-\_]/', $key)) {
throw new \OutOfBoundsException('Invalid CSRF token key "' . $key . '"');
}
$this->key = $key;
$this->app = $app;
}
/**
* Call middleware.
*
* @param Request $request
* @param RequestHandler $handler
* @return Response
* @throws ExpiredException
*/
public function process(Request $request, RequestHandler $handler): Response
{
$container = $this->app->getContainer();
/* @var \Xibo\Helper\Session $session */
$session = $this->app->getContainer()->get('session');
if (!$session->get($this->key)) {
$session->set($this->key, bin2hex(random_bytes(20)));
}
$token = $session->get($this->key);
// Validate the CSRF token.
if (in_array($request->getMethod(), ['POST', 'PUT', 'DELETE'])) {
// Validate the token unless we are on an excluded route
// Get the current route pattern
$routeContext = RouteContext::fromRequest($request);
$route = $routeContext->getRoute();
$resource = $route->getPattern();
$excludedRoutes = $request->getAttribute('excludedCsrfRoutes');
if (($excludedRoutes !== null && is_array($excludedRoutes) && in_array($resource, $excludedRoutes))
|| (Environment::isDevMode() && $resource === '/login')
) {
$container->get('logger')->info('Route excluded from CSRF: ' . $resource);
} else {
// Checking CSRF
$userToken = $request->getHeaderLine('X-XSRF-TOKEN');
if ($userToken == '') {
$parsedBody = $request->getParsedBody();
foreach ($parsedBody as $param => $value) {
if ($param == $this->key) {
$userToken = $value;
}
}
}
if ($token !== $userToken) {
throw new ExpiredException(__('Sorry the form has expired. Please refresh.'));
}
}
}
// Assign CSRF token key and value to view.
$container->get('view')->offsetSet('csrfKey', $this->key);
$container->get('view')->offsetSet('csrfToken',$token);
// Call next middleware.
return $handler->handle($request);
}
}

View File

@@ -0,0 +1,76 @@
<?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\Middleware;
use Xibo\Entity\Display;
use Xibo\Entity\DisplayProfile;
use Xibo\Service\ConfigServiceInterface;
use Xibo\Service\LogServiceInterface;
use Xibo\Support\Sanitizer\SanitizerInterface;
interface CustomDisplayProfileInterface
{
/**
* Return Display Profile type
* @return string
*/
public static function getType():string;
/**
* Return Display Profile name
* @return string
*/
public static function getName():string;
/**
* This function should return an array with default Display Profile config.
*
* @param ConfigServiceInterface $configService
* @return array
*/
public static function getDefaultConfig(ConfigServiceInterface $configService) : array;
/**
* This function should return full name, including extension (.twig) to the custom display profile edit form
* the file is expected to be in the /custom folder along the custom Middleware.
* To match naming convention twig file should be called displayprofile-form-edit-<type>.twig
* This will be done automatically from the CustomDisplayProfileMiddlewareTrait.
*
* If you have named your twig file differently, override getCustomEditTemplate function in your middleware
* @return string
*/
public static function getCustomEditTemplate() : string;
/**
* This function handles any changes to the default Display Profile settings, as well as overrides per Display.
* Each editable setting should have handling here.
*
* @param DisplayProfile $displayProfile
* @param SanitizerInterface $sanitizedParams
* @param array|null $config
* @param Display|null $display
* @param LogServiceInterface $logService
* @return array
*/
public static function editCustomConfigFields(DisplayProfile $displayProfile, SanitizerInterface $sanitizedParams, ?array $config, ?Display $display, LogServiceInterface $logService) : array;
}

View File

@@ -0,0 +1,143 @@
<?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\Middleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\App;
use Xibo\Support\Exception\InvalidArgumentException;
trait CustomDisplayProfileMiddlewareTrait
{
/**
* @return string
*/
public static function getClass():string
{
return self::class;
}
public static function getEditTemplateFunctionName():string
{
return 'getCustomEditTemplate';
}
public static function getDefaultConfigFunctionName():string
{
return 'getDefaultConfig';
}
public static function getEditCustomFieldsFunctionName():string
{
return 'editCustomConfigFields';
}
/**
* @param Request $request
* @param RequestHandler $handler
* @return Response
* @throws InvalidArgumentException
*/
public function process(Request $request, RequestHandler $handler): Response
{
$this->getFromContainer('logService')->debug('Loading additional Middleware for Custom Display Profile type:' . self::getType());
$store = $this->getFromContainer('store');
$results = $store->select('SELECT displayProfileId FROM displayprofile WHERE type = :type', ['type' => self::getType()]);
if (count($results) <= 0) {
$profile = $this->getFromContainer('displayProfileFactory')->createCustomProfile([
'name' => self::getName(),
'type' => self::getType(),
'isDefault' => 1,
'userId' => $this->getFromContainer('userFactory')->getSuperAdmins()[0]->userId
]);
$profile->save();
}
$this->getFromContainer('displayProfileFactory')->registerCustomDisplayProfile(
self::getType(),
self::getClass(),
self::getEditTemplateFunctionName(),
self::getDefaultConfigFunctionName(),
self::getEditCustomFieldsFunctionName()
);
// Next middleware
return $handler->handle($request);
}
/**
* @return string
* @throws InvalidArgumentException
*/
public static function getCustomEditTemplate() : string
{
return 'displayprofile-form-edit-'.self::getType().'.twig';
}
/** @var \Slim\App */
private $app;
/**
* @param \Slim\App $app
* @return $this
*/
public function setApp(App $app)
{
$this->app = $app;
return $this;
}
/**
* @return \Slim\App
*/
protected function getApp()
{
return $this->app;
}
/**
* @return \Psr\Container\ContainerInterface|null
*/
protected function getContainer()
{
return $this->app->getContainer();
}
/***
* @param $key
* @return mixed
*/
protected function getFromContainer($key)
{
return $this->getContainer()->get($key);
}
private static function handleChangedSettings($setting, $oldValue, $newValue, &$changedSettings)
{
if ($oldValue != $newValue) {
$changedSettings[$setting] = $oldValue . ' > ' . $newValue;
}
}
}

View File

@@ -0,0 +1,90 @@
<?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\Middleware;
use Psr\Http\Message\ServerRequestInterface;
use Slim\App;
/**
* Trait CustomMiddlewareTrait
* Add this trait to all custom middleware
* @package Xibo\Middleware
*/
trait CustomMiddlewareTrait
{
/** @var \Slim\App */
private $app;
/**
* @param \Slim\App $app
* @return $this
*/
public function setApp(App $app)
{
$this->app = $app;
return $this;
}
/**
* @return \Slim\App
*/
protected function getApp()
{
return $this->app;
}
/**
* @return \DI\Container|\Psr\Container\ContainerInterface
*/
protected function getContainer()
{
return $this->app->getContainer();
}
/**
* @param $key
* @return mixed
* @throws \DI\DependencyException
* @throws \DI\NotFoundException
* @throws \Psr\Container\ContainerExceptionInterface
* @throws \Psr\Container\NotFoundExceptionInterface
*/
protected function getFromContainer($key): mixed
{
return $this->getContainer()->get($key);
}
/**
* Append public routes
* @param \Psr\Http\Message\ServerRequestInterface $request
* @param array $routes
* @return \Psr\Http\Message\ServerRequestInterface
*/
protected function appendPublicRoutes(ServerRequestInterface $request, array $routes): ServerRequestInterface
{
// Set some public routes
return $request->withAttribute(
'publicRoutes',
array_merge($request->getAttribute('publicRoutes', []), $routes)
);
}
}

View File

@@ -0,0 +1,89 @@
<?php
/*
* Copyright (C) 2020 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Middleware;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Xibo\Support\Exception\AccessDeniedException;
/**
* Class FeatureAuth
* This is route middleware to checks user access against a set of features
* @package Xibo\Middleware
*/
class FeatureAuth implements MiddlewareInterface
{
/** @var \Psr\Container\ContainerInterface */
private $container;
/** @var array */
private $features;
/**
* FeatureAuth constructor.
* @param ContainerInterface $container
* @param array $features an array of one or more features which would authorize access
*/
public function __construct(ContainerInterface $container, array $features)
{
$this->container = $container;
$this->features = $features;
}
/**
* @param \Psr\Http\Message\ServerRequestInterface $request
* @param \Psr\Http\Server\RequestHandlerInterface $handler
* @return \Psr\Http\Message\ResponseInterface
* @throws \Xibo\Support\Exception\AccessDeniedException
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
// If no features are provided, then this must be public
if (count($this->features) <= 0) {
// We handle the rest of the request
return $handler->handle($request);
}
// Compare the features requested with the features this user has access to.
// if none match, throw 403
foreach ($this->features as $feature) {
if ($this->getUser()->featureEnabled($feature)) {
// We handle the rest of the request
return $handler->handle($request);
}
}
throw new AccessDeniedException(__('Feature not enabled'), __('This feature has not been enabled for your user.'));
}
/**
* @return \Xibo\Entity\User
*/
private function getUser()
{
return $this->container->get('user');
}
}

289
lib/Middleware/Handlers.php Normal file
View File

@@ -0,0 +1,289 @@
<?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\Middleware;
use Illuminate\Support\Str;
use League\OAuth2\Server\Exception\OAuthServerException;
use Nyholm\Psr7\Factory\Psr17Factory;
use Slim\Exception\HttpNotFoundException;
use Slim\Exception\HttpSpecializedException;
use Slim\Http\Factory\DecoratedResponseFactory;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Helper\Environment;
use Xibo\Helper\HttpsDetect;
use Xibo\Helper\Translate;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\ExpiredException;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InstanceSuspendedException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\UpgradePendingException;
/**
* Class Handlers
* @package Xibo\Middleware
*/
class Handlers
{
/**
* A JSON error handler to format and output a JSON response and HTTP status code depending on the error received.
* @param \Psr\Container\ContainerInterface $container
* @return \Closure
*/
public static function jsonErrorHandler($container)
{
return function (Request $request, \Throwable $exception, bool $displayErrorDetails, bool $logErrors, bool $logErrorDetails) use ($container) {
self::rollbackAndCloseStore($container);
self::writeLog($request, $logErrors, $logErrorDetails, $exception, $container);
// Generate a response (start with a 500)
$nyholmFactory = new Psr17Factory();
$decoratedResponseFactory = new DecoratedResponseFactory($nyholmFactory, $nyholmFactory);
/** @var Response $response */
$response = $decoratedResponseFactory->createResponse(500);
if ($exception instanceof GeneralException || $exception instanceof OAuthServerException) {
return $exception->generateHttpResponse($response);
} else if ($exception instanceof HttpSpecializedException) {
return $response->withJson([
'success' => false,
'error' => $exception->getCode(),
'message' => $exception->getTitle(),
'help' => $exception->getDescription()
]);
} else {
// Any other exception, check to see if we hide the real message
return $response->withJson([
'success' => false,
'error' => 500,
'message' => $displayErrorDetails
? $exception->getMessage()
: __('Unexpected Error, please contact support.')
]);
}
};
}
/**
* @param \Psr\Container\ContainerInterface $container
* @return \Closure
*/
public static function webErrorHandler($container)
{
return function (Request $request, \Throwable $exception, bool $displayErrorDetails, bool $logErrors, bool $logErrorDetails) use ($container) {
self::rollbackAndCloseStore($container);
self::writeLog($request, $logErrors, $logErrorDetails, $exception, $container);
// Create a response
// we're outside Slim's middleware here, so we have to handle the response ourselves.
$nyholmFactory = new Psr17Factory();
$decoratedResponseFactory = new DecoratedResponseFactory($nyholmFactory, $nyholmFactory);
$response = $decoratedResponseFactory->createResponse();
// We need to build all the functions required in the views manually because our middleware stack will
// not have been built for this handler.
// Slim4 has made this much more difficult!
// Terrible in fact.
// Get the Twig view
/** @var \Slim\Views\Twig $twig */
$twig = $container->get('view');
/** @var \Xibo\Service\ConfigService $configService */
$configService = $container->get('configService');
$configService->setDependencies($container->get('store'), $container->get('rootUri'));
$configService->loadTheme();
// Do we need to issue STS?
$response = HttpsDetect::decorateWithStsIfNecessary($configService, $request, $response);
// Prepend our theme files to the view path
// Does this theme provide an alternative view path?
if ($configService->getThemeConfig('view_path') != '') {
$twig->getLoader()->prependPath(Str::replaceFirst('..', PROJECT_ROOT,
$configService->getThemeConfig('view_path')));
}
// We have translated error/not-found
Translate::InitLocale($configService);
// Build up our own params to pass to Twig
$viewParams = [
'theme' => $configService,
'homeUrl' => $configService->rootUri(),
'aboutUrl' => $configService->rootUri() . 'about',
'loginUrl' => $configService->rootUri() . 'login',
'version' => Environment::$WEBSITE_VERSION_NAME
];
// Handle 404's
if ($exception instanceof HttpNotFoundException) {
if ($request->isXhr()) {
return $response->withJson([
'success' => false,
'error' => 404,
'message' => __('Sorry we could not find that page.')
], 404);
} else {
return $twig->render($response, 'not-found.twig', $viewParams)->withStatus(404);
}
} else {
// Make a friendly message
if ($displayErrorDetails || $exception instanceof GeneralException) {
$message = htmlspecialchars($exception->getMessage());
} else {
$message = __('Unexpected Error, please contact support.');
}
// Parse out data for the exception
$exceptionData = [
'success' => false,
'error' => $exception->getCode(),
'message' => $message
];
// TODO: we need to update the support library to make getErrorData public
/*if ($exception instanceof GeneralException) {
array_merge($exception->getErrorData(), $exceptionData);
}*/
if ($request->isXhr()) {
// Note: these are currently served as 200's, which is expected by the FE.
return $response->withJson($exceptionData);
} else {
// What status code?
$statusCode = 500;
if ($exception instanceof GeneralException) {
$statusCode = $exception->getHttpStatusCode();
}
if ($exception instanceof HttpSpecializedException) {
$statusCode = $exception->getCode();
}
// Decide which error page we should load
$exceptionClass = 'error-' . strtolower(str_replace('\\', '-', get_class($exception)));
// Override the page for an Upgrade Pending Exception
if ($exception instanceof UpgradePendingException) {
$exceptionClass = 'upgrade-in-progress-page';
}
if (file_exists(PROJECT_ROOT . '/views/' . $exceptionClass . '.twig')) {
$template = $exceptionClass;
} else {
$template = 'error';
}
try {
return $twig->render($response, $template . '.twig', array_merge($viewParams, $exceptionData))
->withStatus($statusCode);
} catch (\Exception $exception) {
$response->getBody()->write('Fatal error');
return $response->withStatus(500);
}
}
}
};
}
/**
* @param \Psr\Container\ContainerInterface $container
* @return \Closure
*/
public static function testErrorHandler($container)
{
return function (Request $request, \Throwable $exception, bool $displayErrorDetails, bool $logErrors, bool $logErrorDetails) use ($container) {
self::rollbackAndCloseStore($container);
$nyholmFactory = new Psr17Factory();
$decoratedResponseFactory = new DecoratedResponseFactory($nyholmFactory, $nyholmFactory);
/** @var Response $response */
$response = $decoratedResponseFactory->createResponse($exception->getCode());
return $response->withJson([
'success' => false,
'error' => $exception->getMessage(),
'httpStatus' => $exception->getCode(),
'data' => []
]);
};
}
/**
* Determine if we are a handled exception
* @param $e
* @return bool
*/
private static function handledError($e): bool
{
return ($e instanceof InvalidArgumentException
|| $e instanceof ExpiredException
|| $e instanceof AccessDeniedException
|| $e instanceof InstanceSuspendedException
|| $e instanceof UpgradePendingException
);
}
/**
* @param \Psr\Container\ContainerInterface $container
*/
private static function rollbackAndCloseStore($container)
{
// If we are in a transaction, then we should rollback.
if ($container->get('store')->getConnection()->inTransaction()) {
$container->get('store')->getConnection()->rollBack();
}
$container->get('store')->close();
}
/**
* @param Request $request
* @param bool $logErrors
* @param bool $logErrorDetails
* @param \Throwable $exception
* @param \Psr\Container\ContainerInterface $container
*/
private static function writeLog($request, bool $logErrors, bool $logErrorDetails, \Throwable $exception, $container)
{
/** @var \Psr\Log\LoggerInterface $logger */
$logger = $container->get('logger');
// Add a processor to our log handler
Log::addLogProcessorToLogger($logger, $request);
// Handle logging the error.
if ($logErrors && !self::handledError($exception)) {
$logger->error($exception->getMessage());
if ($logErrorDetails) {
$logger->debug($exception->getTraceAsString());
$previous = $exception->getPrevious();
if ($previous !== null) {
$logger->debug(get_class($previous) . ': ' . $previous->getMessage());
}
}
}
}
}

View File

@@ -0,0 +1,120 @@
<?php
/**
* Copyright (C) 2020 Xibo Signage Ltd
* Copyright (c) 2012-2015 Josh Lockhart
*
* 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\Middleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface as Middleware;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
/**
* Class HttpCache
* Http cache
* @package Xibo\Middleware
*/
class HttpCache implements Middleware
{
/**
* Cache-Control type (public or private)
*
* @var string
*/
protected $type;
/**
* Cache-Control max age in seconds
*
* @var int
*/
protected $maxAge;
/**
* Cache-Control includes must-revalidate flag
*
* @var bool
*/
protected $mustRevalidate;
public function __construct($type = 'private', $maxAge = 86400, $mustRevalidate = false)
{
$this->type = $type;
$this->maxAge = $maxAge;
$this->mustRevalidate = $mustRevalidate;
}
public function process(Request $request, RequestHandler $handler): Response
{
$response = $handler->handle($request);
// Cache-Control header
if (!$response->hasHeader('Cache-Control')) {
if ($this->maxAge === 0) {
$response = $response->withHeader('Cache-Control', sprintf(
'%s, no-cache%s',
$this->type,
$this->mustRevalidate ? ', must-revalidate' : ''
));
} else {
$response = $response->withHeader('Cache-Control', sprintf(
'%s, max-age=%s%s',
$this->type,
$this->maxAge,
$this->mustRevalidate ? ', must-revalidate' : ''
));
}
}
// ETag header and conditional GET check
$etag = $response->getHeader('ETag');
$etag = reset($etag);
if ($etag) {
$ifNoneMatch = $request->getHeaderLine('If-None-Match');
if ($ifNoneMatch) {
$etagList = preg_split('@\s*,\s*@', $ifNoneMatch);
if (in_array($etag, $etagList) || in_array('*', $etagList)) {
return $response->withStatus(304);
}
}
}
// Last-Modified header and conditional GET check
$lastModified = $response->getHeaderLine('Last-Modified');
if ($lastModified) {
if (!is_integer($lastModified)) {
$lastModified = strtotime($lastModified);
}
$ifModifiedSince = $request->getHeaderLine('If-Modified-Since');
if ($ifModifiedSince && $lastModified <= strtotime($ifModifiedSince)) {
return $response->withStatus(304);
}
}
return $response;
}
}

View File

@@ -0,0 +1,218 @@
<?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\Middleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface as Middleware;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Psr\Log\LoggerInterface;
use Slim\App;
use Slim\Routing\RouteContext;
use Stash\Invalidation;
use Stash\Item;
use Stash\Pool;
use Xibo\Helper\DateFormatHelper;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\GeneralException;
/**
* This Middleware will Lock the Layout for the specific User and entry point
* It is not added on the whole Application stack, instead it's added to selected groups of routes in routes.php
*
* For a User designing a Layout there will be no change in the way that User interacts with it
* However if the same Layout will be accessed by different User or Entry Point then this middleware will throw
* an Exception with suitable message.
*/
class LayoutLock implements Middleware
{
/** @var Item */
private $lock;
private $layoutId;
private $userId;
private $entryPoint;
private LoggerInterface $logger;
/**
* @param \Slim\App $app
* @param int $ttl
* @throws \Psr\Container\ContainerExceptionInterface
* @throws \Psr\Container\NotFoundExceptionInterface
*/
public function __construct(
private readonly App $app,
private readonly int $ttl = 300
) {
$this->logger = $this->app->getContainer()->get('logService')->getLoggerInterface();
}
/**
* @param Request $request
* @param RequestHandler $handler
* @return Response
* @throws \Psr\Container\ContainerExceptionInterface
* @throws \Psr\Container\NotFoundExceptionInterface
* @throws \Xibo\Support\Exception\GeneralException
*/
public function process(Request $request, RequestHandler $handler): Response
{
$routeContext = RouteContext::fromRequest($request);
$route = $routeContext->getRoute();
// what route are we in?
$resource = $route->getPattern();
$routeName = $route->getName();
// skip for test suite
if ($request->getAttribute('_entryPoint') === 'test' && $this->app->getContainer()->get('_entryPoint') === 'test') {
return $handler->handle($request);
}
$this->logger->debug('layoutLock: testing route ' . $routeName . ', pattern ' . $resource);
if (str_contains($resource, 'layout') !== false) {
// Layout route, we can get the Layout id from route argument.
$this->layoutId = (int)$route->getArgument('id');
} elseif (str_contains($resource, 'region') !== false) {
// Region route, we need to get the Layout Id from layoutFactory by Region Id
// if it's POST request or positionAll then id in route is already LayoutId we can use
if (str_contains($resource, 'position') !== false || $route->getMethods()[0] === 'POST') {
$this->layoutId = (int)$route->getArgument('id');
} else {
$regionId = (int)$route->getArgument('id');
$this->layoutId = $this->app->getContainer()->get('layoutFactory')->getByRegionId($regionId)->layoutId;
}
} else if (str_contains($routeName, 'playlist') !== false || $routeName === 'module.widget.add') {
// Playlist Route, we need to get to LayoutId, Widget add the same behaviour.
$playlistId = (int)$route->getArgument('id');
$regionId = $this->app->getContainer()->get('playlistFactory')->getById($playlistId)->regionId;
// if we are assigning media or ordering Region Playlist, then we will have regionId
// otherwise it's non Region specific Playlist, in which case we are not interested in locking anything.
if ($regionId != null) {
$this->layoutId = $this->app->getContainer()->get('layoutFactory')->getByRegionId($regionId)->layoutId;
}
} else if (str_contains($routeName, 'widget') !== false) {
// Widget route, the id route argument will be Widget Id
$widgetId = (int)$route->getArgument('id');
// get the Playlist Id for this Widget
$playlistId = $this->app->getContainer()->get('widgetFactory')->getById($widgetId)->playlistId;
$regionId = $this->app->getContainer()->get('playlistFactory')->getById($playlistId)->regionId;
// check if it's Region specific Playlist, otherwise we don't lock anything.
if ($regionId != null) {
$this->layoutId = $this->app->getContainer()->get('layoutFactory')->getByRegionId($regionId)->layoutId;
}
} else {
// this should never happen
throw new GeneralException(sprintf(
__('Layout Lock Middleware called with incorrect route %s'),
$route->getPattern(),
));
}
// run only if we have layout id, that will exclude non Region specific Playlist requests.
if ($this->layoutId !== null) {
$this->userId = $this->app->getContainer()->get('user')->userId;
$this->entryPoint = $this->app->getContainer()->get('name');
$key = $this->getKey();
$this->lock = $this->getPool()->getItem('locks/layout/' . $key);
$objectToCache = new \stdClass();
$objectToCache->layoutId = $this->layoutId;
$objectToCache->userId = $this->userId;
$objectToCache->entryPoint = $this->entryPoint;
$this->logger->debug('Layout Lock middleware for LayoutId ' . $this->layoutId
. ' userId ' . $this->userId . ' emtrypoint ' . $this->entryPoint);
$this->lock->setInvalidationMethod(Invalidation::OLD);
// Get the lock
// other requests will wait here until we're done, or we've timed out
$locked = $this->lock->get();
$this->logger->debug('$locked is ' . var_export($locked, true) . ', key = ' . $key);
if ($this->lock->isMiss() || $locked === []) {
$this->logger->debug('Lock miss or false. Locking for ' . $this->ttl . ' seconds. $locked is '
. var_export($locked, true) . ', key = ' . $key);
// so lock now
$this->lock->expiresAfter($this->ttl);
$objectToCache->expires = $this->lock->getExpiration()->format(DateFormatHelper::getSystemFormat());
$this->lock->set($objectToCache);
$this->lock->save();
} else {
// We are a hit - we must be locked
$this->logger->debug('LOCK hit for ' . $key . ' expires '
. $this->lock->getExpiration()->format(DateFormatHelper::getSystemFormat()) . ', created '
. $this->lock->getCreation()->format(DateFormatHelper::getSystemFormat()));
if ($locked->userId == $this->userId && $locked->entryPoint == $this->entryPoint) {
// the same user in the same entry point is editing the same layoutId
$this->lock->expiresAfter($this->ttl);
$objectToCache->expires = $this->lock->getExpiration()->format(DateFormatHelper::getSystemFormat());
$this->lock->set($objectToCache);
$this->lock->save();
$this->logger->debug('Lock extended to '
. $this->lock->getExpiration()->format(DateFormatHelper::getSystemFormat()));
} else {
// different user or entry point
$this->logger->debug('Sorry Layout is locked by another User!');
throw new AccessDeniedException(sprintf(
__('Layout ID %d is locked by another User! Lock expires on: %s'),
$locked->layoutId,
$locked->expires
));
}
}
}
return $handler->handle($request);
}
/**
* @return Pool
* @throws \Psr\Container\ContainerExceptionInterface
* @throws \Psr\Container\NotFoundExceptionInterface
*/
private function getPool()
{
return $this->app->getContainer()->get('pool');
}
/**
* Get the lock key
* @return mixed
*/
private function getKey()
{
return $this->layoutId;
}
}

View File

@@ -0,0 +1,424 @@
<?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\Middleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\App;
use Xibo\Event\CommandDeleteEvent;
use Xibo\Event\DependencyFileSizeEvent;
use Xibo\Event\DisplayGroupLoadEvent;
use Xibo\Event\FolderMovingEvent;
use Xibo\Event\MediaDeleteEvent;
use Xibo\Event\MediaFullLoadEvent;
use Xibo\Event\ParsePermissionEntityEvent;
use Xibo\Event\PlaylistMaxNumberChangedEvent;
use Xibo\Event\SystemUserChangedEvent;
use Xibo\Event\UserDeleteEvent;
use Xibo\Listener\CampaignListener;
use Xibo\Listener\DataSetDataProviderListener;
use Xibo\Listener\DisplayGroupListener;
use Xibo\Listener\LayoutListener;
use Xibo\Listener\MediaListener;
use Xibo\Listener\MenuBoardProviderListener;
use Xibo\Listener\ModuleTemplateListener;
use Xibo\Listener\NotificationDataProviderListener;
use Xibo\Listener\PlaylistListener;
use Xibo\Listener\SyncGroupListener;
use Xibo\Listener\TaskListener;
use Xibo\Listener\WidgetListener;
use Xibo\Xmds\Listeners\XmdsAssetsListener;
use Xibo\Xmds\Listeners\XmdsDataConnectorListener;
use Xibo\Xmds\Listeners\XmdsFontsListener;
use Xibo\Xmds\Listeners\XmdsPlayerBundleListener;
use Xibo\Xmds\Listeners\XmdsPlayerVersionListener;
/**
* This middleware is used to register listeners against the dispatcher
*/
class ListenersMiddleware implements MiddlewareInterface
{
/* @var App $app */
private $app;
public function __construct($app)
{
$this->app = $app;
}
/**
* @param Request $request
* @param RequestHandler $handler
* @return Response
*/
public function process(Request $request, RequestHandler $handler): Response
{
$app = $this->app;
// Set connectors
self::setListeners($app);
// Next middleware
return $handler->handle($request);
}
/**
* Set listeners
* @param \Slim\App $app
* @return void
*/
public static function setListeners(App $app)
{
$c = $app->getContainer();
$dispatcher = $c->get('dispatcher');
// Register listeners
// ------------------
// Listen for events that affect campaigns
(new CampaignListener(
$c->get('campaignFactory'),
$c->get('store')
))
->useLogger($c->get('logger'))
->registerWithDispatcher($dispatcher);
// Listen for events that affect Layouts
(new LayoutListener(
$c->get('layoutFactory'),
$c->get('store'),
$c->get('permissionFactory'),
))
->useLogger($c->get('logger'))
->registerWithDispatcher($dispatcher);
// Listen for event that affect Display Groups
(new DisplayGroupListener(
$c->get('displayGroupFactory'),
$c->get('displayFactory'),
$c->get('store')
))
->useLogger($c->get('logger'))
->registerWithDispatcher($dispatcher);
// Listen for event that affect Media
(new MediaListener(
$c->get('mediaFactory'),
$c->get('store')
))
->useLogger($c->get('logger'))
->registerWithDispatcher($dispatcher);
// Listen for events that affect ModuleTemplates
(new ModuleTemplateListener(
$c->get('moduleTemplateFactory'),
))
->useLogger($c->get('logger'))
->registerWithDispatcher($dispatcher);
// Listen for event that affect Playlist
(new PlaylistListener(
$c->get('playlistFactory'),
$c->get('store')
))
->useLogger($c->get('logger'))
->registerWithDispatcher($dispatcher);
// Listen for event that affect Sync Group
(new SyncGroupListener(
$c->get('syncGroupFactory'),
$c->get('store')
))
->useLogger($c->get('logger'))
->registerWithDispatcher($dispatcher);
// Listen for event that affect Task
(new TaskListener(
$c->get('taskFactory'),
$c->get('configService'),
$c->get('pool')
))
->useLogger($c->get('logger'))
->registerWithDispatcher($dispatcher);
// Media Delete Events
$dispatcher->addListener(MediaDeleteEvent::$NAME, (new \Xibo\Listener\OnMediaDelete\MenuBoardListener(
$c->get('menuBoardCategoryFactory')
)));
$dispatcher->addListener(MediaDeleteEvent::$NAME, (new \Xibo\Listener\OnMediaDelete\WidgetListener(
$c->get('store'),
$c->get('widgetFactory'),
$c->get('moduleFactory')
)));
$dispatcher->addListener(MediaDeleteEvent::$NAME, (new \Xibo\Listener\OnMediaDelete\PurgeListListener(
$c->get('store'),
$c->get('configService')
)));
// User Delete Events
$dispatcher->addListener(UserDeleteEvent::$NAME, (new \Xibo\Listener\OnUserDelete\ActionListener(
$c->get('store'),
$c->get('actionFactory')
))->useLogger($c->get('logger')));
$dispatcher->addListener(UserDeleteEvent::$NAME, (new \Xibo\Listener\OnUserDelete\CommandListener(
$c->get('store'),
$c->get('commandFactory')
))->useLogger($c->get('logger')));
$dispatcher->addListener(UserDeleteEvent::$NAME, (new \Xibo\Listener\OnUserDelete\DataSetListener(
$c->get('store'),
$c->get('dataSetFactory')
))->useLogger($c->get('logger')));
$dispatcher->addListener(UserDeleteEvent::$NAME, (new \Xibo\Listener\OnUserDelete\DayPartListener(
$c->get('store'),
$c->get('dayPartFactory'),
$c->get('scheduleFactory'),
$c->get('displayNotifyService')
))->useLogger($c->get('logger')));
$dispatcher->addListener(UserDeleteEvent::$NAME, (new \Xibo\Listener\OnUserDelete\DisplayProfileListener(
$c->get('store'),
$c->get('displayProfileFactory')
))->useLogger($c->get('logger')));
$dispatcher->addListener(UserDeleteEvent::$NAME, (new \Xibo\Listener\OnUserDelete\MenuBoardListener(
$c->get('store'),
$c->get('menuBoardFactory')
))->useLogger($c->get('logger')));
$dispatcher->addListener(UserDeleteEvent::$NAME, (new \Xibo\Listener\OnUserDelete\NotificationListener(
$c->get('notificationFactory')
))->useLogger($c->get('logger')));
$dispatcher->addListener(UserDeleteEvent::$NAME, (new \Xibo\Listener\OnUserDelete\OnUserDelete(
$c->get('store')
))->useLogger($c->get('logger')));
$dispatcher->addListener(UserDeleteEvent::$NAME, (new \Xibo\Listener\OnUserDelete\RegionListener(
$c->get('regionFactory')
))->useLogger($c->get('logger')), -1);
$dispatcher->addListener(UserDeleteEvent::$NAME, (new \Xibo\Listener\OnUserDelete\ReportScheduleListener(
$c->get('store'),
$c->get('reportScheduleFactory')
))->useLogger($c->get('logger')));
$dispatcher->addListener(UserDeleteEvent::$NAME, (new \Xibo\Listener\OnUserDelete\ResolutionListener(
$c->get('store'),
$c->get('resolutionFactory')
))->useLogger($c->get('logger')));
$dispatcher->addListener(UserDeleteEvent::$NAME, (new \Xibo\Listener\OnUserDelete\SavedReportListener(
$c->get('store'),
$c->get('savedReportFactory')
))->useLogger($c->get('logger')));
$dispatcher->addListener(UserDeleteEvent::$NAME, (new \Xibo\Listener\OnUserDelete\ScheduleListener(
$c->get('store'),
$c->get('scheduleFactory')
))->useLogger($c->get('logger')));
$dispatcher->addListener(UserDeleteEvent::$NAME, (new \Xibo\Listener\OnUserDelete\WidgetListener(
$c->get('widgetFactory')
))->useLogger($c->get('logger')), -2);
// Display Group Load events
$dispatcher->addListener(DisplayGroupLoadEvent::$NAME, (new \Xibo\Listener\OnDisplayGroupLoad\DisplayGroupDisplayListener(
$c->get('displayFactory')
)));
$dispatcher->addListener(DisplayGroupLoadEvent::$NAME, (new \Xibo\Listener\OnDisplayGroupLoad\DisplayGroupScheduleListener(
$c->get('scheduleFactory')
)));
// Media full load events
$dispatcher->addListener(MediaFullLoadEvent::$NAME, (new \Xibo\Listener\OnMediaLoad\WidgetListener(
$c->get('widgetFactory')
)));
// Parse Permissions Event Listeners
$dispatcher->addListener(ParsePermissionEntityEvent::$NAME . 'command', (new \Xibo\Listener\OnParsePermissions\PermissionsCommandListener(
$c->get('commandFactory')
)));
$dispatcher->addListener(ParsePermissionEntityEvent::$NAME . 'dataSet', (new \Xibo\Listener\OnParsePermissions\PermissionsDataSetListener(
$c->get('dataSetFactory')
)));
$dispatcher->addListener(ParsePermissionEntityEvent::$NAME . 'dayPart', (new \Xibo\Listener\OnParsePermissions\PermissionsDayPartListener(
$c->get('dayPartFactory')
)));
$dispatcher->addListener(ParsePermissionEntityEvent::$NAME . 'folder', (new \Xibo\Listener\OnParsePermissions\PermissionsFolderListener(
$c->get('folderFactory')
)));
$dispatcher->addListener(ParsePermissionEntityEvent::$NAME . 'menuBoard', (new \Xibo\Listener\OnParsePermissions\PermissionsMenuBoardListener(
$c->get('menuBoardFactory')
)));
$dispatcher->addListener(ParsePermissionEntityEvent::$NAME . 'notification', (new \Xibo\Listener\OnParsePermissions\PermissionsNotificationListener(
$c->get('notificationFactory')
)));
$dispatcher->addListener(ParsePermissionEntityEvent::$NAME . 'region', (new \Xibo\Listener\OnParsePermissions\PermissionsRegionListener(
$c->get('regionFactory')
)));
$dispatcher->addListener(ParsePermissionEntityEvent::$NAME . 'widget', (new \Xibo\Listener\OnParsePermissions\PermissionsWidgetListener(
$c->get('widgetFactory')
)));
// On Command delete event listener
$dispatcher->addListener(CommandDeleteEvent::$NAME, (new \Xibo\Listener\OnCommandDelete(
$c->get('displayProfileFactory')
)));
// On System User change event listener
$dispatcher->addListener(SystemUserChangedEvent::$NAME, (new \Xibo\Listener\OnSystemUserChange(
$c->get('store')
)));
// On Playlist Max Number of Items limit change listener
$dispatcher->addListener(PlaylistMaxNumberChangedEvent::$NAME, (new \Xibo\Listener\OnPlaylistMaxNumberChange(
$c->get('store')
)));
// On Folder moving listeners
$dispatcher->addListener(FolderMovingEvent::$NAME, (new \Xibo\Listener\OnFolderMoving\DataSetListener(
$c->get('dataSetFactory')
)));
$dispatcher->addListener(FolderMovingEvent::$NAME, (new \Xibo\Listener\OnFolderMoving\FolderListener(
$c->get('folderFactory')
)), -1);
$dispatcher->addListener(FolderMovingEvent::$NAME, (new \Xibo\Listener\OnFolderMoving\MenuBoardListener(
$c->get('menuBoardFactory')
)));
$dispatcher->addListener(FolderMovingEvent::$NAME, (new \Xibo\Listener\OnFolderMoving\UserListener(
$c->get('userFactory'),
$c->get('store')
)));
// dependencies file size
$dispatcher->addListener(DependencyFileSizeEvent::$NAME, (new \Xibo\Listener\OnGettingDependencyFileSize\FontsListener(
$c->get('fontFactory')
)));
$dispatcher->addListener(DependencyFileSizeEvent::$NAME, (new \Xibo\Listener\OnGettingDependencyFileSize\PlayerVersionListener(
$c->get('playerVersionFactory')
)));
$dispatcher->addListener(DependencyFileSizeEvent::$NAME, (new \Xibo\Listener\OnGettingDependencyFileSize\SavedReportListener(
$c->get('savedReportFactory')
)));
// Widget related listeners for getting core data
(new DataSetDataProviderListener(
$c->get('store'),
$c->get('configService'),
$c->get('dataSetFactory'),
$c->get('displayFactory')
))
->useLogger($c->get('logger'))
->registerWithDispatcher($dispatcher);
(new NotificationDataProviderListener(
$c->get('configService'),
$c->get('notificationFactory'),
$c->get('user')
))
->useLogger($c->get('logger'))
->registerWithDispatcher($dispatcher);
(new WidgetListener(
$c->get('playlistFactory'),
$c->get('moduleFactory'),
$c->get('widgetFactory'),
$c->get('store'),
$c->get('configService')
))
->useLogger($c->get('logger'))
->registerWithDispatcher($dispatcher);
(new MenuBoardProviderListener(
$c->get('menuBoardFactory'),
$c->get('menuBoardCategoryFactory'),
))
->useLogger($c->get('logger'))
->registerWithDispatcher($dispatcher);
}
/**
* Set XMDS specific listeners
* @param App $app
* @return void
*/
public static function setXmdsListeners(App $app)
{
$c = $app->getContainer();
$dispatcher = $c->get('dispatcher');
$playerBundleListener = new XmdsPlayerBundleListener();
$playerBundleListener
->useLogger($c->get('logger'))
->useConfig($c->get('configService'));
$fontsListener = new XmdsFontsListener($c->get('fontFactory'));
$fontsListener
->useLogger($c->get('logger'))
->useConfig($c->get('configService'));
$playerVersionListner = new XmdsPlayerVersionListener($c->get('playerVersionFactory'));
$playerVersionListner->useLogger($c->get('logger'));
$assetsListener = new XmdsAssetsListener(
$c->get('moduleFactory'),
$c->get('moduleTemplateFactory')
);
$assetsListener
->useLogger($c->get('logger'))
->useConfig($c->get('configService'));
$dataConnectorListener = new XmdsDataConnectorListener();
$dataConnectorListener
->useLogger($c->get('logger'))
->useConfig($c->get('configService'));
$dispatcher->addListener('xmds.dependency.list', [$playerBundleListener, 'onDependencyList']);
$dispatcher->addListener('xmds.dependency.request', [$playerBundleListener, 'onDependencyRequest']);
$dispatcher->addListener('xmds.dependency.list', [$fontsListener, 'onDependencyList']);
$dispatcher->addListener('xmds.dependency.request', [$fontsListener, 'onDependencyRequest']);
$dispatcher->addListener('xmds.dependency.list', [$playerVersionListner, 'onDependencyList']);
$dispatcher->addListener('xmds.dependency.request', [$playerVersionListner, 'onDependencyRequest']);
$dispatcher->addListener('xmds.dependency.request', [$assetsListener, 'onDependencyRequest']);
$dispatcher->addListener('xmds.dependency.request', [$dataConnectorListener, 'onDependencyRequest']);
}
}

77
lib/Middleware/Log.php Normal file
View File

@@ -0,0 +1,77 @@
<?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\Middleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface as Middleware;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Psr\Log\LoggerInterface;
use Slim\App as App;
use Xibo\Helper\RouteLogProcessor;
/**
* Log Middleware
*/
class Log implements Middleware
{
private App $app;
/**
* @param $app
*/
public function __construct($app)
{
$this->app = $app;
}
/**
* @param Request $request
* @param RequestHandler $handler
* @return Response
* @throws \Psr\Container\ContainerExceptionInterface
* @throws \Psr\Container\NotFoundExceptionInterface
*/
public function process(Request $request, RequestHandler $handler): Response
{
$container = $this->app->getContainer();
self::addLogProcessorToLogger($container->get('logger'), $request);
return $handler->handle($request);
}
/**
* @param LoggerInterface $logger
* @param \Psr\Http\Message\ServerRequestInterface $request
*/
public static function addLogProcessorToLogger(
LoggerInterface $logger,
Request $request,
): void {
$logger->pushProcessor(new RouteLogProcessor(
$request->getUri()->getPath(),
$request->getMethod(),
));
}
}

View File

@@ -0,0 +1,488 @@
<?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\Middleware;
use OneLogin\Saml2\Auth;
use OneLogin\Saml2\Error;
use OneLogin\Saml2\Settings;
use OneLogin\Saml2\Utils;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Xibo\Helper\ApplicationState;
use Xibo\Helper\LogoutTrait;
use Xibo\Helper\Random;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\ConfigurationException;
use Xibo\Support\Exception\NotFoundException;
/**
* Class SAMLAuthentication
* @package Xibo\Middleware
*
* Provide SAML authentication to Xibo configured via settings.php.
*/
class SAMLAuthentication extends AuthenticationBase
{
use LogoutTrait;
/**
* @return $this
*/
public function addRoutes()
{
$app = $this->app;
$app->getContainer()->set('logoutRoute', 'saml.logout');
// Route providing SAML metadata
$app->get('/saml/metadata', function (Request $request, Response $response) {
$settings = new Settings($this->getConfig()->samlSettings, true);
$metadata = $settings->getSPMetadata();
$errors = $settings->validateMetadata($metadata);
if (empty($errors)) {
return $response
->withHeader('Content-Type', 'text/xml')
->write($metadata);
} else {
throw new ConfigurationException(
'Invalid SP metadata: ' . implode(', ', $errors),
Error::METADATA_SP_INVALID
);
}
});
// SAML Login
$app->get('/saml/login', function (Request $request, Response $response) {
// Initiate SAML SSO
$auth = new Auth($this->getConfig()->samlSettings);
return $auth->login();
});
// SAML Logout
$app->get('/saml/logout', function (Request $request, Response $response) {
return $this->samlLogout($request, $response);
})->setName('saml.logout');
// SAML Assertion Consumer Endpoint
$app->post('/saml/acs', function (Request $request, Response $response) {
// Log some interesting things
$this->getLog()->debug('Arrived at the ACS route with own URL: ' . Utils::getSelfRoutedURLNoQuery());
// Pull out the SAML settings
$samlSettings = $this->getConfig()->samlSettings;
$auth = new Auth($samlSettings);
$auth->processResponse();
// Check for errors
$errors = $auth->getErrors();
if (!empty($errors)) {
$this->getLog()->error('Single Sign on Failed: ' . implode(', ', $errors)
. '. Last Reason: ' . $auth->getLastErrorReason());
throw new AccessDeniedException(__('Your authentication provider could not log you in.'));
} else {
// Pull out the SAML attributes
$samlAttrs = $auth->getAttributes();
$this->getLog()->debug('SAML attributes: ' . json_encode($samlAttrs));
// How should we look up the user?
$identityField = (isset($samlSettings['workflow']['field_to_identify']))
? $samlSettings['workflow']['field_to_identify']
: 'UserName';
if ($identityField !== 'nameId' && empty($samlAttrs)) {
// We will need some attributes
throw new AccessDeniedException(__('No attributes retrieved from the IdP'));
}
// If appropriate convert the SAML Attributes into userData mapped against the workflow mappings.
$userData = [];
if (isset($samlSettings['workflow']) && isset($samlSettings['workflow']['mapping'])) {
foreach ($samlSettings['workflow']['mapping'] as $key => $value) {
if (!empty($value) && isset($samlAttrs[$value])) {
$userData[$key] = $samlAttrs[$value];
}
}
// If we can't map anything, then we better throw an error
if (empty($userData)) {
throw new AccessDeniedException(__('No attributes could be mapped'));
}
}
// If we're using the nameId as the identity, then we should populate our userData with that value
if ($identityField === 'nameId') {
$userData[$identityField] = $auth->getNameId();
} else {
// Check to ensure that our identity has been populated from attributes successfully
if (!isset($userData[$identityField]) || empty($userData[$identityField])) {
throw new AccessDeniedException(sprintf(__('%s not retrieved from the IdP and required since is the field to identify the user'), $identityField));
}
}
// Are we going to try and match our Xibo groups to our Idp groups?
$isMatchGroupFromIdp = ($samlSettings['workflow']['matchGroups']['enabled'] ?? false) === true
&& ($samlSettings['workflow']['matchGroups']['attribute'] ?? null) !== null;
// Try and get the user record.
$user = null;
try {
switch ($identityField) {
case 'nameId':
$user = $this->getUserFactory()->getByName($userData[$identityField]);
break;
case 'UserID':
$user = $this->getUserFactory()->getById($userData[$identityField][0]);
break;
case 'UserName':
$user = $this->getUserFactory()->getByName($userData[$identityField][0]);
break;
case 'email':
$user = $this->getUserFactory()->getByEmail($userData[$identityField][0]);
break;
default:
throw new AccessDeniedException(__('Invalid field_to_identify value. Review settings.'));
}
} catch (NotFoundException $e) {
// User does not exist - this is valid as we might create them JIT.
}
if (!isset($user)) {
if (!isset($samlSettings['workflow']['jit']) || $samlSettings['workflow']['jit'] == false) {
throw new AccessDeniedException(__('User logged at the IdP but the account does not exist in the CMS and Just-In-Time provisioning is disabled'));
} else {
// Provision the user
$user = $this->getEmptyUser();
$user->homeFolderId = 1;
if (isset($userData["UserName"])) {
$user->userName = $userData["UserName"][0];
}
if (isset($userData["email"])) {
$user->email = $userData["email"][0];
}
if (isset($userData["usertypeid"])) {
$user->userTypeId = $userData["usertypeid"][0];
} else {
$user->userTypeId = 3;
}
// Xibo requires a password, generate a random one (it won't ever be used by SAML)
$password = Random::generateString(20);
$user->setNewPassword($password);
// Home page
if (isset($samlSettings['workflow']['homePage'])) {
try {
$user->homePageId = $this->getUserGroupFactory()->getHomepageByName(
$samlSettings['workflow']['homePage']
)->homepage;
} catch (NotFoundException $exception) {
$this->getLog()->info(
sprintf(
'Provided homepage %s, does not exist,
setting the icondashboard.view as homepage',
$samlSettings['workflow']['homePage']
)
);
$user->homePageId = 'icondashboard.view';
}
} else {
$user->homePageId = 'icondashboard.view';
}
// Library Quota
if (isset($samlSettings['workflow']['libraryQuota'])) {
$user->libraryQuota = $samlSettings['workflow']['libraryQuota'];
} else {
$user->libraryQuota = 0;
}
// Match references
if (isset($samlSettings['workflow']['ref1']) && isset($userData['ref1'])) {
$user->ref1 = $userData['ref1'];
}
if (isset($samlSettings['workflow']['ref2']) && isset($userData['ref2'])) {
$user->ref2 = $userData['ref2'];
}
if (isset($samlSettings['workflow']['ref3']) && isset($userData['ref3'])) {
$user->ref3 = $userData['ref3'];
}
if (isset($samlSettings['workflow']['ref4']) && isset($userData['ref4'])) {
$user->ref4 = $userData['ref4'];
}
if (isset($samlSettings['workflow']['ref5']) && isset($userData['ref5'])) {
$user->ref5 = $userData['ref5'];
}
// Save the user
$user->save();
// Assign the initial group
if (isset($samlSettings['workflow']['group']) && !$isMatchGroupFromIdp) {
$group = $this->getUserGroupFactory()->getByName($samlSettings['workflow']['group']);
} else {
$group = $this->getUserGroupFactory()->getByName('Users');
}
$group->assignUser($user);
$group->save(['validate' => false]);
$this->getLog()->setIpAddress($request->getAttribute('ip_address'));
// Audit Log
$this->getLog()->audit('User', $user->userId, 'User created with SAML workflow', [
'UserName' => $user->userName,
'UserAgent' => $request->getHeader('User-Agent')
]);
}
}
if (isset($user) && $user->userId > 0) {
// Load User
$user = $this->getUser(
$user->userId,
$request->getAttribute('ip_address'),
$this->getSession()->get('sessionHistoryId')
);
// Overwrite our stored user with this new object.
$this->setUserForRequest($user);
// Switch Session ID's
$this->getSession()->setIsExpired(0);
$this->getSession()->regenerateSessionId();
$this->getSession()->setUser($user->userId);
$user->touch();
// Audit Log
$this->getLog()->audit('User', $user->userId, 'Login Granted via SAML', [
'UserAgent' => $request->getHeader('User-Agent')
]);
}
// Match groups from IdP?
if ($isMatchGroupFromIdp) {
$this->getLog()->debug('group matching enabled');
// Match groups is enabled, and we have an attribute to get groups from.
$idpGroups = [];
$extractionRegEx = $samlSettings['workflow']['matchGroups']['extractionRegEx'] ?? null;
// Get groups.
foreach ($samlAttrs[$samlSettings['workflow']['matchGroups']['attribute']] as $groupAttr) {
// Regex?
if (!empty($extractionRegEx)) {
$matches = [];
preg_match_all($extractionRegEx, $groupAttr, $matches);
if (count($matches[1]) > 0) {
$groupAttr = $matches[1][0];
}
}
$this->getLog()->debug('checking for group ' . $groupAttr);
// Does this group exist?
try {
$idpGroups[$groupAttr] = $this->getUserGroupFactory()->getByName($groupAttr);
} catch (NotFoundException) {
$this->getLog()->debug('group ' . $groupAttr . ' does not exist');
}
}
// Go through the users groups
$usersGroups = [];
foreach ($user->groups as $userGroup) {
$usersGroups[$userGroup->group] = $userGroup;
}
foreach ($user->groups as $userGroup) {
// Does this group exist in the Idp? If not, remove.
if (!array_key_exists($userGroup->group, $idpGroups)) {
// Group exists in Xibo, does not exist in the response, so remove.
$userGroup->unassignUser($user);
$userGroup->save(['validate' => false]);
$this->getLog()->debug($userGroup->group
. ' not matched to any IdP groups linked, removing');
unset($usersGroups[$userGroup->group]);
} else {
// Matched, so remove from idpGroups
unset($idpGroups[$userGroup->group]);
$this->getLog()->debug($userGroup->group . ' already linked.');
}
}
// Go through remaining groups and assign the user to them.
foreach ($idpGroups as $idpGroup) {
$this->getLog()->debug($idpGroup->group . ' already linked.');
$idpGroup->assignUser($user);
$idpGroup->save(['validate' => false]);
}
// Does this user still not have any groups?
if (count($usersGroups) <= 0) {
$group = $this->getUserGroupFactory()->getByName($samlSettings['workflow']['group'] ?? 'Users');
$group->assignUser($user);
$group->save(['validate' => false]);
}
}
// Redirect back to the originally-requested url, if provided
// it is not clear why basename is used here, it seems to be something to do with a logout loop
$params = $request->getParams();
$relayState = $params['RelayState'] ?? null;
$redirect = empty($relayState) || basename($relayState) === 'login'
? $this->getRouteParser()->urlFor('home')
: $relayState;
$this->getLog()->debug('redirecting to ' . $redirect);
return $response->withRedirect($redirect);
}
});
// Single Logout Service
$app->map(['GET', 'POST'], '/saml/sls', function (Request $request, Response $response) use ($app) {
// Make request to IDP
$auth = new Auth($app->getContainer()->get('configService')->samlSettings);
try {
$auth->processSLO(false, null, false, function () use ($request) {
// Audit that the IDP has completed this request.
$this->getLog()->setIpAddress($request->getAttribute('ip_address'));
$this->getLog()->setSessionHistoryId($this->getSession()->get('sessionHistoryId'));
$this->getLog()->audit('User', 0, 'Idp SLO completed', [
'UserAgent' => $request->getHeader('User-Agent')
]);
});
} catch (\Exception $e) {
// Ignored - get with getErrors()
}
$errors = $auth->getErrors();
if (empty($errors)) {
return $response->withRedirect($this->getRouteParser()->urlFor('home'));
} else {
throw new AccessDeniedException('SLO failed. ' . implode(', ', $errors));
}
});
return $this;
}
/**
* @param \Slim\Http\ServerRequest $request
* @param \Slim\Http\Response $response
* @return \Psr\Http\Message\ResponseInterface|\Slim\Http\Response
* @throws \OneLogin\Saml2\Error
*/
public function samlLogout(Request $request, Response $response)
{
$samlSettings = $this->getConfig()->samlSettings;
if (isset($samlSettings['workflow'])
&& isset($samlSettings['workflow']['slo'])
&& $samlSettings['workflow']['slo'] == true
) {
// Complete our own logout flow
$this->completeLogoutFlow(
$this->getUser(
$_SESSION['userid'],
$request->getAttribute('ip_address'),
$_SESSION['sessionHistoryId']
),
$this->getSession(),
$this->getLog(),
$request
);
// Initiate SAML SLO
$auth = new Auth($samlSettings);
return $response->withRedirect($auth->logout());
} else {
return $response->withRedirect($this->getRouteParser()->urlFor('logout'));
}
}
/**
* @param Request $request
* @return Response
* @throws \OneLogin\Saml2\Error
*/
public function redirectToLogin(\Psr\Http\Message\ServerRequestInterface $request)
{
if ($this->isAjax($request)) {
return $this->createResponse($request)->withJson(ApplicationState::asRequiresLogin());
} else {
// Initiate SAML SSO
$auth = new Auth($this->getConfig()->samlSettings);
return $this->createResponse($request)->withRedirect($auth->login());
}
}
/** @inheritDoc */
public function getPublicRoutes(\Psr\Http\Message\ServerRequestInterface $request)
{
return array_merge($request->getAttribute('publicRoutes', []), [
'/saml/metadata',
'/saml/login',
'/saml/acs',
'/saml/logout',
'/saml/sls'
]);
}
/** @inheritDoc */
public function shouldRedirectPublicRoute($route)
{
return ($this->getSession()->isExpired()
&& ($route == '/login/ping' || $route == 'clock'))
|| $route == '/login';
}
/** @inheritDoc */
public function addToRequest(\Psr\Http\Message\ServerRequestInterface $request)
{
return $request->withAttribute(
'excludedCsrfRoutes',
array_merge($request->getAttribute('excludedCsrfRoutes', []), ['/saml/acs', '/saml/sls'])
);
}
}

336
lib/Middleware/State.php Normal file
View File

@@ -0,0 +1,336 @@
<?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\Middleware;
use Carbon\Carbon;
use Monolog\Logger;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface as Middleware;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Respect\Validation\Factory;
use Slim\App;
use Slim\Routing\RouteContext;
use Slim\Views\Twig;
use Xibo\Entity\User;
use Xibo\Helper\Environment;
use Xibo\Helper\HttpsDetect;
use Xibo\Helper\NullSession;
use Xibo\Helper\Session;
use Xibo\Helper\Translate;
use Xibo\Service\ReportService;
use Xibo\Support\Exception\InstanceSuspendedException;
use Xibo\Support\Exception\UpgradePendingException;
use Xibo\Twig\TwigMessages;
/**
* Class State
* @package Xibo\Middleware
*/
class State implements Middleware
{
/* @var App $app */
private $app;
public function __construct($app)
{
$this->app = $app;
}
/**
* @param Request $request
* @param RequestHandler $handler
* @return Response
* @throws InstanceSuspendedException
* @throws UpgradePendingException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function process(Request $request, RequestHandler $handler): Response
{
$app = $this->app;
$container = $app->getContainer();
// Set state
$request = State::setState($app, $request);
// Check to see if the instance has been suspended, if so call the special route
if ($container->get('configService')->getSetting('INSTANCE_SUSPENDED') == 'yes') {
throw new InstanceSuspendedException();
}
// Get to see if upgrade is pending, we don't want to throw this when we are on error page, causes
// redirect problems with error handler.
if (Environment::migrationPending() && $request->getUri()->getPath() != '/error') {
throw new UpgradePendingException();
}
// Next middleware
$response = $handler->handle($request);
// Do we need SSL/STS?
if (HttpsDetect::isShouldIssueSts($container->get('configService'), $request)) {
$response = HttpsDetect::decorateWithSts($container->get('configService'), $response);
} else if (!HttpsDetect::isHttps()) {
// We are not HTTPS, should we redirect?
// Get the current route pattern
$routeContext = RouteContext::fromRequest($request);
$route = $routeContext->getRoute();
$resource = $route->getPattern();
// Allow non-https access to the clock page, otherwise force https
if ($resource !== '/clock' && $container->get('configService')->getSetting('FORCE_HTTPS', 0) == 1) {
$redirect = "https://" . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
$response = $response->withHeader('Location', $redirect)
->withStatus(302);
}
}
// Reset the ETAGs for GZIP
$requestEtag = $request->getHeaderLine('IF_NONE_MATCH');
if ($requestEtag) {
$response = $response->withHeader('IF_NONE_MATCH', str_replace('-gzip', '', $requestEtag));
}
// Handle correctly outputting cache headers for AJAX requests
// IE cache busting
if ($this->isAjax($request) && $request->getMethod() == 'GET' && $request->getAttribute('_entryPoint') == 'web') {
$response = $response->withHeader('Cache-control', 'no-cache')
->withHeader('Cache-control', 'no-store')
->withHeader('Pragma', 'no-cache')
->withHeader('Expires', '0');
}
return $response;
}
/**
* @param App $app
* @param \Psr\Http\Message\ServerRequestInterface $request
* @return \Psr\Http\Message\ServerRequestInterface
* @throws \Xibo\Support\Exception\NotFoundException
*/
public static function setState(App $app, Request $request): Request
{
$container = $app->getContainer();
// Set the config dependencies
$container->get('configService')->setDependencies($container->get('store'), $container->get('rootUri'));
// set the system user for XTR/XMDS
if ($container->get('name') == 'xtr' || $container->get('name') == 'xmds') {
// Configure a user
/** @var User $user */
$user = $container->get('userFactory')->getSystemUser();
$user->setChildAclDependencies($container->get('userGroupFactory'));
// Load the user
$user->load(false);
$container->set('user', $user);
}
// Register the report service
$container->set('reportService', function (ContainerInterface $container) {
$reportService = new ReportService(
$container,
$container->get('store'),
$container->get('timeSeriesStore'),
$container->get('logService'),
$container->get('configService'),
$container->get('sanitizerService'),
$container->get('savedReportFactory')
);
$reportService->setDispatcher($container->get('dispatcher'));
return $reportService;
});
// Set some public routes
$request = $request->withAttribute('publicRoutes', array_merge($request->getAttribute('publicRoutes', []), [
'/login',
'/login/forgotten',
'/clock',
'/about',
'/login/ping',
'/rss/{psk}',
'/sssp_config.xml',
'/sssp_dl.wgt',
'/playersoftware/{nonce}/sssp_dl.wgt',
'/playersoftware/{nonce}/sssp_config.xml',
'/tfa',
'/error',
'/notFound',
'/public/thumbnail/{id}',
]));
// Setup the translations for gettext
Translate::InitLocale($container->get('configService'));
// Set Carbon locale
Carbon::setLocale(Translate::GetLocale(2));
// Default timezone
$defaultTimezone = $container->get('configService')->getSetting('defaultTimezone') ?? 'UTC';
date_default_timezone_set($defaultTimezone);
$container->set('session', function (ContainerInterface $container) use ($app) {
if ($container->get('name') == 'web' || $container->get('name') == 'auth') {
$sessionHandler = new Session($container->get('logService'));
session_set_save_handler($sessionHandler, true);
register_shutdown_function('session_write_close');
// Start the session
session_cache_limiter(false);
session_start();
return $sessionHandler;
} else {
return new NullSession();
}
});
// We use Slim Flash Messages so we must immediately start a session (boo)
$container->get('session')->set('init', '1');
// App Mode
$mode = $container->get('configService')->getSetting('SERVER_MODE');
$container->get('logService')->setMode($mode);
// Inject some additional changes on a per-container basis
$containerName = $container->get('name');
if ($containerName == 'web' || $containerName == 'xtr' || $containerName == 'xmds') {
/** @var Twig $view */
$view = $container->get('view');
if ($containerName == 'web') {
$container->set('flash', function () {
return new \Slim\Flash\Messages();
});
$view->addExtension(new TwigMessages(new \Slim\Flash\Messages()));
}
$twigEnvironment = $view->getEnvironment();
// add the urldecode filter to Twig.
$filter = new \Twig\TwigFilter('url_decode', 'urldecode');
$twigEnvironment->addFilter($filter);
// Set Twig auto reload if needed
// XMDS only renders widget html cache, and shouldn't need auto reload.
if ($containerName !== 'xmds') {
$twigEnvironment->enableAutoReload();
}
}
// Configure logging
// -----------------
// Standard handlers
if (Environment::isForceDebugging() || strtolower($mode) == 'test') {
error_reporting(E_ALL);
ini_set('display_errors', 1);
$container->get('logService')->setLevel(Logger::DEBUG);
} else {
// Log level
$level = \Xibo\Service\LogService::resolveLogLevel($container->get('configService')->getSetting('audit'));
$restingLevel = \Xibo\Service\LogService::resolveLogLevel($container->get('configService')->getSetting('RESTING_LOG_LEVEL'));
// the higher the number the less strict the logging.
if ($level < $restingLevel) {
// Do we allow the log level to be this high
$elevateUntil = $container->get('configService')->getSetting('ELEVATE_LOG_UNTIL');
if (intval($elevateUntil) < Carbon::now()->format('U')) {
// Elevation has expired, revert log level
$container->get('configService')->changeSetting('audit', $container->get('configService')->getSetting('RESTING_LOG_LEVEL'));
$level = $restingLevel;
}
}
$container->get('logService')->setLevel($level);
}
// Update logger containers to use the CMS default timezone
$container->get('logger')->setTimezone(new \DateTimeZone($defaultTimezone));
// Configure any extra log handlers
// we do these last so that they can provide their own log levels independent of the system settings
if ($container->get('configService')->logHandlers != null && is_array($container->get('configService')->logHandlers)) {
$container->get('logService')->debug('Configuring %d additional log handlers from Config', count($container->get('configService')->logHandlers));
foreach ($container->get('configService')->logHandlers as $handler) {
// Direct access to the LoggerInterface here, rather than via our log service
$container->get('logger')->pushHandler($handler);
}
}
// Configure any extra log processors
if ($container->get('configService')->logProcessors != null && is_array($container->get('configService')->logProcessors)) {
$container->get('logService')->debug('Configuring %d additional log processors from Config', count($container->get('configService')->logProcessors));
foreach ($container->get('configService')->logProcessors as $processor) {
$container->get('logger')->pushProcessor($processor);
}
}
// Add additional validation rules
Factory::setDefaultInstance(
(new Factory())
->withRuleNamespace('Xibo\\Validation\\Rules')
->withExceptionNamespace('Xibo\\Validation\\Exceptions')
);
return $request;
}
/**
* Set additional middleware
* @param App $app
*/
public static function setMiddleWare($app)
{
// Handle additional Middleware
if (isset($app->getContainer()->get('configService')->middleware) && is_array($app->getContainer()->get('configService')->middleware)) {
foreach ($app->getContainer()->get('configService')->middleware as $object) {
// Decorate our middleware with the App if it has a method to do so
if (method_exists($object, 'setApp')) {
$object->setApp($app);
}
// Add any new routes from custom middleware
if (method_exists($object, 'addRoutes')) {
$object->addRoutes();
}
$app->add($object);
}
}
}
/**
* @param \Psr\Http\Message\ServerRequestInterface $request
* @return bool
*/
private function isAjax(Request $request)
{
return strtolower($request->getHeaderLine('X-Requested-With')) === 'xmlhttprequest';
}
}

View File

@@ -0,0 +1,87 @@
<?php
/**
* Copyright (C) 2020 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Middleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface as Middleware;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\App;
/**
* Class Storage
* @package Xibo\Middleware
*/
class Storage implements Middleware
{
/* @var App $app */
private $app;
/**
* Storage constructor.
* @param $app
*/
public function __construct($app)
{
$this->app = $app;
}
/**
* Middleware process
* @param \Psr\Http\Message\ServerRequestInterface $request
* @param \Psr\Http\Server\RequestHandlerInterface $handler
* @return \Psr\Http\Message\ResponseInterface
*/
public function process(Request $request, RequestHandler $handler): Response
{
$container = $this->app->getContainer();
$startTime = microtime(true);
// Pass straight down to the next middleware
$response = $handler->handle($request);
// Are we in a transaction coming out of the stack?
if ($container->get('store')->getConnection()->inTransaction()) {
// We need to commit or rollback? Default is commit
if ($container->get('state')->getCommitState()) {
$container->get('store')->commitIfNecessary();
} else {
$container->get('logService')->debug('Storage rollback.');
$container->get('store')->getConnection()->rollBack();
}
}
// Get the stats for this connection
$stats = $container->get('store')->stats();
$stats['length'] = microtime(true) - $startTime;
$stats['memoryUsage'] = memory_get_usage();
$stats['peakMemoryUsage'] = memory_get_peak_usage();
$container->get('logService')->info('Request stats: %s.', json_encode($stats, JSON_PRETTY_PRINT));
$container->get('store')->close();
return $response;
}
}

View File

@@ -0,0 +1,77 @@
<?php
/*
* Copyright (C) 2020 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Middleware;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Xibo\Support\Exception\AccessDeniedException;
/**
* Class SuperAdminAuth
* @package Xibo\Middleware
*/
class SuperAdminAuth implements MiddlewareInterface
{
/** @var \Psr\Container\ContainerInterface */
private $container;
/** @var array */
private $features;
/**
* FeatureAuth constructor.
* @param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* @param \Psr\Http\Message\ServerRequestInterface $request
* @param \Psr\Http\Server\RequestHandlerInterface $handler
* @return \Psr\Http\Message\ResponseInterface
* @throws \Xibo\Support\Exception\AccessDeniedException
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
// If no features are provided, then this must be public
if (!$this->getUser()->isSuperAdmin()) {
throw new AccessDeniedException(__('You do not have sufficient access'));
}
return $handler->handle($request);
}
/**
* @return \Xibo\Entity\User
*/
private function getUser()
{
return $this->container->get('user');
}
}

162
lib/Middleware/Theme.php Normal file
View File

@@ -0,0 +1,162 @@
<?php
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Middleware;
use Illuminate\Support\Str;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface as Middleware;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\App as App;
use Slim\Interfaces\RouteParserInterface;
use Slim\Routing\RouteContext;
use Xibo\Helper\ByteFormatter;
use Xibo\Helper\DateFormatHelper;
use Xibo\Helper\Environment;
use Xibo\Helper\Translate;
/**
* Class Theme
* @package Xibo\Middleware
*/
class Theme implements Middleware
{
/* @var App $app */
private $app;
public function __construct($app)
{
$this->app = $app;
}
/**
* @param Request $request
* @param RequestHandler $handler
* @return Response
* @throws \Twig\Error\LoaderError
* @throws \Xibo\Support\Exception\GeneralException
*/
public function process(Request $request, RequestHandler $handler): Response
{
// Inject our Theme into the Twig View (if it exists)
$app = $this->app;
$app->getContainer()->get('configService')->loadTheme();
self::setTheme($app->getContainer(), $request, $app->getRouteCollector()->getRouteParser());
return $handler->handle($request);
}
/**
* Set theme
* @param \Psr\Container\ContainerInterface $container
* @param Request $request
* @param RouteParserInterface $routeParser
* @throws \Twig\Error\LoaderError
* @throws \Xibo\Support\Exception\GeneralException
*/
public static function setTheme(ContainerInterface $container, Request $request, RouteParserInterface $routeParser)
{
$view = $container->get('view');
// Provide the view path to Twig
$twig = $view->getLoader();
/* @var \Twig\Loader\FilesystemLoader $twig */
// Does this theme provide an alternative view path?
if ($container->get('configService')->getThemeConfig('view_path') != '') {
$twig->prependPath(
Str::replaceFirst(
'..',
PROJECT_ROOT,
$container->get('configService')->getThemeConfig('view_path')
)
);
}
$settings = $container->get('configService')->getSettings();
// Date format
$settings['DATE_FORMAT_JS'] = DateFormatHelper::convertPhpToMomentFormat($settings['DATE_FORMAT']);
$settings['DATE_FORMAT_JALALI_JS'] = DateFormatHelper::convertMomentToJalaliFormat($settings['DATE_FORMAT_JS']);
$settings['TIME_FORMAT'] = DateFormatHelper::extractTimeFormat($settings['DATE_FORMAT']);
$settings['TIME_FORMAT_JS'] = DateFormatHelper::convertPhpToMomentFormat($settings['TIME_FORMAT']);
$settings['DATE_ONLY_FORMAT'] = DateFormatHelper::extractDateOnlyFormat($settings['DATE_FORMAT']);
$settings['DATE_ONLY_FORMAT_JS'] = DateFormatHelper::convertPhpToMomentFormat($settings['DATE_ONLY_FORMAT']);
$settings['DATE_ONLY_FORMAT_JALALI_JS'] = DateFormatHelper::convertMomentToJalaliFormat(
$settings['DATE_ONLY_FORMAT_JS']
);
$settings['systemDateFormat'] = DateFormatHelper::convertPhpToMomentFormat(DateFormatHelper::getSystemFormat());
$settings['systemTimeFormat'] = DateFormatHelper::convertPhpToMomentFormat(
DateFormatHelper::extractTimeFormat(DateFormatHelper::getSystemFormat())
);
$routeContext = RouteContext::fromRequest($request);
$route = $routeContext->getRoute();
// Resolve the current route name
$routeName = ($route == null) ? 'notfound' : $route->getName();
$view['baseUrl'] = $routeParser->urlFor('home');
try {
$logoutRoute = empty($container->get('logoutRoute')) ? 'logout' : $container->get('logoutRoute');
$view['logoutUrl'] = $routeParser->urlFor($logoutRoute);
} catch (\Exception $e) {
$view['logoutUrl'] = $routeParser->urlFor('logout');
}
$view['route'] = $routeName;
$view['theme'] = $container->get('configService');
$view['settings'] = $settings;
$view['helpService'] = $container->get('helpService');
$view['translate'] = [
'locale' => Translate::GetLocale(),
'jsLocale' => Translate::getRequestedJsLocale(),
'jsShortLocale' => Translate::getRequestedJsLocale(['short' => true])
];
$view['translations'] ='{}';
$view['libraryUpload'] = [
'maxSize' => ByteFormatter::toBytes(Environment::getMaxUploadSize()),
'maxSizeMessage' => sprintf(
__('This form accepts files up to a maximum size of %s'),
Environment::getMaxUploadSize()
),
'validExt' => implode('|', $container->get('moduleFactory')->getValidExtensions()),
'validImageExt' => implode('|', $container->get('moduleFactory')->getValidExtensions(['type' => 'image']))
];
$view['version'] = Environment::$WEBSITE_VERSION_NAME;
$view['revision'] = Environment::getGitCommit();
$view['playerVersion'] = Environment::$PLAYER_SUPPORT;
$view['isDevMode'] = Environment::isDevMode();
$view['accountId'] = defined('ACCOUNT_ID') ? constant('ACCOUNT_ID') : null;
$samlSettings = $container->get('configService')->samlSettings;
if (isset($samlSettings['workflow'])
&& isset($samlSettings['workflow']['slo'])
&& $samlSettings['workflow']['slo'] == false) {
$view['hideLogout'] = true;
}
}
}

View File

@@ -0,0 +1,70 @@
<?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\Middleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\App;
/**
* Trailing Slash Middleware
* this middleware is used for routes contained inside a directory
* Apache automatically adds a trailing slash to these URLs, Nginx does not.
* Slim treats trailing slashes differently to non-trailing slashes
* We need to mimic Apache for the director route.
* @package Xibo\Middleware
*/
class TrailingSlashMiddleware implements MiddlewareInterface
{
/* @var App $app */
private $app;
/**
* Storage constructor.
* @param $app
*/
public function __construct($app)
{
$this->app = $app;
}
/**
* Middleware process
* @param \Psr\Http\Message\ServerRequestInterface $request
* @param \Psr\Http\Server\RequestHandlerInterface $handler
* @return \Psr\Http\Message\ResponseInterface
*/
public function process(Request $request, RequestHandler $handler): Response
{
$uri = $request->getUri();
$path = $uri->getPath();
if ($path === $this->app->getBasePath()) {
// Add a trailing slash for the route middleware to match
$request = $request->withUri($uri->withPath($path . '/'));
}
return $handler->handle($request);
}
}

View File

@@ -0,0 +1,69 @@
<?php
/**
* Copyright (C) 2020 Xibo Signage Ltd
*
* Xibo - Digital Signage - http://www.xibo.org.uk
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Middleware;
use Psr\Http\Message\ServerRequestInterface as Request;
use Xibo\Helper\ApplicationState;
/**
* Class WebAuthentication
* @package Xibo\Middleware
*/
class WebAuthentication extends AuthenticationBase
{
/** @inheritDoc */
public function addRoutes()
{
return $this;
}
/** @inheritDoc */
public function redirectToLogin(Request $request)
{
if ($this->isAjax($request)) {
return $this->createResponse($request)
->withJson(ApplicationState::asRequiresLogin());
} else {
return $this->createResponse($request)->withRedirect($this->getRouteParser()->urlFor('login'));
}
}
/** @inheritDoc */
public function getPublicRoutes(Request $request)
{
return $request->getAttribute('publicRoutes', []);
}
/** @inheritDoc */
public function shouldRedirectPublicRoute($route)
{
return $this->getSession()->isExpired() && ($route == '/login/ping' || $route == 'clock');
}
/** @inheritDoc */
public function addToRequest(Request $request)
{
return $request;
}
}

142
lib/Middleware/Xmr.php Normal file
View File

@@ -0,0 +1,142 @@
<?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\Middleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface as Middleware;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\App as App;
use Xibo\Service\DisplayNotifyService;
use Xibo\Service\NullDisplayNotifyService;
use Xibo\Service\PlayerActionService;
use Xibo\Support\Exception\GeneralException;
/**
* Class Xmr
* @package Xibo\Middleware
*
* NOTE: This must be the very last layer in the onion
*/
class Xmr implements Middleware
{
/* @var App $app */
private $app;
/**
* Xmr constructor.
* @param $app
*/
public function __construct($app)
{
$this->app = $app;
}
/**
* Call
* @param Request $request
* @param RequestHandler $handler
* @return Response
*/
public function process(Request $request, RequestHandler $handler): Response
{
$app = $this->app;
// Start
self::setXmr($app);
// Pass along the request
$response = $handler->handle($request);
// Finish
// this must happen at the very end of the request
self::finish($app);
// Return the response to the browser
return $response;
}
/**
* Finish XMR
* @param App $app
*/
public static function finish($app)
{
$container = $app->getContainer();
// Handle display notifications
if ($container->has('displayNotifyService')) {
try {
$container->get('displayNotifyService')->processQueue();
} catch (GeneralException $e) {
$container->get('logService')->error(
'Unable to Process Queue of Display Notifications due to %s',
$e->getMessage()
);
}
}
// Handle player actions
if ($container->has('playerActionService')) {
try {
$container->get('playerActionService')->processQueue();
} catch (\Exception $e) {
$container->get('logService')->error(
'Unable to Process Queue of Player actions due to %s',
$e->getMessage()
);
}
}
// Re-terminate any DB connections
$app->getContainer()->get('store')->close();
}
/**
* Set XMR
* @param App $app
* @param bool $triggerPlayerActions
*/
public static function setXmr($app, $triggerPlayerActions = true)
{
// Player Action Helper
$app->getContainer()->set('playerActionService', function () use ($app, $triggerPlayerActions) {
return new PlayerActionService(
$app->getContainer()->get('configService'),
$app->getContainer()->get('logService'),
$triggerPlayerActions
);
});
// Register the display notify service
$app->getContainer()->set('displayNotifyService', function () use ($app) {
return new DisplayNotifyService(
$app->getContainer()->get('configService'),
$app->getContainer()->get('logService'),
$app->getContainer()->get('store'),
$app->getContainer()->get('pool'),
$app->getContainer()->get('playerActionService'),
$app->getContainer()->get('scheduleFactory')
);
});
}
}

88
lib/Middleware/Xtr.php Normal file
View File

@@ -0,0 +1,88 @@
<?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\Middleware;
use Illuminate\Support\Str;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface as Middleware;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\App as App;
use Xibo\Support\Exception\InstanceSuspendedException;
/**
* Class Xtr
* Middleware for XTR.
* - sets the theme
* - sets the module theme files
* @package Xibo\Middleware
*/
class Xtr implements Middleware
{
/* @var App $app */
private $app;
public function __construct($app)
{
$this->app = $app;
}
/**
* @param Request $request
* @param RequestHandler $handler
* @return Response
* @throws \Twig\Error\LoaderError
* @throws \Xibo\Support\Exception\InstanceSuspendedException
*/
public function process(Request $request, RequestHandler $handler): Response
{
// Inject our Theme into the Twig View (if it exists)
$app = $this->app;
$container = $app->getContainer();
// Check to see if the instance has been suspended, if so call the special route
$instanceSuspended = $container->get('configService')->getSetting('INSTANCE_SUSPENDED');
if ($instanceSuspended == 'yes' || $instanceSuspended == 'partial') {
throw new InstanceSuspendedException();
}
$container->get('configService')->loadTheme();
$view = $container->get('view');
// Provide the view path to Twig
/* @var \Twig\Loader\FilesystemLoader $twig */
$twig = $view->getLoader();
$twig->setPaths([PROJECT_ROOT . '/views', PROJECT_ROOT . '/custom', PROJECT_ROOT . '/reports']);
// Does this theme provide an alternative view path?
if ($container->get('configService')->getThemeConfig('view_path') != '') {
$twig->prependPath(Str::replaceFirst(
'..',
PROJECT_ROOT,
$container->get('configService')->getThemeConfig('view_path'),
));
}
// Call Next
return $handler->handle($request);
}
}