maintenance-1.0.0-beta1/src/EventSubscriber/MaintenanceSubscriber.php

src/EventSubscriber/MaintenanceSubscriber.php
<?php

namespace Drupal\maintenance\EventSubscriber;

use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Utility\Html;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Path\CurrentPathStack;
use Drupal\Core\Render\BareHtmlPageRendererInterface;
use Drupal\Core\Routing\RouteMatch;
use Drupal\Core\Routing\TrustedRedirectResponse;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Site\MaintenanceModeEvents;
use Drupal\Core\State\StateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\maintenance\Maintenance;
use Drupal\maintenance\MaintenanceManagerInterface;
use Drupal\node\NodeInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

/**
 * Subscribes to maintenance mode events and handles custom logic.
 *
 * @package Drupal\maintenance\EventSubscriber
 */
class MaintenanceSubscriber implements EventSubscriberInterface {

  use StringTranslationTrait;

  /**
   * The maintenance mode.
   *
   * @var \Drupal\maintenance\Maintenance
   */
  protected Maintenance $maintenanceMode;

  /**
   * The custom maintenance manager service.
   *
   * @var \Drupal\maintenance\MaintenanceManagerInterface
   */
  protected MaintenanceManagerInterface $maintenance;

  /**
   * The config factory.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected ConfigFactoryInterface $config;

  /**
   * The state service.
   *
   * @var \Drupal\Core\State\StateInterface
   */
  protected StateInterface $state;

  /**
   * The current user service.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected AccountInterface $account;

  /**
   * The current path service.
   *
   * @var \Drupal\Core\Path\CurrentPathStack
   */
  protected CurrentPathStack $currentPath;

  /**
   * The time service.
   *
   * @var \Drupal\Component\Datetime\TimeInterface
   */
  protected TimeInterface $time;

  /**
   * The bare HTML page renderer.
   *
   * @var \Drupal\Core\Render\BareHtmlPageRendererInterface
   */
  protected BareHtmlPageRendererInterface $bareHtmlPageRenderer;

  /**
   * The entity type manager service.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected EntityTypeManagerInterface $entityTypeManager;

  /**
   * The messenger.
   *
   * @var \Drupal\Core\Messenger\MessengerInterface
   */
  protected MessengerInterface $messenger;

  /**
   * Constructs a new MaintenanceSubscriber.
   *
   * @param \Drupal\maintenance\Maintenance $maintenance_mode
   *   The maintenance mode service.
   * @param \Drupal\maintenance\MaintenanceManagerInterface $maintenance
   *   The custom maintenance manager service.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The configuration factory.
   * @param \Drupal\Core\State\StateInterface $state
   *   The state service.
   * @param \Drupal\Core\Session\AccountInterface $account
   *   The current user account.
   * @param \Drupal\Core\Path\CurrentPathStack $current_path
   *   The current path stack.
   * @param \Drupal\Component\Datetime\TimeInterface $time
   *   The time service.
   * @param \Drupal\Core\Render\BareHtmlPageRendererInterface $bare_html_page_renderer
   *   The bare HTML page renderer.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager service.
   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
   *   The messenger service.
   */
  public function __construct(
    Maintenance $maintenance_mode,
    MaintenanceManagerInterface $maintenance,
    ConfigFactoryInterface $config_factory,
    StateInterface $state,
    AccountInterface $account,
    CurrentPathStack $current_path,
    TimeInterface $time,
    BareHtmlPageRendererInterface $bare_html_page_renderer,
    EntityTypeManagerInterface $entity_type_manager,
    MessengerInterface $messenger,
  ) {
    $this->maintenanceMode = $maintenance_mode;
    $this->maintenance = $maintenance;
    $this->config = $config_factory;
    $this->state = $state;
    $this->account = $account;
    $this->currentPath = $current_path;
    $this->time = $time;
    $this->bareHtmlPageRenderer = $bare_html_page_renderer;
    $this->entityTypeManager = $entity_type_manager;
    $this->messenger = $messenger;
  }

  /**
   * Adds a response header indicating maintenance mode is active.
   *
   * This header can be used by proxies or monitoring systems to detect
   * maintenance state and act accordingly.
   *
   * @param \Symfony\Component\HttpKernel\Event\ResponseEvent $event
   *   The response event object from the HTTP kernel.
   */
  public function onRespond(ResponseEvent $event): void {
    // Only handle the main request, ignore sub-requests (e.g., assets).
    if (!$event->isMainRequest()) {
      return;
    }

    // Check global maintenance mode state.
    if ($this->state->get('system.maintenance_mode')) {
      // Add a custom header to signal that the site is under maintenance.
      $event->getResponse()->headers->set('maintenance-mode', 'on');
    }
  }

  /**
   * Overrides the HTTP status code if configured and applicable.
   *
   * If the override is enabled and user is not exempt, replace the
   * default status code with the one from configuration (e.g., 503).
   *
   * @param \Symfony\Component\HttpKernel\Event\ResponseEvent $event
   *   The response event dispatched by the kernel.
   */
  public function onKernelResponse(ResponseEvent $event): void {
    // Only proceed if custom status override is enabled in config.
    $config = $this->config->get('maintenance.settings');
    if (!$config->get('status.enabled')) {
      return;
    }

    // Fetch the status code to be applied (e.g. 503).
    $status_code = (int) $config->get('status.code');

    // Create a route match object for current request.
    $route_match = RouteMatch::createFromRequest($event->getRequest());

    // Get the response object from the event.
    $response = $event->getResponse();

    // Only override if the current route is subject to maintenance
    // and the user does not have access to bypass it.
    if (
      $this->maintenanceMode->applies($route_match) &&
      !$this->maintenanceMode->exempt($this->account)
    ) {
      $response->setStatusCode($status_code);
    }
  }

  /**
   * Displays a maintenance page for non-exempt users.
   *
   * This method renders a customized maintenance page when maintenance
   * mode is active and the current user does not have access.
   * It supports both config-based and node-based content, optional
   * redirect logic, and auto-reload options for enhanced UX.
   *
   * @param \Symfony\Component\HttpKernel\Event\RequestEvent $event
   *   The request event triggered by the kernel.
   */
  public function onMaintenanceModeRequest(RequestEvent $event): void {
    // Load full configuration for maintenance mode.
    $config = $this->maintenance->getConfig();

    // Determine the source of the maintenance message content,
    // Supports node content, custom HTML, or fallback to default message.
    switch ($config->get('content.base')) {
      case 'node':
        // Get node ID from settings.
        $node_id = $config->get('content.node');

        // If node not set, abort rendering.
        if (empty($node_id)) {
          return;
        }

        // Attempt to load node object.
        /** @var \Drupal\node\NodeInterface|null $node */
        $node = $this->entityTypeManager
          ->getStorage('node')
          ->load($node_id);

        // If node not found, abort rendering.
        if (!$node instanceof NodeInterface) {
          return;
        }

        // Use node title and body as page content.
        $title = $node->label();
        $message = new FormattableMarkup($node->get('body')->value, [
          '@site' => $this->config->get('system.site')->get('name'),
        ]);
        break;

      case 'html':
        // Load title and body from config.
        $title = $config->get('content.title');
        $message = new FormattableMarkup($config->get('content.message'), [
          '@site' => $this->config->get('system.site')->get('name'),
        ]);
        break;

      default:
        // Load title and body from config.
        $title = $this->t('Site under maintenance');
        $message = $this->maintenanceMode->getSiteMaintenanceMessage();
        break;
    }

    // Get the current request object (used for content type detection, etc.).
    $request = $event->getRequest();

    // If request is non-HTML (e.g., JSON, XML), return plain text response.
    if ($request->getRequestFormat() !== 'html') {
      $response = new Response($message, 503, ['Content-Type' => 'text/plain']);
      // Calling RequestEvent::setResponse() also stops propagation of event.
      $event->setResponse($response);
      return;
    }

    // Build the render array for the maintenance page.
    $build = [
      '#title' => $title,
      '#markup' => $message,
    ];

    // Load access permission and settings for redirection and reload.
    $has_access = $this->maintenanceMode->exempt($this->account);

    // Extract redirect settings.
    $redirect_enabled = $config->get('redirect.enabled');
    $redirect_url = trim((string) $config->get('redirect.url'));
    $redirect_delay = (int) $config->get('redirect.delay');

    // If redirection is enabled and delay > 0, attach JS redirection.
    if (
      $redirect_enabled &&
      $redirect_delay > 0 &&
      !$has_access &&
      !empty($redirect_url)
    ) {
      $build['#attached']['html_head'][] = [
        [
          '#tag' => 'script',
          '#value' => sprintf(
            'setTimeout(function(){ window.location.href = "%s"; }, %d);',
            htmlspecialchars($redirect_url, ENT_QUOTES, 'UTF-8'),
            $redirect_delay * 1000
          ),
        ],
        'maintenance_redirect_script',
      ];
    }

    // Extract reload settings.
    $reload_enabled = $config->get('refresh.enabled');
    $reload_mode = (int) $config->get('refresh.reload');

    // Append manual reload button or auto-reload logic.
    if ($reload_enabled && !$has_access) {
      if ($reload_mode === 0) {
        // Show reload button.
        $reload_button =
          '<div style="text-align:center;margin-top:30px;">
            <button type="button" data-maintenance-reload class="button button--primary">
              Reload Page
            </button>
          </div>';

        $build['#markup'] .= $reload_button;
        $build['#allowed_tags'] = ['div', 'button'];
        $build['#attached']['library'][] = 'maintenance/maintenance.reload';
      }
      elseif ($reload_mode === 2) {
        // Auto-reload the page after 15 seconds.
        $build['#attached']['html_head'][] = [
          [
            '#tag' => 'script',
            '#value' => 'setTimeout(function(){ location.reload(); }, 15000);',
          ],
          'maintenance_auto_reload_script',
        ];
      }
    }

    // Load the maintenance theme for bare page rendering.
    drupal_maintenance_theme();

    // Render the maintenance page using the bare page renderer.
    $response = $this->bareHtmlPageRenderer->renderBarePage(
      $build,
      $title,
      'maintenance_page'
    );

    // Set the HTTP status code to 503 (Service Unavailable).
    $response->setStatusCode(503);

    // Calling RequestEvent::setResponse() also stops propagation of the event.
    $event->setResponse($response);
  }

  /**
   * Performs immediate redirect during maintenance mode if required.
   *
   * This handles the case where redirection should happen immediately via
   * HTTP 302 (without delay). JavaScript-based delayed redirection is handled
   * separately inside the maintenance page rendering.
   *
   * @param \Symfony\Component\HttpKernel\Event\RequestEvent $event
   *   The current kernel request event.
   */
  public function checkForRedirection(RequestEvent $event): void {
    // Ensure we are processing the main request only.
    if (!$event->isMainRequest()) {
      return;
    }

    // Skip redirect for users with maintenance access permission.
    if ($this->account->hasPermission('access site in maintenance mode')) {
      return;
    }

    // Check if the site is currently in maintenance mode.
    $maintenance_mode = $this->state->get('system.maintenance_mode');

    // Retrieve maintenance module configuration.
    $config = $this->config->get('maintenance.settings');

    // Extract redirect settings.
    $redirect_enabled = (bool) $config->get('redirect.enabled');
    $redirect_url = trim((string) $config->get('redirect.url'));
    $redirect_delay = (int) $config->get('redirect.delay');

    // Skip redirection if:
    // - Maintenance mode is not active,
    // - Redirect feature is disabled,
    // - No target URL is provided,
    // - Redirect delay is greater than zero (handled by JS).
    if (
      !$maintenance_mode ||
      !$redirect_enabled ||
      empty($redirect_url) ||
      $redirect_delay > 0
    ) {
      return;
    }

    // Define system paths that should be excluded from redirection.
    $excluded_paths = [
      '/user',
      '/user/login',
      '/user/password',
    ];

    // Get the current request path.
    $current_path = $this->currentPath->getPath();

    // Only redirect if the current path is not in the exclusion list.
    if (!in_array($current_path, $excluded_paths, TRUE)) {
      // Perform HTTP 302 temporary redirect to target URL.
      $event->setResponse(new TrustedRedirectResponse($redirect_url, 302));
    }
  }

  /**
   * Handles scheduled start and end of maintenance mode.
   *
   * This method checks if a scheduled maintenance time is configured
   * and either activates maintenance mode automatically or displays
   * a warning message ahead of time.
   *
   * @param \Symfony\Component\HttpKernel\Event\RequestEvent $event
   *   The request event object.
   */
  public function checkForScheduled(RequestEvent $event): void {
    // Load configuration from manager service.
    $config = $this->maintenance->getConfig();

    // Skip processing if schedule feature is disabled.
    if (!$config->get('schedule.enabled')) {
      return;
    }

    // Get current Unix timestamp.
    $now = $this->time->getRequestTime();

    // Load scheduled start datetime string.
    $start_time_str = $config->get('schedule.start');
    $start_time = $start_time_str ? strtotime($start_time_str) : NULL;

    // Check if Auto-disable is enabled (defaults to TRUE if not set).
    $auto_disable = (bool) $config->get('schedule.automatic');

    if ($start_time && $now >= $start_time) {
      if (!$this->state->get('system.maintenance_mode')) {
        // Turn ON maintenance mode.
        $this->state->set('system.maintenance_mode', TRUE);
      }

      // Clear start time after activation.
      $config->clear('schedule.start');

      if (!$auto_disable) {
        // This is a one-time schedule with no auto-disable.
        // Disable the scheduling system to prevent future activation.
        $config->set('schedule.enabled', FALSE);
      }

      // Save the updated configuration.
      $config->save();
    }

    // Load scheduled end datetime string.
    $end_time_str = $config->get('schedule.end');
    $end_time = $end_time_str ? strtotime($end_time_str) : NULL;

    if (
      $auto_disable &&
      $end_time &&
      $this->state->get('system.maintenance_mode') &&
      $now >= $end_time
    ) {
      // Turn OFF maintenance mode.
      $this->state->set('system.maintenance_mode', FALSE);

      // Clear end time and auto-disable flag.
      $config
        ->set('schedule.enabled', FALSE)
        ->set('schedule.automatic', FALSE)
        ->clear('schedule.end')
        ->save();
    }

    // Display message before start time using offset.
    $warning_text = $config->get('schedule.warning');

    // Proceed only if warning message is set and start time is valid.
    if (!empty($warning_text) && $start_time) {
      $offset = $config->get('schedule.offset') ?? [];

      $offset_value = $offset['value'] ?? 0;
      $offset_unit = $offset['unit'] ?? '';

      // Validate unit and value.
      if (
        is_numeric($offset_value) && $offset_value > 0 &&
        in_array($offset_unit, array_keys($this->maintenance->getAllowedUnits()), TRUE)
      ) {
        // Compute timestamp for warning.
        $warning_time = strtotime("-{$offset_value} {$offset_unit}", $start_time);

        // Show warning only if current time reached warning window.
        if ($now >= $warning_time && $now < $start_time) {
          $this->messenger->addWarning(
            $this->t('@message', ['@message' => Html::escape($warning_text)])
          );
        }
      }
    }
  }

  /**
   * {@inheritdoc}
   *
   * Subscribes to relevant maintenance-related kernel and system events.
   * Prioritization ensures this subscriber executes before Drupal Core's
   * default maintenance mode handling.
   *
   * @return array
   *   An array of event listener definitions.
   */
  public static function getSubscribedEvents(): array {
    $events = [];

    // Handle maintenance mode requests early.
    // This ensures our logic overrides the default behavior.
    $events[MaintenanceModeEvents::MAINTENANCE_MODE_REQUEST][] = [
      'onMaintenanceModeRequest', -999,
    ];

    // Add custom HTTP headers (e.g., `maintenance-mode: on`) to all responses.
    $events[KernelEvents::RESPONSE][] = [
      'onRespond', -99,
    ];

    // Customize HTTP response code for maintenance pages.
    $events[KernelEvents::RESPONSE][] = [
      'onKernelResponse', 36,
    ];

    // Check for redirect instructions before page builds.
    $events[KernelEvents::REQUEST][] = [
      'checkForRedirection', 45,
    ];

    // Check if maintenance should start based on a scheduled time.
    $events[KernelEvents::REQUEST][] = [
      'checkForScheduled', 54,
    ];

    return $events;
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc