337 lines
13 KiB
PHP
337 lines
13 KiB
PHP
|
|
<?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';
|
||
|
|
}
|
||
|
|
}
|