Initial Upload
This commit is contained in:
150
lib/Middleware/Actions.php
Normal file
150
lib/Middleware/Actions.php
Normal 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';
|
||||
}
|
||||
}
|
||||
110
lib/Middleware/ApiAuthentication.php
Normal file
110
lib/Middleware/ApiAuthentication.php
Normal 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'));
|
||||
}
|
||||
}
|
||||
215
lib/Middleware/ApiAuthorization.php
Normal file
215
lib/Middleware/ApiAuthorization.php
Normal 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
129
lib/Middleware/ApiView.php
Normal 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();
|
||||
}
|
||||
}
|
||||
116
lib/Middleware/AuthenticationBase.php
Normal file
116
lib/Middleware/AuthenticationBase.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
69
lib/Middleware/AuthenticationInterface.php
Normal file
69
lib/Middleware/AuthenticationInterface.php
Normal 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);
|
||||
}
|
||||
209
lib/Middleware/AuthenticationTrait.php
Normal file
209
lib/Middleware/AuthenticationTrait.php
Normal 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);
|
||||
}
|
||||
}
|
||||
175
lib/Middleware/CASAuthentication.php
Normal file
175
lib/Middleware/CASAuthentication.php
Normal 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'])
|
||||
);
|
||||
}
|
||||
}
|
||||
82
lib/Middleware/ConnectorMiddleware.php
Normal file
82
lib/Middleware/ConnectorMiddleware.php
Normal 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
74
lib/Middleware/Csp.php
Normal 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);
|
||||
}
|
||||
}
|
||||
123
lib/Middleware/CsrfGuard.php
Normal file
123
lib/Middleware/CsrfGuard.php
Normal 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);
|
||||
}
|
||||
}
|
||||
76
lib/Middleware/CustomDisplayProfileInterface.php
Normal file
76
lib/Middleware/CustomDisplayProfileInterface.php
Normal 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;
|
||||
}
|
||||
143
lib/Middleware/CustomDisplayProfileMiddlewareTrait.php
Normal file
143
lib/Middleware/CustomDisplayProfileMiddlewareTrait.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
90
lib/Middleware/CustomMiddlewareTrait.php
Normal file
90
lib/Middleware/CustomMiddlewareTrait.php
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
89
lib/Middleware/FeatureAuth.php
Normal file
89
lib/Middleware/FeatureAuth.php
Normal 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
289
lib/Middleware/Handlers.php
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
120
lib/Middleware/HttpCache.php
Normal file
120
lib/Middleware/HttpCache.php
Normal 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;
|
||||
}
|
||||
}
|
||||
218
lib/Middleware/LayoutLock.php
Normal file
218
lib/Middleware/LayoutLock.php
Normal 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;
|
||||
}
|
||||
}
|
||||
424
lib/Middleware/ListenersMiddleware.php
Normal file
424
lib/Middleware/ListenersMiddleware.php
Normal 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
77
lib/Middleware/Log.php
Normal 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(),
|
||||
));
|
||||
}
|
||||
}
|
||||
488
lib/Middleware/SAMLAuthentication.php
Normal file
488
lib/Middleware/SAMLAuthentication.php
Normal 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
336
lib/Middleware/State.php
Normal 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';
|
||||
}
|
||||
}
|
||||
87
lib/Middleware/Storage.php
Normal file
87
lib/Middleware/Storage.php
Normal 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;
|
||||
}
|
||||
}
|
||||
77
lib/Middleware/SuperAdminAuth.php
Normal file
77
lib/Middleware/SuperAdminAuth.php
Normal 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
162
lib/Middleware/Theme.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
70
lib/Middleware/TrailingSlashMiddleware.php
Normal file
70
lib/Middleware/TrailingSlashMiddleware.php
Normal 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);
|
||||
}
|
||||
}
|
||||
69
lib/Middleware/WebAuthentication.php
Normal file
69
lib/Middleware/WebAuthentication.php
Normal 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
142
lib/Middleware/Xmr.php
Normal 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
88
lib/Middleware/Xtr.php
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user