BatchUrlGenerator.php 13.3 KB
Newer Older
1 2 3 4 5 6 7 8 9 10
<?php

namespace Drupal\simple_sitemap\Batch;

use Drupal\Core\Url;
use Drupal\Component\Utility\Html;
use Drupal\Core\Cache\Cache;
use Drupal\Core\StringTranslation\StringTranslationTrait;

/**
Pawel G's avatar
Pawel G committed
11 12
 * Class BatchUrlGenerator.
 *
13 14 15 16 17 18 19
 * @package Drupal\simple_sitemap\Batch
 */
class BatchUrlGenerator {

  use StringTranslationTrait;

  const ANONYMOUS_USER_ID = 0;
20 21
  const PATH_DOES_NOT_EXIST_OR_NO_ACCESS_MESSAGE = "The path @path has been omitted from the XML sitemap as it either does not exist, or it is not accessible to anonymous users.";
  const PROCESSING_PATH_MESSAGE = 'Processing path #@current out of @max: @path';
Pawel G's avatar
Pawel G committed
22
  const REGENERATION_FINISHED_MESSAGE = "The <a href='@url' target='_blank'>XML sitemap</a> has been regenerated for all languages.";
23 24

  protected $sitemapGenerator;
25
  protected $languageManager;
26
  protected $languages;
27
  protected $defaultLanguageId;
28 29 30
  protected $entityTypeManager;
  protected $pathValidator;
  protected $entityQuery;
Pawel G's avatar
Pawel G committed
31
  protected $logger;
32 33
  protected $anonUser;

34 35 36
  protected $context;
  protected $batchInfo;

37 38
  /**
   * BatchUrlGenerator constructor.
Pawel G's avatar
Pawel G committed
39
   *
40 41 42 43 44
   * @param $sitemap_generator
   * @param $language_manager
   * @param $entity_type_manager
   * @param $path_validator
   * @param $entity_query
Pawel G's avatar
Pawel G committed
45
   * @param $logger
46
   */
47 48 49 50 51
  public function __construct(
    $sitemap_generator,
    $language_manager,
    $entity_type_manager,
    $path_validator,
Pawel G's avatar
Pawel G committed
52 53
    $entity_query,
    $logger
54
  ) {
Pawel G's avatar
Pawel G committed
55 56
    // Todo using only one method, maybe make method static instead?
    $this->sitemapGenerator = $sitemap_generator;
57
    $this->languageManager = $language_manager;
58
    $this->languages = $language_manager->getLanguages();
59
    $this->defaultLanguageId = $language_manager->getDefaultLanguage()->getId();
60 61 62
    $this->entityTypeManager = $entity_type_manager;
    $this->pathValidator = $path_validator;
    $this->entityQuery = $entity_query;
Pawel G's avatar
Pawel G committed
63
    $this->logger = $logger;
64 65 66
    $this->anonUser = $this->entityTypeManager->getStorage('user')->load(self::ANONYMOUS_USER_ID);
  }

67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
  /**
   * The Drupal batch API can only call procedural functions or static methods.
   * To circumvent exclusively procedural code, on every batch iteration this
   * static method is called by the batch API and returns a freshly created
   * Drupal service object of this class. All following calls can be made on
   * the returned service the OOP way. This is is obviously trading performance
   * for cleanness. The service is created within its own class to improve
   * testability.
   *
   * @return object
   *   Symfony service object of this class
   */
  public static function service() {
    return \Drupal::service('simple_sitemap.batch_url_generator');
  }

83 84 85 86 87 88 89 90 91
  /**
   * @param $context
   * @return $this
   */
  public function setContext(&$context) {
    $this->context = &$context;
    return $this;
  }

92 93
  /**
   * @param $batch_info
94
   * @return $this
95
   */
96 97 98
  public function setBatchInfo($batch_info) {
    $this->batchInfo = $batch_info;
    return $this;
99 100 101 102 103
  }

  /**
   * @return bool
   */
104 105
  protected function isBatch() {
    return $this->batchInfo['from'] != 'nobatch';
106 107 108 109 110 111
  }

  /**
   * @param $path
   * @return bool
   */
112 113
  protected function pathProcessed($path) {
    $path_pool = isset($this->context['results']['processed_paths']) ? $this->context['results']['processed_paths'] : [];
114 115 116
    if (in_array($path, $path_pool)) {
      return TRUE;
    }
117
    $this->context['results']['processed_paths'][] = $path;
118 119 120 121 122 123
    return FALSE;
  }

  /**
   * @param $max
   */
124 125 126 127 128 129 130 131 132 133
  protected function initializeBatch($max) {
    if ($this->needsInitialization()) {
      $this->context['results']['generate'] = !empty($this->context['results']['generate']) ? $this->context['results']['generate'] : [];
      if ($this->isBatch()) {
        $this->context['sandbox']['progress'] = 0;
        $this->context['sandbox']['current_id'] = 0;
        $this->context['sandbox']['max'] = $max;
        $this->context['results']['processed_paths'] = !empty($this->context['results']['processed_paths'])
          ? $this->context['results']['processed_paths'] : [];
      }
134 135 136 137
    }
  }

  /**
138
   * @return bool
139
   */
140 141
  protected function needsInitialization() {
    return empty($this->context['sandbox']);
142 143 144
  }

  /**
145
   * @param $id
146
   */
147 148 149 150 151 152 153
  protected function setCurrentId($id) {
    if ($this->isBatch()) {
      $this->context['sandbox']['progress']++;
      $this->context['sandbox']['current_id'] = $id;
    }
  }

Pawel G's avatar
Pawel G committed
154 155 156
  /**
   *
   */
157 158 159
  protected function processSegment() {
    if ($this->isBatch()) {
      $this->setProgressInfo();
Pawel G's avatar
Pawel G committed
160
    }
161 162
    if (!empty($this->batchInfo['max_links']) && count($this->context['results']['generate']) >= $this->batchInfo['max_links']) {
      $chunks = array_chunk($this->context['results']['generate'], $this->batchInfo['max_links']);
163
      foreach ($chunks as $i => $chunk_links) {
164 165
        if (count($chunk_links) == $this->batchInfo['max_links']) {
          $remove_sitemap = empty($this->context['results']['chunk_count']);
166
          $this->sitemapGenerator->generateSitemap($chunk_links, $remove_sitemap);
167 168 169
          $this->context['results']['chunk_count'] = !isset($this->context['results']['chunk_count'])
            ? 1 : $this->context['results']['chunk_count'] + 1;
          $this->context['results']['generate'] = array_slice($this->context['results']['generate'], count($chunk_links));
170 171 172 173 174
        }
      }
    }
  }

Pawel G's avatar
Pawel G committed
175 176 177
  /**
   *
   */
178 179
  protected function setProgressInfo() {
    if ($this->context['sandbox']['progress'] != $this->context['sandbox']['max']) {
Pawel G's avatar
Pawel G committed
180
      // Providing progress info to the batch API.
181
      $this->context['finished'] = $this->context['sandbox']['progress'] / $this->context['sandbox']['max'];
Pawel G's avatar
Pawel G committed
182
      // Adding processing message after finishing every batch segment.
183 184 185 186 187 188 189
      end($this->context['results']['generate']);
      $last_key = key($this->context['results']['generate']);
      if (!empty($this->context['results']['generate'][$last_key]['path'])) {
        $this->context['message'] = $this->t(self::PROCESSING_PATH_MESSAGE, [
          '@current' => $this->context['sandbox']['progress'],
          '@max' => $this->context['sandbox']['max'],
          '@path' => HTML::escape($this->context['results']['generate'][$last_key]['path']),
Pawel G's avatar
Pawel G committed
190 191 192 193
        ]);
      }
    }
  }
194 195 196 197 198 199

  /**
   * Batch callback function which generates urls to entity paths.
   *
   * @param array $entity_info
   */
200
  public function generateBundleUrls($entity_info) {
201

202
    $query = $this->entityQuery->get($entity_info['entity_type_name']);
Pawel G's avatar
Pawel G committed
203
    if (!empty($entity_info['keys']['id'])) {
204
      $query->sort($entity_info['keys']['id'], 'ASC');
Pawel G's avatar
Pawel G committed
205 206
    }
    if (!empty($entity_info['keys']['bundle'])) {
207
      $query->condition($entity_info['keys']['bundle'], $entity_info['bundle_name']);
Pawel G's avatar
Pawel G committed
208 209
    }
    if (!empty($entity_info['keys']['status'])) {
210
      $query->condition($entity_info['keys']['status'], 1);
Pawel G's avatar
Pawel G committed
211
    }
212

213 214
    $count_query = clone $query;
    $this->initializeBatch($count_query->count()->execute());
215 216

    // Creating a query limited to n=batch_process_limit entries.
217 218
    if ($this->isBatch()) {
      $query->range($this->context['sandbox']['progress'], $this->batchInfo['batch_process_limit']);
219 220 221 222 223 224
    }

    $results = $query->execute();
    if (!empty($results)) {
      $entities = $this->entityTypeManager->getStorage($entity_info['entity_type_name'])->loadMultiple($results);

Pawel G's avatar
Pawel G committed
225
      foreach ($entities as $entity_id => $entity) {
226
        $this->setCurrentId($entity_id);
227
        $priority = NULL;
228 229

        // Overriding entity settings if it has been overridden on entity edit page...
230
        if (isset($this->batchInfo['entity_types'][$entity_info['entity_type_name']][$entity_info['bundle_name']]['entities'][$entity_id]['index'])) {
231 232

          // Skipping entity if it has been excluded on entity edit page.
233
          if (!$this->batchInfo['entity_types'][$entity_info['entity_type_name']][$entity_info['bundle_name']]['entities'][$entity_id]['index']) {
234 235 236
            continue;
          }
          // Otherwise overriding priority settings for this entity.
237
          $priority = $this->batchInfo['entity_types'][$entity_info['entity_type_name']][$entity_info['bundle_name']]['entities'][$entity_id]['priority'];
238 239 240
        }

        switch ($entity_info['entity_type_name']) {
Pawel G's avatar
Pawel G committed
241 242 243
          // Loading url object for menu links.
          case 'menu_link_content':
            if (!$entity->isEnabled()) {
244
              continue;
Pawel G's avatar
Pawel G committed
245
            }
246 247
            $url_object = $entity->getUrlObject();
            break;
Pawel G's avatar
Pawel G committed
248 249 250 251 252

          // Loading url object for other entities.
          default:
            // todo: file entity type does not have a canonical url and breaks generation, hopefully fixed in https://www.drupal.org/node/2402533
            $url_object = $entity->toUrl();
253 254 255
        }

        // Do not include external paths.
Pawel G's avatar
Pawel G committed
256
        if (!$url_object->isRouted()) {
257
          continue;
Pawel G's avatar
Pawel G committed
258
        }
259 260

        // Do not include paths inaccessible to anonymous users.
Pawel G's avatar
Pawel G committed
261
        if (!$url_object->access($this->anonUser)) {
262
          continue;
Pawel G's avatar
Pawel G committed
263
        }
264 265 266

        // Do not include paths that have been already indexed.
        $path = $url_object->getInternalPath();
Pawel G's avatar
Pawel G committed
267
        if ($this->batchInfo['remove_duplicates'] && $this->pathProcessed($path)) {
268
          continue;
Pawel G's avatar
Pawel G committed
269
        }
270 271 272 273 274 275 276 277 278

        $url_object->setOption('absolute', TRUE);

        $path_data = [
          'path' => $path,
          'entity_info' => ['entity_type' => $entity_info['entity_type_name'], 'id' => $entity_id],
          'lastmod' => method_exists($entity, 'getChangedTime') ? date_iso8601($entity->getChangedTime()) : NULL,
          'priority' => isset($priority) ? $priority : (isset($entity_info['bundle_settings']['priority']) ? $entity_info['bundle_settings']['priority'] : NULL),
        ];
279
        $this->addUrlVariants($url_object, $path_data, $entity);
280 281
      }
    }
282
    $this->processSegment();
283 284 285 286 287 288 289
  }

  /**
   * Batch function which generates urls to custom paths.
   *
   * @param array $custom_paths
   */
290
  public function generateCustomUrls($custom_paths) {
291

292
    $this->initializeBatch(count($custom_paths));
293

Pawel G's avatar
Pawel G committed
294
    foreach ($custom_paths as $i => $custom_path) {
295
      $this->setCurrentId($i);
296

Pawel G's avatar
Pawel G committed
297 298
      // todo: Change to different function, as this also checks if current user has access. The user however varies depending if process was started from the web interface or via cron/drush. Use getUrlIfValidWithoutAccessCheck()?
      if (!$this->pathValidator->isValid($custom_path['path'])) {
299
        $this->logger->registerError([self::PATH_DOES_NOT_EXIST_OR_NO_ACCESS_MESSAGE, ['@path' => $custom_path['path']]], 'warning');
300 301 302 303
        continue;
      }
      $url_object = Url::fromUserInput($custom_path['path'], ['absolute' => TRUE]);

Pawel G's avatar
Pawel G committed
304
      if (!$url_object->access($this->anonUser)) {
305
        continue;
Pawel G's avatar
Pawel G committed
306
      }
307 308

      $path = $url_object->getInternalPath();
Pawel G's avatar
Pawel G committed
309
      if ($this->batchInfo['remove_duplicates'] && $this->pathProcessed($path)) {
310
        continue;
Pawel G's avatar
Pawel G committed
311
      }
312 313 314 315

      // Load entity object if this is an entity route.
      $route_parameters = $url_object->getRouteParameters();
      $entity = !empty($route_parameters)
316 317
        ? $this->entityTypeManager->getStorage(key($route_parameters))
          ->load($route_parameters[key($route_parameters)])
318 319 320 321 322 323 324 325 326 327
        : NULL;

      $path_data = [
        'path' => $path,
        'lastmod' => method_exists($entity, 'getChangedTime') ? date_iso8601($entity->getChangedTime()) : NULL,
        'priority' => isset($custom_path['priority']) ? $custom_path['priority'] : NULL,
      ];
      if (!is_null($entity)) {
        $path_data['entity_info'] = ['entity_type' => $entity->getEntityTypeId(), 'id' => $entity->id()];
      }
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349
      $this->addUrlVariants($url_object, $path_data, $entity);
    }
    $this->processSegment();
  }

  /**
   * @param $url_object
   * @param $path_data
   * @param $entity
   */
  private function addUrlVariants($url_object, $path_data, $entity) {
    $alternate_urls = [];

    $translation_languages = !is_null($entity) && $this->batchInfo['skip_untranslated']
      ? $entity->getTranslationLanguages() : $this->languages;

    if (!is_null($entity) && isset($translation_languages['und'])) {
      $alternate_urls[$this->defaultLanguageId] = $url_object
        ->setOption('language', $this->languages[$this->defaultLanguageId])
        ->toString();
    }
    else {
Pawel G's avatar
Pawel G committed
350
      foreach ($translation_languages as $language) {
351 352 353 354 355
        if (!is_null($entity) && $this->batchInfo['skip_untranslated']) {
          $translation = $entity->getTranslation($language->getId());
          if (!$translation->access('view')) {
            continue;
          }
356
        }
357 358 359
        $alternate_urls[$language->getId()] = $url_object
          ->setOption('language', $language)
          ->toString();
360 361
      }
    }
362

Pawel G's avatar
Pawel G committed
363
    foreach ($alternate_urls as $langcode => $url) {
364 365
      $this->context['results']['generate'][] = $path_data + ['langcode' => $langcode, 'url' => $url, 'alternate_urls' => $alternate_urls];
    }
366 367 368 369 370 371 372 373 374 375 376 377 378 379
  }

  /**
   * Callback function called by the batch API when all operations are finished.
   *
   * @see https://api.drupal.org/api/drupal/core!includes!form.inc/group/batch/8
   */
  public function finishGeneration($success, $results, $operations) {
    if ($success) {
      $remove_sitemap = empty($results['chunk_count']);
      if (!empty($results['generate']) || $remove_sitemap) {
        $this->sitemapGenerator->generateSitemap($results['generate'], $remove_sitemap);
      }
      Cache::invalidateTags(['simple_sitemap']);
380
      drupal_set_message($this->t(self::REGENERATION_FINISHED_MESSAGE,
381 382 383
        ['@url' => $GLOBALS['base_url'] . '/sitemap.xml']));
    }
    else {
Pawel G's avatar
Pawel G committed
384
      // todo: register error.
385 386
    }
  }
Pawel G's avatar
Pawel G committed
387

388
}