dc2084fa20d761008ad2141e0cc79428f5f207f8
[yaffs-website] / web / modules / contrib / simple_sitemap / src / Batch / BatchUrlGenerator.php
1 <?php
2
3 namespace Drupal\simple_sitemap\Batch;
4
5 use Drupal\Core\Url;
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;
16
17 /**
18  * Class BatchUrlGenerator
19  * @package Drupal\simple_sitemap\Batch
20  */
21 class BatchUrlGenerator {
22
23   use StringTranslationTrait;
24
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.';
30
31   /**
32    * @var \Drupal\simple_sitemap\Simplesitemap
33    */
34   protected $generator;
35
36   /**
37    * @var \Drupal\simple_sitemap\SitemapGenerator
38    */
39   protected $sitemapGenerator;
40
41   /**
42    * @var \Drupal\Core\Language\LanguageManagerInterface
43    */
44   protected $languageManager;
45
46   /**
47    * @var \Drupal\Core\Language\LanguageInterface[]
48    */
49   protected $languages;
50
51   /**
52    * @var string
53    */
54   protected $defaultLanguageId;
55
56   /**
57    * @var \Drupal\Core\Entity\EntityTypeManagerInterface
58    */
59   protected $entityTypeManager;
60
61   /**
62    * @var \Drupal\Core\Path\PathValidator
63    */
64   protected $pathValidator;
65
66   /**
67    * @var \Drupal\Core\Entity\Query\QueryFactory
68    */
69   protected $entityQuery;
70
71   /**
72    * @var \Drupal\simple_sitemap\Logger
73    */
74   protected $logger;
75
76   /**
77    * @var \Drupal\Core\Entity\EntityInterface|null
78    */
79   protected $anonUser;
80
81   /**
82    * @var array
83    */
84   protected $context;
85
86   /**
87    * @var array
88    */
89   protected $batchInfo;
90
91   /**
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
100    */
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,
108     Logger $logger
109   ) {
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);
121   }
122
123   /**
124    * @param $context
125    * @return $this
126    */
127   public function setContext(&$context) {
128     $this->context = &$context;
129     return $this;
130   }
131
132   /**
133    * @param array $batch_info
134    * @return $this
135    */
136   public function setBatchInfo(array $batch_info) {
137     $this->batchInfo = $batch_info;
138     return $this;
139   }
140
141   /**
142    * Batch callback function which generates urls to entity paths.
143    *
144    * @param array $entity_info
145    */
146   public function generateBundleUrls(array $entity_info) {
147
148     foreach ($this->getBatchIterationEntities($entity_info) as $entity_id => $entity) {
149
150       $this->setCurrentId($entity_id);
151
152       $entity_settings = $this->generator->getEntityInstanceSettings($entity_info['entity_type_name'], $entity_id);
153
154       if (empty($entity_settings['index'])) {
155         continue;
156       }
157
158       switch ($entity_info['entity_type_name']) {
159         // Loading url object for menu links.
160         case 'menu_link_content':
161           if (!$entity->isEnabled()) {
162             continue 2;
163           }
164           $url_object = $entity->getUrlObject();
165           break;
166
167         // Loading url object for other entities.
168         default:
169           $url_object = $entity->toUrl();
170       }
171
172       // Do not include external paths.
173       if (!$url_object->isRouted()) {
174         continue;
175       }
176
177       $path = $url_object->getInternalPath();
178
179       // Do not include paths that have been already indexed.
180       if ($this->batchInfo['remove_duplicates'] && $this->pathProcessed($path)) {
181         continue;
182       }
183
184       $url_object->setOption('absolute', TRUE);
185
186       $path_data = [
187         'path' => $path,
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'],
191       ];
192       $this->addUrlVariants($url_object, $path_data, $entity);
193     }
194     $this->processSegment();
195   }
196
197   /**
198    * Batch function which generates urls to custom paths.
199    *
200    * @param array $custom_paths
201    */
202   public function generateCustomUrls(array $custom_paths) {
203
204     $custom_paths = $this->getBatchIterationCustomPaths($custom_paths);
205
206     if ($this->needsInitialization()) {
207       $this->initializeBatch(count($custom_paths));
208     }
209
210     foreach ($custom_paths as $i => $custom_path) {
211       $this->setCurrentId($i);
212
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')
219           ->log('warning');
220         continue;
221       }
222       $url_object = Url::fromUserInput($custom_path['path'], ['absolute' => TRUE]);
223
224       $path = $url_object->getInternalPath();
225       if ($this->batchInfo['remove_duplicates'] && $this->pathProcessed($path)) {
226         continue;
227       }
228
229       $entity = $this->getEntityFromUrlObject($url_object);
230
231       $path_data = [
232         'path' => $path,
233         'lastmod' => method_exists($entity, 'getChangedTime') ? date_iso8601($entity->getChangedTime()) : NULL,
234         'priority' => isset($custom_path['priority']) ? $custom_path['priority'] : NULL,
235       ];
236       if (NULL !== $entity) {
237         $path_data['entity_info'] = ['entity_type' => $entity->getEntityTypeId(), 'id' => $entity->id()];
238       }
239       $this->addUrlVariants($url_object, $path_data, $entity);
240     }
241     $this->processSegment();
242   }
243
244   /**
245    * @return bool
246    */
247   protected function isBatch() {
248     return $this->batchInfo['from'] != 'nobatch';
249   }
250
251   /**
252    * @param string $path
253    * @return bool
254    */
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)) {
258       return TRUE;
259     }
260     $this->context['results']['processed_paths'][] = $path;
261     return FALSE;
262   }
263
264   /**
265    * @param $entity_info
266    * @return mixed
267    */
268   protected function getBatchIterationEntities($entity_info) {
269     $query = $this->entityQuery->get($entity_info['entity_type_name']);
270
271     if (!empty($entity_info['keys']['id'])) {
272       $query->sort($entity_info['keys']['id'], 'ASC');
273     }
274     if (!empty($entity_info['keys']['bundle'])) {
275       $query->condition($entity_info['keys']['bundle'], $entity_info['bundle_name']);
276     }
277     if (!empty($entity_info['keys']['status'])) {
278       $query->condition($entity_info['keys']['status'], 1);
279     }
280
281     if ($this->needsInitialization()) {
282       $count_query = clone $query;
283       $this->initializeBatch($count_query->count()->execute());
284     }
285
286     if ($this->isBatch()) {
287       $query->range($this->context['sandbox']['progress'], $this->batchInfo['batch_process_limit']);
288     }
289
290     return $this->entityTypeManager
291       ->getStorage($entity_info['entity_type_name'])
292       ->loadMultiple($query->execute());
293   }
294
295   /**
296    * @param array $custom_paths
297    * @return array
298    */
299   protected function getBatchIterationCustomPaths(array $custom_paths) {
300
301     if ($this->needsInitialization()) {
302       $this->initializeBatch(count($custom_paths));
303     }
304
305     if ($this->isBatch()) {
306       $custom_paths = array_slice($custom_paths, $this->context['sandbox']['progress'], $this->batchInfo['batch_process_limit']);
307     }
308
309     return $custom_paths;
310   }
311
312   /**
313    * @param $url_object
314    * @param $path_data
315    * @param $entity
316    */
317   protected function addUrlVariants($url_object, $path_data, $entity) {
318     $alternate_urls = [];
319
320     $translation_languages = NULL !== $entity && $this->batchInfo['skip_untranslated']
321       ? $entity->getTranslationLanguages() : $this->languages;
322
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());
328       }
329     }
330     else {
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());
338           }
339         }
340       }
341
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());
347         }
348       }
349     }
350
351     foreach ($alternate_urls as $langcode => $url) {
352       $this->context['results']['generate'][] = $path_data + ['langcode' => $langcode, 'url' => $url, 'alternate_urls' => $alternate_urls];
353     }
354   }
355
356   /**
357    * @return bool
358    */
359   protected function needsInitialization() {
360     return empty($this->context['sandbox']);
361   }
362
363   /**
364    * @param $max
365    */
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'] : [];
374     }
375   }
376
377   /**
378    * @param $id
379    */
380   protected function setCurrentId($id) {
381     if ($this->isBatch()) {
382       $this->context['sandbox']['progress']++;
383       $this->context['sandbox']['current_id'] = $id;
384     }
385   }
386
387   /**
388    *
389    */
390   protected function processSegment() {
391     if ($this->isBatch()) {
392       $this->setProgressInfo();
393     }
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));
403         }
404       }
405     }
406   }
407
408   /**
409    *
410    */
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']),
423         ]);
424       }
425     }
426   }
427
428   /**
429    * @param $url_object
430    * @return object|null
431    */
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])
438       : NULL;
439   }
440
441   /**
442    * @param string $url
443    * @return string
444    */
445   protected function replaceBaseUrlWithCustom($url) {
446     return !empty($this->batchInfo['base_url'])
447       ? str_replace($GLOBALS['base_url'], $this->batchInfo['base_url'], $url)
448       : $url;
449   }
450
451   /**
452    * Callback function called by the batch API when all operations are finished.
453    *
454    * @see https://api.drupal.org/api/drupal/core!includes!form.inc/group/batch/8
455    */
456   public function finishGeneration($success, $results, $operations) {
457     if ($success) {
458       $remove_sitemap = empty($results['chunk_count']);
459       if (!empty($results['generate']) || $remove_sitemap) {
460         $this->sitemapGenerator->generateSitemap($results['generate'], $remove_sitemap);
461       }
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.
466         ->display('status')
467         ->log('info');
468     }
469     else {
470       $this->logger->m(self::REGENERATION_FINISHED_ERROR_MESSAGE)
471         ->display('error', 'administer sitemap settings')
472         ->log('error');
473     }
474   }
475 }