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) . ']',
]));
}
}
}
