maintenance-1.0.0-beta1/src/MaintenanceManager.php
src/MaintenanceManager.php
<?php
namespace Drupal\maintenance;
use Drupal\Core\Config\Config;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Provides the default implementation of the maintenance manager service.
*/
class MaintenanceManager implements MaintenanceManagerInterface {
use StringTranslationTrait;
use MaintenanceStringHelperTrait;
/**
* The configuration factory service.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected ConfigFactoryInterface $config;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected AccountInterface $account;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected ModuleHandlerInterface $moduleHandler;
/**
* Constructs a new MaintenanceManager instance.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\Core\Session\AccountInterface $account
* The current user object.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
*/
public function __construct(ConfigFactoryInterface $config_factory, AccountInterface $account, ModuleHandlerInterface $module_handler) {
$this->config = $config_factory;
$this->account = $account;
$this->moduleHandler = $module_handler;
}
/**
* {@inheritdoc}
*/
public function getConfig(): ImmutableConfig|Config {
return $this->config->getEditable('maintenance.settings');
}
/**
* {@inheritdoc}
*/
public function getDefaultDateFormat(): string {
// Could be moved to settings later.
return 'Y-m-d H:i:s';
}
/**
* {@inheritdoc}
*/
public function getAllowedUnits(): array {
return [
'days' => $this->t('day(s)'),
'hours' => $this->t('hour(s)'),
'minutes' => $this->t('minute(s)'),
'seconds' => $this->t('second(s)'),
];
}
/**
* {@inheritdoc}
*/
public function getAllowedIps(): array {
// Retrieve the list of allowed IP addresses.
$allowed_ips = $this->getConfig()->get('ip.addresses') ?? '';
// Normalize to string if stored as array.
if (is_array($allowed_ips)) {
$allowed_ips = $this->arrayToString($allowed_ips);
}
// Cast non-string input safely.
$ips = is_string($allowed_ips) ? $allowed_ips : '';
// Safely split into trimmed, non-empty lines.
$lines = preg_split('/\r?\n/', $ips) ?: [];
// Return filtered list with trimmed, non-empty or invalid entries.
return array_values(array_filter(array_map('trim', $lines), static function ($ip) {
return $ip !== '';
}));
}
/**
* {@inheritdoc}
*/
public function getAllowedUrls(): array {
// Retrieve the list of allowed route paths.
$allowed_urls = $this->getConfig()->get('page.routes') ?? '';
// Normalize to string if stored as array.
if (is_array($allowed_urls)) {
$allowed_urls = $this->arrayToString($allowed_urls);
}
// Ensure it's a string before splitting.
$urls = is_string($allowed_urls) ? $allowed_urls : '';
// Safely split into trimmed, non-empty lines.
$lines = preg_split('/\r?\n/', $urls) ?: [];
// Return filtered list with trimmed, non-empty or invalid entries.
return array_values(array_filter(array_map('trim', $lines), static function ($url) {
return $url !== '';
}));
}
/**
* {@inheritdoc}
*/
public function getAvailableThemes(array $options = []): array {
// Get all available theme options via hook_maintenance_page_theme().
// Array reversed to prioritize module-defined templates.
$theme_info = array_reverse(
$this->moduleHandler->invokeAll('maintenance_page_theme', [$options])
);
// Default options provided by the module.
$theme_options = [
'' => $this->t('Default theme'),
'clean' => $this->t('Clean'),
];
foreach ($theme_info as $theme => $info) {
// Allow only lowercase alphabetic machine names.
if (!preg_match('/^[a-z]+$/', $theme)) {
continue;
}
// Validate presence of required metadata.
$name = trim($info['name'] ?? '');
$description = trim($info['description'] ?? '');
if ($name === '' || $description === '') {
continue;
}
// Add the valid option to the list.
$theme_options[$theme] = [
'name' => $name,
'description' => $description,
];
}
return $theme_options;
}
/**
* {@inheritdoc}
*/
public function getHttpStatusCodes(): array {
return [
// Informational 1xx.
100 => 'Continue',
101 => 'Switching Protocols',
102 => 'Processing',
// Success 2xx.
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authoritative Information',
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
207 => 'Multi-Status',
// Redirection 3xx.
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found',
303 => 'See Other',
304 => 'Not Modified',
305 => 'Use Proxy',
306 => 'Switch Proxy',
307 => 'Temporary Redirect',
// Client Errors 4xx.
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required',
408 => 'Request Timeout',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Request Entity Too Large',
414 => 'Request-URI Too Long',
415 => 'Unsupported Media Type',
416 => 'Requested Range Not Satisfiable',
417 => 'Expectation Failed',
418 => 'I\'m a teapot',
422 => 'Unprocessable Entity',
423 => 'Locked',
424 => 'Failed Dependency',
425 => 'Unordered Collection',
426 => 'Upgrade Required',
449 => 'Retry With',
450 => 'Blocked by Windows Parental Controls',
// Server Errors 5xx.
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
505 => 'HTTP Version Not Supported',
506 => 'Variant Also Negotiates',
507 => 'Insufficient Storage',
509 => 'Bandwidth Limit Exceeded',
510 => 'Not Extended',
];
}
/**
* {@inheritdoc}
*/
public function getHttpStatusLabel(int $code): ?string {
$codes = $this->getHttpStatusCodes();
return $codes[$code] ?? NULL;
}
/**
* {@inheritdoc}
*/
public function hasAccess(): bool {
return $this->account->hasPermission('access site in maintenance mode');
}
/**
* {@inheritdoc}
*/
public function isCidrMatch(string $ip): bool {
// Load all IP addresses allowed during maintenance mode.
$allowed_ips = $this->getAllowedIps();
// Loop through each entry to check for CIDR match.
foreach ($allowed_ips as $entry) {
// Only evaluate entries in CIDR format (ignore plain IPs).
// Return TRUE if the IP matches the current CIDR block.
if (strpos($entry, '/') !== FALSE && $this->checkCidrMatch($ip, $entry)) {
return TRUE;
}
}
// No matching CIDR found.
return FALSE;
}
/**
* {@inheritdoc}
*/
public function checkCidrMatch(string $ip, string $cidr): bool {
// Check if the CIDR string contains a '/' separator.
if (strpos($cidr, '/') === FALSE) {
return FALSE;
}
// Split the CIDR into network address and mask length.
[$network, $maskLength] = explode('/', $cidr, 2);
// Convert the network address to a 32-bit integer.
$networkLong = ip2long($network);
if ($networkLong === FALSE) {
return FALSE;
}
// Convert the input IP address to a 32-bit integer.
$ipLong = ip2long($ip);
if ($ipLong === FALSE) {
return FALSE;
}
// Cast mask length to integer and validate its range (0-32).
$maskLength = (int) $maskLength;
if ($maskLength < 0 || $maskLength > 32) {
return FALSE;
}
// Generate the subnet mask using bitwise operations.
$subnetMask = ~((1 << (32 - $maskLength)) - 1);
// Compare the masked IP and network addresses.
return ($ipLong & $subnetMask) === ($networkLong & $subnetMask);
}
}
