3 namespace Drupal\simple_sitemap\Batch;
6 use Drupal\Component\Utility\Html;
7 use Drupal\Core\Cache\Cache;
8 use Drupal\Core\StringTranslation\StringTranslationTrait;
9 use Drupal\simple_sitemap\Logger;
10 use Drupal\simple_sitemap\Simplesitemap;
11 use Drupal\simple_sitemap\SitemapGenerator;
12 use Drupal\Core\Language\LanguageManagerInterface;
13 use Drupal\Core\Entity\EntityTypeManagerInterface;
14 use Drupal\Core\Path\PathValidator;
15 use Drupal\Core\Entity\Query\QueryFactory;
18 * Class BatchUrlGenerator
19 * @package Drupal\simple_sitemap\Batch
21 class BatchUrlGenerator {
23 use StringTranslationTrait;
25 const ANONYMOUS_USER_ID = 0;
26 const PATH_DOES_NOT_EXIST_OR_NO_ACCESS_MESSAGE = "The custom path @path has been omitted from the XML sitemap as it either does not exist, or it is not accessible to anonymous users. You can review custom paths <a href='@custom_paths_url'>here</a>.";
27 const PROCESSING_PATH_MESSAGE = 'Processing path #@current out of @max: @path';
28 const REGENERATION_FINISHED_MESSAGE = "The <a href='@url' target='_blank'>XML sitemap</a> has been regenerated for all languages.";
29 const REGENERATION_FINISHED_ERROR_MESSAGE = 'The sitemap generation finished with an error.';
32 * @var \Drupal\simple_sitemap\Simplesitemap
37 * @var \Drupal\simple_sitemap\SitemapGenerator
39 protected $sitemapGenerator;
42 * @var \Drupal\Core\Language\LanguageManagerInterface
44 protected $languageManager;
47 * @var \Drupal\Core\Language\LanguageInterface[]
54 protected $defaultLanguageId;
57 * @var \Drupal\Core\Entity\EntityTypeManagerInterface
59 protected $entityTypeManager;
62 * @var \Drupal\Core\Path\PathValidator
64 protected $pathValidator;
67 * @var \Drupal\Core\Entity\Query\QueryFactory
69 protected $entityQuery;
72 * @var \Drupal\simple_sitemap\Logger
77 * @var \Drupal\Core\Entity\EntityInterface|null
92 * BatchUrlGenerator constructor.
93 * @param \Drupal\simple_sitemap\Simplesitemap $generator
94 * @param \Drupal\simple_sitemap\SitemapGenerator $sitemap_generator
95 * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
96 * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
97 * @param \Drupal\Core\Path\PathValidator $path_validator
98 * @param \Drupal\Core\Entity\Query\QueryFactory $entity_query
99 * @param \Drupal\simple_sitemap\Logger $logger
101 public function __construct(
102 Simplesitemap $generator,
103 SitemapGenerator $sitemap_generator, //todo: use $this->generator->sitemapGenerator instead?
104 LanguageManagerInterface $language_manager,
105 EntityTypeManagerInterface $entity_type_manager,
106 PathValidator $path_validator,
107 QueryFactory $entity_query,
110 $this->generator = $generator;
111 // todo: using only one method, maybe make method static instead?
112 $this->sitemapGenerator = $sitemap_generator;
113 $this->languageManager = $language_manager;
114 $this->languages = $language_manager->getLanguages();
115 $this->defaultLanguageId = $language_manager->getDefaultLanguage()->getId();
116 $this->entityTypeManager = $entity_type_manager;
117 $this->pathValidator = $path_validator;
118 $this->entityQuery = $entity_query;
119 $this->logger = $logger;
120 $this->anonUser = $this->entityTypeManager->getStorage('user')->load(self::ANONYMOUS_USER_ID);
127 public function setContext(&$context) {
128 $this->context = &$context;
133 * @param array $batch_info
136 public function setBatchInfo(array $batch_info) {
137 $this->batchInfo = $batch_info;
142 * Batch callback function which generates urls to entity paths.
144 * @param array $entity_info
146 public function generateBundleUrls(array $entity_info) {
148 foreach ($this->getBatchIterationEntities($entity_info) as $entity_id => $entity) {
150 $this->setCurrentId($entity_id);
152 $entity_settings = $this->generator->getEntityInstanceSettings($entity_info['entity_type_name'], $entity_id);
154 if (empty($entity_settings['index'])) {
158 switch ($entity_info['entity_type_name']) {
159 // Loading url object for menu links.
160 case 'menu_link_content':
161 if (!$entity->isEnabled()) {
164 $url_object = $entity->getUrlObject();
167 // Loading url object for other entities.
169 $url_object = $entity->toUrl();
172 // Do not include external paths.
173 if (!$url_object->isRouted()) {
177 $path = $url_object->getInternalPath();
179 // Do not include paths that have been already indexed.
180 if ($this->batchInfo['remove_duplicates'] && $this->pathProcessed($path)) {
184 $url_object->setOption('absolute', TRUE);
188 'entity_info' => ['entity_type' => $entity_info['entity_type_name'], 'id' => $entity_id],
189 'lastmod' => method_exists($entity, 'getChangedTime') ? date_iso8601($entity->getChangedTime()) : NULL,
190 'priority' => $entity_settings['priority'],
192 $this->addUrlVariants($url_object, $path_data, $entity);
194 $this->processSegment();
198 * Batch function which generates urls to custom paths.
200 * @param array $custom_paths
202 public function generateCustomUrls(array $custom_paths) {
204 $custom_paths = $this->getBatchIterationCustomPaths($custom_paths);
206 if ($this->needsInitialization()) {
207 $this->initializeBatch(count($custom_paths));
210 foreach ($custom_paths as $i => $custom_path) {
211 $this->setCurrentId($i);
213 // 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()?
214 if (!$this->pathValidator->isValid($custom_path['path'])) {
215 // if (!(bool) $this->pathValidator->getUrlIfValidWithoutAccessCheck($custom_path['path'])) {
216 $this->logger->m(self::PATH_DOES_NOT_EXIST_OR_NO_ACCESS_MESSAGE,
217 ['@path' => $custom_path['path'], '@custom_paths_url' => $GLOBALS['base_url'] . '/admin/config/search/simplesitemap/custom'])
218 ->display('warning', 'administer sitemap settings')
222 $url_object = Url::fromUserInput($custom_path['path'], ['absolute' => TRUE]);
224 $path = $url_object->getInternalPath();
225 if ($this->batchInfo['remove_duplicates'] && $this->pathProcessed($path)) {
229 $entity = $this->getEntityFromUrlObject($url_object);
233 'lastmod' => method_exists($entity, 'getChangedTime') ? date_iso8601($entity->getChangedTime()) : NULL,
234 'priority' => isset($custom_path['priority']) ? $custom_path['priority'] : NULL,
236 if (NULL !== $entity) {
237 $path_data['entity_info'] = ['entity_type' => $entity->getEntityTypeId(), 'id' => $entity->id()];
239 $this->addUrlVariants($url_object, $path_data, $entity);
241 $this->processSegment();
247 protected function isBatch() {
248 return $this->batchInfo['from'] != 'nobatch';
252 * @param string $path
255 protected function pathProcessed($path) {
256 $path_pool = isset($this->context['results']['processed_paths']) ? $this->context['results']['processed_paths'] : [];
257 if (in_array($path, $path_pool)) {
260 $this->context['results']['processed_paths'][] = $path;
265 * @param $entity_info
268 protected function getBatchIterationEntities($entity_info) {
269 $query = $this->entityQuery->get($entity_info['entity_type_name']);
271 if (!empty($entity_info['keys']['id'])) {
272 $query->sort($entity_info['keys']['id'], 'ASC');
274 if (!empty($entity_info['keys']['bundle'])) {
275 $query->condition($entity_info['keys']['bundle'], $entity_info['bundle_name']);
277 if (!empty($entity_info['keys']['status'])) {
278 $query->condition($entity_info['keys']['status'], 1);
281 if ($this->needsInitialization()) {
282 $count_query = clone $query;
283 $this->initializeBatch($count_query->count()->execute());
286 if ($this->isBatch()) {
287 $query->range($this->context['sandbox']['progress'], $this->batchInfo['batch_process_limit']);
290 return $this->entityTypeManager
291 ->getStorage($entity_info['entity_type_name'])
292 ->loadMultiple($query->execute());
296 * @param array $custom_paths
299 protected function getBatchIterationCustomPaths(array $custom_paths) {
301 if ($this->needsInitialization()) {
302 $this->initializeBatch(count($custom_paths));
305 if ($this->isBatch()) {
306 $custom_paths = array_slice($custom_paths, $this->context['sandbox']['progress'], $this->batchInfo['batch_process_limit']);
309 return $custom_paths;
317 protected function addUrlVariants($url_object, $path_data, $entity) {
318 $alternate_urls = [];
320 $translation_languages = NULL !== $entity && $this->batchInfo['skip_untranslated']
321 ? $entity->getTranslationLanguages() : $this->languages;
323 // Entity is not translated.
324 if (NULL !== $entity && isset($translation_languages['und'])) {
325 if ($url_object->access($this->anonUser)) {
326 $url_object->setOption('language', $this->languages[$this->defaultLanguageId]);
327 $alternate_urls[$this->defaultLanguageId] = $this->replaceBaseUrlWithCustom($url_object->toString());
331 // Including only translated variants of entity.
332 if (NULL !== $entity && $this->batchInfo['skip_untranslated']) {
333 foreach ($translation_languages as $language) {
334 $translation = $entity->getTranslation($language->getId());
335 if ($translation->access('view', $this->anonUser)) {
336 $url_object->setOption('language', $language);
337 $alternate_urls[$language->getId()] = $this->replaceBaseUrlWithCustom($url_object->toString());
342 // Not an entity or including all untranslated variants.
343 elseif ($url_object->access($this->anonUser)) {
344 foreach ($translation_languages as $language) {
345 $url_object->setOption('language', $language);
346 $alternate_urls[$language->getId()] = $this->replaceBaseUrlWithCustom($url_object->toString());
351 foreach ($alternate_urls as $langcode => $url) {
352 $this->context['results']['generate'][] = $path_data + ['langcode' => $langcode, 'url' => $url, 'alternate_urls' => $alternate_urls];
359 protected function needsInitialization() {
360 return empty($this->context['sandbox']);
366 protected function initializeBatch($max) {
367 $this->context['results']['generate'] = !empty($this->context['results']['generate']) ? $this->context['results']['generate'] : [];
368 if ($this->isBatch()) {
369 $this->context['sandbox']['progress'] = 0;
370 $this->context['sandbox']['current_id'] = 0;
371 $this->context['sandbox']['max'] = $max;
372 $this->context['results']['processed_paths'] = !empty($this->context['results']['processed_paths'])
373 ? $this->context['results']['processed_paths'] : [];
380 protected function setCurrentId($id) {
381 if ($this->isBatch()) {
382 $this->context['sandbox']['progress']++;
383 $this->context['sandbox']['current_id'] = $id;
390 protected function processSegment() {
391 if ($this->isBatch()) {
392 $this->setProgressInfo();
394 if (!empty($this->batchInfo['max_links']) && count($this->context['results']['generate']) >= $this->batchInfo['max_links']) {
395 $chunks = array_chunk($this->context['results']['generate'], $this->batchInfo['max_links']);
396 foreach ($chunks as $i => $chunk_links) {
397 if (count($chunk_links) == $this->batchInfo['max_links']) {
398 $remove_sitemap = empty($this->context['results']['chunk_count']);
399 $this->sitemapGenerator->generateSitemap($chunk_links, $remove_sitemap);
400 $this->context['results']['chunk_count'] = !isset($this->context['results']['chunk_count'])
401 ? 1 : $this->context['results']['chunk_count'] + 1;
402 $this->context['results']['generate'] = array_slice($this->context['results']['generate'], count($chunk_links));
411 protected function setProgressInfo() {
412 if ($this->context['sandbox']['progress'] != $this->context['sandbox']['max']) {
413 // Providing progress info to the batch API.
414 $this->context['finished'] = $this->context['sandbox']['progress'] / $this->context['sandbox']['max'];
415 // Adding processing message after finishing every batch segment.
416 end($this->context['results']['generate']);
417 $last_key = key($this->context['results']['generate']);
418 if (!empty($this->context['results']['generate'][$last_key]['path'])) {
419 $this->context['message'] = $this->t(self::PROCESSING_PATH_MESSAGE, [
420 '@current' => $this->context['sandbox']['progress'],
421 '@max' => $this->context['sandbox']['max'],
422 '@path' => HTML::escape($this->context['results']['generate'][$last_key]['path']),
430 * @return object|null
432 protected function getEntityFromUrlObject($url_object) {
433 $route_parameters = $url_object->getRouteParameters();
434 return !empty($route_parameters) && $this->entityTypeManager
435 ->getDefinition($entity_type_id = key($route_parameters), FALSE)
436 ? $this->entityTypeManager->getStorage($entity_type_id)
437 ->load($route_parameters[$entity_type_id])
445 protected function replaceBaseUrlWithCustom($url) {
446 return !empty($this->batchInfo['base_url'])
447 ? str_replace($GLOBALS['base_url'], $this->batchInfo['base_url'], $url)
452 * Callback function called by the batch API when all operations are finished.
454 * @see https://api.drupal.org/api/drupal/core!includes!form.inc/group/batch/8
456 public function finishGeneration($success, $results, $operations) {
458 $remove_sitemap = empty($results['chunk_count']);
459 if (!empty($results['generate']) || $remove_sitemap) {
460 $this->sitemapGenerator->generateSitemap($results['generate'], $remove_sitemap);
462 Cache::invalidateTags(['simple_sitemap']);
463 $this->logger->m(self::REGENERATION_FINISHED_MESSAGE,
464 ['@url' => $GLOBALS['base_url'] . '/sitemap.xml'])
465 // ['@url' => $this->sitemapGenerator->getCustomBaseUrl() . '/sitemap.xml']) //todo: Use actual base URL for message.
470 $this->logger->m(self::REGENERATION_FINISHED_ERROR_MESSAGE)
471 ->display('error', 'administer sitemap settings')