maintenance-1.0.0-beta1/src/Maintenance.php
src/Maintenance.php
<?php
namespace Drupal\maintenance;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Path\CurrentPathStack;
use Drupal\Core\Path\PathMatcherInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Site\MaintenanceMode;
use Drupal\Core\Site\MaintenanceModeInterface;
use Drupal\Core\State\StateInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Custom implementation of the core MaintenanceMode service.
*
* Adds support for IP, route, and query string exemptions during
* maintenance mode.
*/
class Maintenance extends MaintenanceMode implements MaintenanceModeInterface {
use MaintenanceStringHelperTrait;
/**
* The Request Stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected RequestStack $requestStack;
/**
* Path matcher service for checking route patterns.
*
* @var \Drupal\Core\Path\PathMatcherInterface
*/
protected PathMatcherInterface $pathMatcher;
/**
* Current path stack for alias-aware path resolution.
*
* @var \Drupal\Core\Path\CurrentPathStack
*/
protected CurrentPathStack $currentPathStack;
/**
* The custom maintenance manager service.
*
* @var \Drupal\maintenance\MaintenanceManagerInterface
*/
protected MaintenanceManagerInterface $maintenance;
/**
* Constructs a new Maintenance object.
*
* @param \Drupal\Core\State\StateInterface $state
* The state service for site-wide flags.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* Stack for retrieving the current HTTP request.
* @param \Drupal\Core\Path\PathMatcherInterface $path_matcher
* Matches current request path against wildcard paths.
* @param \Drupal\Core\Path\CurrentPathStack $current_path
* Provides access to the aliased and internal paths.
* @param \Drupal\maintenance\MaintenanceManagerInterface $maintenance
* The custom maintenance manager service.
*/
public function __construct(
StateInterface $state,
ConfigFactoryInterface $config_factory,
RequestStack $request_stack,
PathMatcherInterface $path_matcher,
CurrentPathStack $current_path,
MaintenanceManagerInterface $maintenance,
) {
parent::__construct($state, $config_factory);
$this->requestStack = $request_stack;
$this->pathMatcher = $path_matcher;
$this->currentPathStack = $current_path;
$this->maintenance = $maintenance;
}
/**
* {@inheritdoc}
*/
public function exempt(AccountInterface $account): bool {
// Grant immediate exemption if the user has the required permission.
if ($account->hasPermission('access site in maintenance mode')) {
return TRUE;
}
// Check exemption rules in a logical order: IP, URL, then query string.
return $this->isIpExempt() || $this->isUrlExempt() || $this->isQueryExempt();
}
/**
* Checks if client's IP is exempt from maintenance mode.
*
* Exemption is based on the IP visibility setting:
* - visibility = 0:
* Allow access to listed IPs (deny others).
* - visibility = 1:
* Show maintenance page only to listed IPs (allow others).
*
* @return bool
* TRUE if the client IP is exempt, FALSE otherwise.
*/
protected function isIpExempt(): bool {
// Get configured IP addresses.
$ips = $this->maintenance->getAllowedIps();
// Skip IP filter if no IPs configured.
if (empty($ips)) {
return FALSE;
}
// Get IP visibility setting.
$visibility = (int) $this->maintenance->getConfig()->get('ip.visibility');
// Get client IP from current request.
$request = $this->requestStack->getCurrentRequest();
$client_ip = $request->getClientIp();
// Check IP match or CIDR range.
$match = in_array($client_ip, $ips, TRUE) || $this->maintenance->isCidrMatch($client_ip);
// Apply visibility: 0=allow listed, 1=show to listed.
return $visibility === 0 ? $match : !$match;
}
/**
* Checks if current URL path is exempt from maintenance mode.
*
* Exemption is based on the page visibility setting:
* - visibility = 0:
* Allow access to listed pages (deny others).
* - visibility = 1:
* Show maintenance page only on listed pages (allow others).
*
* @return bool
* TRUE if the current path is exempt, FALSE otherwise.
*/
protected function isUrlExempt(): bool {
// Get configured exempt URLs.
$urls = $this->maintenance->getAllowedUrls();
// Skip URL filter if no URLs configured.
if (empty($urls)) {
return FALSE;
}
// Get page visibility setting.
$visibility = (int) $this->maintenance->getConfig()->get('page.visibility');
// Convert URLs to newline-separated string.
$urls_string = implode("\n", $urls);
// Get actual and alias paths from request.
$request = $this->requestStack->getCurrentRequest();
$actual_path = $request->getPathInfo();
$alias_path = $this->currentPathStack->getPath();
// Check if path or alias matches exempt URLs.
$match = $this->pathMatcher->matchPath($actual_path, $urls_string) ||
$this->pathMatcher->matchPath($alias_path, $urls_string);
// Apply visibility: 0=allow listed, 1=show to listed.
return $visibility === 0 ? $match : !$match;
}
/**
* Checks if query string grants exemption from maintenance mode.
*
* If a configured query string key is present in the request or session,
* the user is exempt from maintenance mode.
*
* @return bool
* TRUE if query string matches, FALSE otherwise.
*/
protected function isQueryExempt(): bool {
// Get configured query string.
$key = $this->maintenance->getConfig()->get('query.strings') ?? '';
// Return FALSE if no query string configured.
if (empty($key) || !is_string($key)) {
return FALSE;
}
// Get current request.
$request = $this->requestStack->getCurrentRequest();
// Check if the exemption key was previously stored in the session.
if (isset($_SESSION['maintenance']) && $_SESSION['maintenance'] == $key) {
return TRUE;
}
// If query has the key, store in session and allow access.
if ($request->query->has($key)) {
$_SESSION['maintenance_exempt'] = $key;
return TRUE;
}
// Query key not found, access is not exempted.
return FALSE;
}
}
