sitemap_status-1.0.0-alpha4/src/SitemapStatus.php

src/SitemapStatus.php
<?php

namespace Drupal\sitemap_status;

use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Queue\QueueFactory;
use Drupal\Core\Queue\QueueWorkerManagerInterface;
use Drupal\Core\Queue\SuspendQueueException;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;

/**
 * Class SitemapStatus.
 */
class SitemapStatus implements SitemapStatusInterface {

  use StringTranslationTrait;

  /**
   * Drupal\Core\Config\ConfigFactoryInterface definition.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;

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

  /**
   * Drupal\Core\Queue\QueueFactory definition.
   *
   * @var \Drupal\Core\Queue\QueueFactory
   */
  protected $queueFactory;

  /**
   * Drupal\Core\Queue\QueueWorkerManagerInterface definition.
   *
   * @var \Drupal\Core\Queue\QueueWorkerManagerInterface
   */
  protected $pluginManagerQueueWorker;

  /**
   * Drupal\Core\Cache\CacheBackendInterface definition.
   *
   * @var \Drupal\Core\Cache\CacheBackendInterface
   */
  protected $cacheBackend;

  /**
   * @var bool
   */
  private $errorsCheck;

  /**
   * @var bool
   */
  private $elementsCheck;

  /**
   * @var string
   */
  private $basicAuth;

  /**
   * @var string
   */
  private $pathPrefix;

  /**
   * @var bool
   */
  private $async;

  /**
   * @var int
   */
  private $concurrentRequests;

  /**
   * @var bool
   */
  private $stopOnError;

  /**
   * Constructs a new SitemapStatus object.
   */
  public function __construct(
    ConfigFactoryInterface $config_factory,
    MessengerInterface $messenger,
    QueueFactory $queue_factory,
    QueueWorkerManagerInterface $plugin_manager_queue_worker,
    CacheBackendInterface $cache_backend
  ) {
    $this->configFactory = $config_factory;
    $this->messenger = $messenger;
    $this->queueFactory = $queue_factory;
    $this->pluginManagerQueueWorker = $plugin_manager_queue_worker;
    $this->cacheBackend = $cache_backend;

    // Set properties from the configuration.
    $config = $this->configFactory->get('sitemap_status.settings');
    $hasErrorCheck = $config->get('has_error_check');
    $hasElementCheck = $config->get('has_element_check');
    $pathPrefix = $config->get('path_prefix');
    $basicAuth = $config->get('basic_auth');
    $asyncRequests = $config->get('async');
    $stopOnError = $config->get('stop_on_error');

    $this->errorsCheck = $hasErrorCheck === 1;
    $this->elementsCheck = $hasElementCheck === 1;
    $this->stopOnError = $stopOnError === 1;

    if (!empty($basicAuth)) {
      $this->setBasicAuth($basicAuth);
    }
    if (!empty($pathPrefix)) {
      $this->setPathPrefix($pathPrefix);
    }

    if ($asyncRequests === 1) {
      $this->async = TRUE;
      $this->concurrentRequests = (int) $config->get('concurrent_requests') ?: SitemapStatusInterface::MAX_CONCURRENT_REQUESTS;
    }
  }

  /**
   * {@inheritdoc}
   */
  public function overridePropertiesWithState() {
    $configurationOverride = \Drupal::state()->get(SitemapStatusInterface::STATE_CONFIG_OVERRIDE);
    if (!empty($configurationOverride)) {
      if (array_key_exists('has_error_check', $configurationOverride)) {
        $this->errorsCheck = $configurationOverride['has_error_check'];
      }
      if (array_key_exists('has_element_check', $configurationOverride)) {
        $this->elementsCheck = $configurationOverride['has_element_check'];
      }
      if (array_key_exists('basic_auth', $configurationOverride)) {
        $this->setBasicAuth($configurationOverride['basic_auth']);
      }
      if (array_key_exists('path_prefix', $configurationOverride)) {
        $this->setPathPrefix($configurationOverride['path_prefix']);
      }
      if (array_key_exists('async', $configurationOverride)) {
        $this->async = $configurationOverride['async'];
      }
      if (array_key_exists('concurrent_requests', $configurationOverride)) {
        $this->concurrentRequests = (int) $configurationOverride['concurrent_requests'];
      }
      if (array_key_exists('stop_on_error', $configurationOverride)) {
        $this->stopOnError = $configurationOverride['stop_on_error'];
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function checkLocations() {
    $sitemapLocations = $this->fetchSitemapLocations();

    if (!$this->async) {
      $locationsStatuses = $this->fetchLocationsByStatusSync($sitemapLocations);
    }
    else {
      $locationsStatuses = $this->fetchLocationsByStatusAsync($sitemapLocations, $this->concurrentRequests);
    }

    $groupedLocations = [];
    if (is_array($locationsStatuses)) {
      $groupedLocations = $this->groupLocationsByStatus($locationsStatuses);
      if (!empty($groupedLocations)) {
        \Drupal::logger('sitemap_status')->info($this->t('Sitemap status check has been completed.'));
      }
      else {
        \Drupal::logger('sitemap_status')->warning($this->t('Sitemap status check has not been completed.'));
      }
      // As a side effect, store in the cache.
      $this->cacheBackend->set(SitemapStatusInterface::CACHE_ID, $groupedLocations);
    }

    // @todo make sure to delete the state override from Drush when the queue has finished
    //   the processing. Currently deleting the state before starting a cron or ui check as a workaround.
    //   This might probably require an event subscriber, e.g. onQueueFinishedProcessing.
    return $groupedLocations;
  }

  /**
   * {@inheritdoc}
   */
  public function setBasicAuth($basic_auth) {
    if (!empty($basic_auth)) {
      $basicAuthParts = explode(':', $basic_auth);
      if (count($basicAuthParts) !== 2) {
        $this->messenger->addError(
          $this->t('The basic auth must be with the format <em>username:password</em>. It has been ignored.')
        );
      }
      else {
        $this->basicAuth = $basic_auth;
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function setPathPrefix($path_prefix) {
    if (!empty($path_prefix)) {
      $currentHost = \Drupal::request()->getSchemeAndHttpHost();
      $environmentUrl = $currentHost . '/' . $path_prefix;
      if (!$this->isUrlValid($environmentUrl)) {
        $this->messenger->addError(
          $this->t('The resulting url with the path prefix (@environment_url) is not valid. It has been ignored.', [
            '@environment_url' => $environmentUrl,
          ])
        );
      }
      else {
        $this->pathPrefix = $path_prefix;
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getLocationsStatuses() {
    $result = [];
    if (\Drupal::currentUser()->hasPermission('access sitemap status')) {
      if ($cache = $this->cacheBackend->get(SitemapStatusInterface::CACHE_ID)) {
        $result = $cache->data;
      }
    }
    return $result;
  }

  /**
   * {@inheritdoc}
   */
  public function getStatusDescription(int $status_code) {
    $result = $this->t('Unknown');
    switch ($status_code) {
      case 100: $result = $this->t('Continue');
        break;

      case 101: $result = $this->t('Switching Protocols');
        break;

      case 200: $result = $this->t('OK');
        break;

      case 201: $result = $this->t('Created');
        break;

      case 202: $result = $this->t('Accepted');
        break;

      case 203: $result = $this->t('Non-Authoritative Information');
        break;

      case 204: $result = $this->t('No Content');
        break;

      case 205: $result = $this->t('Reset Content');
        break;

      case 206: $result = $this->t('Partial Content');
        break;

      case 300: $result = $this->t('Multiple Choices');
        break;

      case 301: $result = $this->t('Moved Permanently');
        break;

      case 302: $result = $this->t('Moved Temporarily');
        break;

      case 303: $result = $this->t('See Other');
        break;

      case 304: $result = $this->t('Not Modified');
        break;

      case 305: $result = $this->t('Use Proxy');
        break;

      case 400: $result = $this->t('Bad Request');
        break;

      case 401: $result = $this->t('Unauthorized');
        break;

      case 402: $result = $this->t('Payment Required');
        break;

      case 403: $result = $this->t('Forbidden');
        break;

      case 404: $result = $this->t('Not Found');
        break;

      case 405: $result = $this->t('Method Not Allowed');
        break;

      case 406: $result = $this->t('Not Acceptable');
        break;

      case 407: $result = $this->t('Proxy Authentication Required');
        break;

      case 408: $result = $this->t('Request Time-out');
        break;

      case 409: $result = $this->t('Conflict');
        break;

      case 410: $result = $this->t('Gone');
        break;

      case 411: $result = $this->t('Length Required');
        break;

      case 412: $result = $this->t('Precondition Failed');
        break;

      case 413: $result = $this->t('Request Entity Too Large');
        break;

      case 414: $result = $this->t('Request-URI Too Large');
        break;

      case 415: $result = $this->t('Unsupported Media Type');
        break;

      case 500: $result = $this->t('Internal Server Error');
        break;

      case 501: $result = $this->t('Not Implemented');
        break;

      case 502: $result = $this->t('Bad Gateway');
        break;

      case 503: $result = $this->t('Service Unavailable');
        break;

      case 504: $result = $this->t('Gateway Time-out');
        break;

      case 505: $result = $this->t('HTTP Version not supported');
        break;

      // Custom status code for DOM checks.
      case 901: $result = $this->t('DOM Error');
        break;

      case 902: $result = $this->t('DOM GraphQL Twig error');
        break;

      case 903: $result = $this->t('DOM Missing element');
        break;
    }
    return $result;
  }

  /**
   * {@inheritdoc}
   */
  public function getLocationsStatusesByGroup() {
    $result = [];
    $locationsStatuses = $this->getLocationsStatuses();
    $statusGroups = [
      SitemapStatusInterface::STATUS_NOT_REACHED,
      SitemapStatusInterface::STATUS_DOM_ERROR,
      SitemapStatusInterface::STATUS_2xx,
      SitemapStatusInterface::STATUS_3xx,
      SitemapStatusInterface::STATUS_4xx,
      SitemapStatusInterface::STATUS_5xx,
    ];
    foreach ($locationsStatuses as $httpsStatus => $locations) {
      foreach ($statusGroups as $httpStatusGroup) {
        if ($this->startsWith((string) $httpsStatus, $httpStatusGroup)) {
          if (!array_key_exists($httpStatusGroup, $result)) {
            $result[$httpStatusGroup] = [];
          }
          $result[$httpStatusGroup] += $locations;
        }
      }
    }
    return $result;
  }

  /**
   * {@inheritdoc}
   */
  public function getStatusLevel() {
    $statusGroups = $this->getLocationsStatusesByGroup();
    if (
      array_key_exists(SitemapStatusInterface::STATUS_5xx, $statusGroups) &&
      !empty($statusGroups[SitemapStatusInterface::STATUS_5xx])
    ) {
      return SitemapStatusInterface::REQUIREMENT_ERROR;
    }
    if (
      array_key_exists(SitemapStatusInterface::STATUS_DOM_ERROR, $statusGroups) &&
      !empty($statusGroups[SitemapStatusInterface::STATUS_DOM_ERROR])
    ) {
      return SitemapStatusInterface::REQUIREMENT_ERROR;
    }
    if (
      array_key_exists(SitemapStatusInterface::STATUS_4xx, $statusGroups) &&
      !empty($statusGroups[SitemapStatusInterface::STATUS_4xx])
    ) {
      return SitemapStatusInterface::REQUIREMENT_WARNING;
    }
    if (
      array_key_exists(SitemapStatusInterface::STATUS_NOT_REACHED, $statusGroups) &&
      !empty($statusGroups[SitemapStatusInterface::STATUS_NOT_REACHED])
    ) {
      return SitemapStatusInterface::REQUIREMENT_INFO;
    }
    if (
      array_key_exists(SitemapStatusInterface::STATUS_3xx, $statusGroups) &&
      !empty($statusGroups[SitemapStatusInterface::STATUS_3xx])
    ) {
      return SitemapStatusInterface::REQUIREMENT_OK;
    }
    if (
      array_key_exists(SitemapStatusInterface::STATUS_2xx, $statusGroups) &&
      !empty($statusGroups[SitemapStatusInterface::STATUS_2xx])
    ) {
      return SitemapStatusInterface::REQUIREMENT_OK;
    }
    return SitemapStatus::REQUIREMENT_UNKNOWN;
  }

  /**
   * {@inheritdoc}
   */
  public function getStatusLevelIcon() {
    $result = '';
    $statusLevel = $this->getStatusLevel();
    switch ($statusLevel) {
      case SitemapStatus::REQUIREMENT_ERROR:
        $result = '❌';
        break;

      case SitemapStatus::REQUIREMENT_UNKNOWN:
        $result = '❓';
        break;

      case SitemapStatus::REQUIREMENT_WARNING:
        $result = '⚠️';
        break;

      case SitemapStatus::REQUIREMENT_INFO:
        $result = 'ℹ️';
        break;

      case SitemapStatus::REQUIREMENT_OK:
        $result = '✅';
        break;
    }
    return $result;
  }

  /**
   * {@inheritdoc}
   */
  public function hasLocationStatusError(int $status) {
    return $status >= 500;
  }

  /**
   * {@inheritdoc}
   */
  public function stopOnError() {
    return $this->stopOnError;
  }

  /**
   * {@inheritdoc}
   */
  public function getStatusSummary() {
    if (empty($this->getLocationsStatuses())) {
      $build = [
        '#type' => 'inline_template',
        '#template' => '<p>{{ message }}</p>',
        '#context' => [
          // @todo add call to action to run the status check.
          'message' => $this->t('No sitemap status yet.'),
        ],
      ];
    }
    else {
      $groupedLocations = $this->getLocationsStatusesByGroup();
      $listItems = [];
      foreach ($groupedLocations as $locationGroup => $locations) {
        $listItems[] = $this->t('Locations with status @group_status: @locations_count', [
          '@group_status' => $locationGroup . 'xx',
          '@locations_count' => count($locations),
        ]);
      }
      $build = [
        'summary' => [
          '#type' => 'inline_template',
          '#template' => '<p>{{ message }}</p> {{ groups }}',
          '#context' => [
            'message' => $this->t('Sitemap status summary.'),
            'groups' => [
              '#theme' => 'item_list',
              '#list_type' => 'ul',
              '#items' => $listItems,
            ],
          ],
        ],
        'link' => [
          '#type' => 'link',
          '#title' => $this->t('Details'),
          '#url' => Url::fromRoute('sitemap_status.details'),
        ],
      ];
    }
    return $build;
  }

  /**
   * {@inheritdoc}
   */
  public function isUrlValid($url) {
    // @todo delegate to Drupal url validator.
    $result = TRUE;
    if (!$url || !is_string($url) || !preg_match('/^http(s)?:\/\/[a-z0-9-]+(.[a-z0-9-]+)*(:[0-9]+)?(\/.*)?$/i', $url)) {
      $result = FALSE;
    }
    return $result;
  }

  /**
   * Checks if a string starts with another one.
   *
   * @param string $haystack
   * @param string $needle
   *
   * @return bool
   */
  private function startsWith($haystack, $needle) {
    $length = strlen($needle);
    return (substr($haystack, 0, $length) === $needle);
  }

  /**
   * Sets the default curl options.
   *
   * @param resource $curl_handle
   */
  private function setDefaultCurlOptions(&$curl_handle) {
    curl_setopt($curl_handle, CURLOPT_RETURNTRANSFER, TRUE);
    curl_setopt($curl_handle, CURLOPT_SSL_VERIFYHOST, FALSE);
    curl_setopt($curl_handle, CURLOPT_SSL_VERIFYPEER, FALSE);
    curl_setopt($curl_handle, CURLOPT_CONNECTTIMEOUT, 5);
    curl_setopt($curl_handle, CURLOPT_TIMEOUT, 20);
    if (!empty($this->basicAuth)) {
      curl_setopt($curl_handle, CURLOPT_USERPWD, $this->basicAuth);
    }
    // @todo add option to follow redirect
    // curl_setopt($curlHandle, CURLOPT_FOLLOWLOCATION, TRUE);.
  }

  /**
   * {@inheritdoc}
   */
  public function fetchSitemapLocations() {
    $result = [];
    $environmentUrl = \Drupal::request()->getSchemeAndHttpHost();
    if (!empty($this->pathPrefix)) {
      $environmentUrl .= '/' . $this->pathPrefix;
    }
    $simpleSitemapConfig = $this->configFactory->get('simple_sitemap.settings');
    $simpleSitemapBaseUrl = $simpleSitemapConfig->get('base_url');

    $curlHandle = curl_init();
    $this->setDefaultCurlOptions($curlHandle);
    curl_setopt($curlHandle, CURLOPT_URL, $environmentUrl . '/sitemap.xml');
    $sitemap = curl_exec($curlHandle);
    $httpStatus = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE);
    curl_close($curlHandle);
    if ((int) $httpStatus === 200) {
      $xmlElement = new \SimpleXMLElement($sitemap);
      foreach ($xmlElement->url as $urlElement) {
        $location = (string) $urlElement->loc;
        // Replace the sitemap url for this environment if necessary:
        // if the environment url does not match the simple sitemap base url
        // or if a path prefix has been added.
        if ($simpleSitemapBaseUrl !== $environmentUrl) {
          $location = str_replace($simpleSitemapBaseUrl, $environmentUrl, $location);
        }
        $result[] = $location;
      }
    }
    else {
      $this->messenger->addError($this->t('The sitemap returned a status of @status. Check if it has already been generated (<code>drush ssg</code>) or if the environment is accessible.', [
        '@status' => $httpStatus,
      ]));
    }

    $config = $this->configFactory->get('sitemap_status.settings');
    $maximumLocations = (int) $config->get('maximum_locations');
    if ($maximumLocations > 0 && count($result) > $maximumLocations) {
      $result = array_slice($result, 0, $maximumLocations);
    }
    return $result;
  }

  /**
   * Fetches the sitemap locations status synchronously.
   *
   * @param string[] $sitemap_locations
   *
   * @return int[]
   *   Locations grouped by http status code.
   */
  private function fetchLocationsByStatusSync(array $sitemap_locations) {
    $result = [];
    $queueId = SitemapStatusInterface::LOCATION;
    /** @var \Drupal\Core\Queue\QueueInterface $queue */
    $queue = $this->queueFactory->get($queueId);
    // Remove previous queue items if any.
    $queue->deleteQueue();
    foreach ($sitemap_locations as $location) {
      $queue->createItem($location);
    }
    $result = $this->processQueue($queueId);
    return $result;
  }

  /**
   * Fetches the sitemap locations status asynchronously.
   *
   * @param string[] $sitemap_locations
   * @param int|null $concurrent_requests
   *
   * @return int[]
   *   Locations grouped by http status code.
   */
  private function fetchLocationsByStatusAsync(array $sitemap_locations, $concurrent_requests) {
    $result = [];
    $concurrentRequests = $concurrent_requests;
    if (empty($concurrentRequests)) {
      $concurrentRequests = SitemapStatusInterface::MAX_CONCURRENT_REQUESTS;
    }
    $locationsGroups = array_chunk($sitemap_locations, $concurrentRequests);
    $queueId = SitemapStatusInterface::LOCATIONS_GROUP;
    /** @var \Drupal\Core\Queue\QueueInterface $queue */
    $queue = $this->queueFactory->get($queueId);
    // Remove previous queue items if any.
    $queue->deleteQueue();
    foreach ($locationsGroups as $locationsGroup) {
      $queue->createItem($locationsGroup);
    }
    $result = $this->processQueue($queueId);
    return $result;
  }

  /**
   * {@inheritdoc}
   */
  public function groupLocationsByStatus(array $locations) {
    $result = [];
    foreach ($locations as $location => $httpStatus) {
      if (!array_key_exists($httpStatus, $result)) {
        $result[$httpStatus] = [];
      }
      $result[$httpStatus][] = $location;
    }
    return $result;
  }

  /**
   * Processes queue items.
   *
   * @param string $queue_id
   *
   * @return array
   *
   * @throws \Drupal\Component\Plugin\Exception\PluginException
   */
  private function processQueue($queue_id) {
    $result = [];
    /** @var \Drupal\Core\Queue\QueueInterface $queue */
    $queue = $this->queueFactory->get($queue_id);
    switch ($queue_id) {
      case SitemapStatusInterface::LOCATIONS_GROUP:
        /** @var \Drupal\Core\Queue\QueueWorkerInterface $queueWorker */
        $queueWorker = $this->pluginManagerQueueWorker->createInstance('locations_group');
        break;

      case SitemapStatusInterface::LOCATION:
        /** @var \Drupal\Core\Queue\QueueWorkerInterface $queueWorker */
        $queueWorker = $this->pluginManagerQueueWorker->createInstance('location');
        break;
    }

    $hasError = FALSE;
    while ($item = $queue->claimItem()) {
      try {
        $processedLocations = $queueWorker->processItem($item->data);
        $result += $processedLocations;
        $queue->deleteItem($item);
        if ($this->stopOnError()) {
          // Single location check.
          if (count($processedLocations) === 1) {
            $locationResult = reset($processedLocations);
            if ($this->hasLocationStatusError($locationResult)) {
              $hasError = TRUE;
              $this->messenger->addError($this->t('The sitemap status check stopped on the first error met.'));
              break;
            }
          }
          // Grouped (async) locations check.
          else {
            foreach ($processedLocations as $locationStatus) {
              if ($this->hasLocationStatusError($locationStatus)) {
                $hasError = TRUE;
                $this->messenger->addError($this->t('The sitemap status check stopped on the first error met in the locations group.'));
                break;
              }
            }
            if ($hasError) {
              break;
            }
          }
        }

        if ($hasError) {
          $queue->deleteQueue();
        }
      }
      catch (SuspendQueueException $exception) {
        $this->messenger->addError($exception->getMessage());
        $queue->releaseItem($item);
        break;
      }
      catch (\Exception $exception) {
        $this->messenger->addError($exception->getMessage());
      }
    }
    return $result;
  }

  /**
   * {@inheritdoc}
   */
  public function fetchLocationStatus($location) {
    $curlHandle = curl_init();
    $noBody = !$this->errorsCheck && !$this->elementsCheck;
    $this->setDefaultCurlOptions($curlHandle);
    curl_setopt($curlHandle, CURLOPT_HEADER, TRUE);
    curl_setopt($curlHandle, CURLOPT_NOBODY, $noBody);
    curl_setopt($curlHandle, CURLOPT_URL, $location);
    $curlResponse = curl_exec($curlHandle);
    if ($curlResponse) {
      $httpStatus = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE);
      $status = $this->getStatus($httpStatus, $noBody, $curlHandle, $curlResponse);
      $this->printLocationErrors($location, $status, $curlResponse);
    }
    else {
      // Not reachable.
      $status = (int) SitemapStatusInterface::STATUS_NOT_REACHED;
    }
    curl_close($curlHandle);
    return $status;
  }

  /**
   * {@inheritdoc}
   */
  public function fetchLocationStatusMultiple(array $locations) {
    $result = [];
    $noBody = !$this->errorsCheck && !$this->elementsCheck;
    $locationsCount = count($locations);
    $curlHandles = [];
    $curlMultipleHandle = curl_multi_init();
    for ($i = 0; $i < $locationsCount; $i++) {
      $location = $locations[$i];
      $curlHandles[$i] = curl_init($location);
      $this->setDefaultCurlOptions($curlHandles[$i]);
      curl_setopt($curlHandles[$i], CURLOPT_HEADER, TRUE);
      curl_setopt($curlHandles[$i], CURLOPT_NOBODY, $noBody);
      curl_multi_add_handle($curlMultipleHandle, $curlHandles[$i]);
    }

    do {
      $status = curl_multi_exec($curlMultipleHandle, $running);
      if ($running) {
        curl_multi_select($curlMultipleHandle);
      }
    } while ($running && $status === CURLM_OK);

    for ($i = 0; $i < $locationsCount; $i++) {
      $httpStatus = curl_getinfo($curlHandles[$i], CURLINFO_HTTP_CODE);
      // Prevent to get response if not necessary.
      // @todo review as it introduces a side effect for getStatus().
      $curlResponse = '';
      if ($noBody === FALSE) {
        $curlResponse = curl_multi_getcontent($curlHandles[$i]);
      }
      $status = $this->getStatus($httpStatus, $noBody, $curlHandles[$i], $curlResponse);
      $result[$locations[$i]] = $status;
      // @todo implement specific case for curl status if we want to print headers.
      $this->printLocationErrors($locations[$i], $httpStatus);
      curl_multi_remove_handle($curlMultipleHandle, $curlHandles[$i]);
    }
    curl_multi_close($curlMultipleHandle);
    return $result;
  }

  /**
   * Returns the http status or the custom DOM status.
   *
   * @param int $http_status
   * @param bool $no_body
   * @param resource|false $curl_handle
   * @param string $curl_response
   *
   * @return int
   *   The http status or the custom DOM status.
   */
  private function getStatus(int $http_status, bool $no_body, $curl_handle, string $curl_response) {
    // @todo logic needs refactoring as the nested conditions are
    //   becoming way too complex to maintain.
    $status = $http_status;
    // Only check the body if required and if the status has no server error.
    if (!$no_body && $http_status < 500) {
      $headerSize = curl_getinfo($curl_handle, CURLINFO_HEADER_SIZE);
      $body = substr($curl_response, $headerSize);
      if (empty($body)) {
        // Custom DOM error status code.
        // @todo this error could be specialized as an empty body means
        //   that there is a more significant error.
        $status = 901;
      }
      else {
        $document = new \DOMDocument();
        // Possibly move this outside of this call, before/after the iteration.
        $libXmlPreviousState = libxml_use_internal_errors(TRUE);
        $document->loadHTML($body);
        libxml_clear_errors();
        libxml_use_internal_errors($libXmlPreviousState);
        $xpath = new \DOMXPath($document);
        if ($this->errorsCheck) {
          $errorClasses = $this->getMultilineConfig('error_classes');
          if (empty($errorClasses)) {
            $errorClasses = ['messages--error'];
          }
          $expressionParts = [];
          foreach ($errorClasses as $errorClass) {
            $expressionParts[] = "contains(@class, '" . $errorClass . "')";
          }
          // "//div[contains(@class, 'messages--error') or contains(@class, 'form-item--error-message')]".
          $expression = "//div[" . implode(" or ", $expressionParts) . "]";
          $errorNodes = $xpath->query($expression);
          if ($errorNodes->count() > 0) {
            // Custom DOM error status code.
            $status = 901;
          }
          // Further check if GraphQL Twig is installed.
          // Error element is ul and not div,
          // so using a specific expression to not use a wildcard (//*[])
          elseif (\Drupal::moduleHandler()->moduleExists('graphql_twig')) {
            $expression = "//ul[contains(@class, 'graphql-twig-errors')]";
            $errorNodes = $xpath->query($expression);
            if ($errorNodes->count() > 0) {
              // Custom DOM GraphQL error status code.
              $status = 902;
            }
          }
        }

        if ($status !== SitemapStatusInterface::STATUS_DOM_ERROR && $this->elementsCheck) {
          $elements = $this->getMultilineConfig('elements');
          if (empty($elements)) {
            $elements = ['h1'];
          }
          foreach ($elements as $element) {
            $elementNodes = $document->getElementsByTagName($element);
            if ($elementNodes->count() === 0) {
              // Custom DOM missing element status code.
              $status = 903;
              break;
            }
          }
        }
      }
    }
    return $status;
  }

  /**
   * Returns the multiline config from a textarea as an array.
   *
   * @param string $config_key
   *
   * @return array
   */
  private function getMultilineConfig(string $config_key):array {
    $config = $this->configFactory->get('sitemap_status.settings');
    $lines = explode("\n", $config->get($config_key));
    // Filter empty lines.
    $lines = array_filter($lines, function ($value) {
      return !is_null($value) && $value !== '';
    });
    // Remove whitespaces.
    $result = array_map(function ($value) {
      return preg_replace('/\s+/', '', $value);
    }, $lines);
    return $result;
  }

  /**
   * Prints a curl_exec result for a location.
   *
   * @param string $location
   * @param int $status
   * @param string $curl_response
   */
  private function printLocationErrors(string $location, int $status, string $curl_response = NULL):void {
    if (
      $status === (int) SitemapStatusInterface::STATUS_NOT_REACHED ||
      $status >= 400
    ) {
      $this->messenger->addError($this->t('Location @location: status @status.', [
        '@location' => $location,
        '@status' => $status . ' [' . $this->getStatusDescription($status) . ']',
      ]));
    }
  }

}

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

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