3 namespace Drupal\simple_sitemap;
5 use Drupal\Core\Database\Connection;
6 use Drupal\Core\Entity\EntityTypeManagerInterface;
7 use Drupal\Core\Path\PathValidator;
8 use Drupal\Core\Config\ConfigFactory;
9 use Drupal\Core\Datetime\DateFormatter;
10 use Drupal\Component\Datetime\Time;
11 use Drupal\simple_sitemap\Plugin\simple_sitemap\UrlGenerator\UrlGeneratorManager;
15 * @package Drupal\simple_sitemap
20 * @var \Drupal\simple_sitemap\SitemapGenerator
22 protected $sitemapGenerator;
25 * @var \Drupal\simple_sitemap\EntityHelper
27 protected $entityHelper;
30 * @var \Drupal\Core\Config\ConfigFactory
32 protected $configFactory;
35 * @var \Drupal\Core\Database\Connection
40 * @var \Drupal\Core\Entity\EntityTypeManagerInterface
42 protected $entityTypeManager;
45 * @var \Drupal\Core\Path\PathValidator
47 protected $pathValidator;
50 * @var \Drupal\Core\Datetime\DateFormatter
52 protected $dateFormatter;
55 * @var \Drupal\Component\Datetime\Time
60 * @var \Drupal\simple_sitemap\Batch
65 * @var \Drupal\Core\Extension\ModuleHandler
67 protected $moduleHandler;
70 * @var \Drupal\simple_sitemap\Plugin\simple_sitemap\UrlGenerator\UrlGeneratorManager
72 protected $urlGeneratorManager;
77 protected static $allowedLinkSettings = [
78 'entity' => ['index', 'priority', 'changefreq', 'include_images'],
79 'custom' => ['priority', 'changefreq'],
85 protected static $linkSettingDefaults = [
89 'include_images' => 0,
93 * Simplesitemap constructor.
94 * @param \Drupal\simple_sitemap\SitemapGenerator $sitemapGenerator
95 * @param \Drupal\simple_sitemap\EntityHelper $entityHelper
96 * @param \Drupal\Core\Config\ConfigFactory $configFactory
97 * @param \Drupal\Core\Database\Connection $database
98 * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
99 * @param \Drupal\Core\Path\PathValidator $pathValidator
100 * @param \Drupal\Core\Datetime\DateFormatter $dateFormatter
101 * @param \Drupal\Component\Datetime\Time $time
102 * @param \Drupal\simple_sitemap\Batch $batch
103 * @param \Drupal\simple_sitemap\Plugin\simple_sitemap\UrlGenerator\UrlGeneratorManager $urlGeneratorManager
105 public function __construct(
106 SitemapGenerator $sitemapGenerator,
107 EntityHelper $entityHelper,
108 ConfigFactory $configFactory,
109 Connection $database,
110 EntityTypeManagerInterface $entityTypeManager,
111 PathValidator $pathValidator,
112 DateFormatter $dateFormatter,
115 UrlGeneratorManager $urlGeneratorManager
117 $this->sitemapGenerator = $sitemapGenerator;
118 $this->entityHelper = $entityHelper;
119 $this->configFactory = $configFactory;
120 $this->db = $database;
121 $this->entityTypeManager = $entityTypeManager;
122 $this->pathValidator = $pathValidator;
123 $this->dateFormatter = $dateFormatter;
125 $this->batch = $batch;
126 $this->urlGeneratorManager = $urlGeneratorManager;
130 * Returns a specific sitemap setting or a default value if setting does not
133 * @param string $name
134 * Name of the setting, like 'max_links'.
136 * @param mixed $default
137 * Value to be returned if the setting does not exist in the configuration.
140 * The current setting from configuration or a default value.
142 public function getSetting($name, $default = FALSE) {
143 $setting = $this->configFactory
144 ->get('simple_sitemap.settings')
146 return NULL !== $setting ? $setting : $default;
150 * Stores a specific sitemap setting in configuration.
152 * @param string $name
153 * Setting name, like 'max_links'.
154 * @param mixed $setting
155 * The setting to be saved.
159 public function saveSetting($name, $setting) {
160 $this->configFactory->getEditable('simple_sitemap.settings')
161 ->set($name, $setting)->save();
166 * Returns the whole sitemap, a requested sitemap chunk,
167 * or the sitemap index file.
169 * @param int $chunk_id
171 * @return string|false
172 * If no sitemap id provided, either a sitemap index is returned, or the
173 * whole sitemap, if the amount of links does not exceed the max links
174 * setting. If a sitemap id is provided, a sitemap chunk is returned. False
175 * if sitemap is not retrievable from the database.
177 public function getSitemap($chunk_id = NULL) {
178 $chunk_info = $this->fetchSitemapChunkInfo();
180 if (NULL === $chunk_id || !isset($chunk_info[$chunk_id])) {
182 if (count($chunk_info) > 1) {
183 // Return sitemap index, if there are multiple sitemap chunks.
184 return $this->getSitemapIndex($chunk_info);
187 // Return sitemap if there is only one chunk.
188 return count($chunk_info) === 1
189 && isset($chunk_info[SitemapGenerator::FIRST_CHUNK_INDEX])
190 ? $this->fetchSitemapChunk(SitemapGenerator::FIRST_CHUNK_INDEX)
196 // Return specific sitemap chunk.
197 return $this->fetchSitemapChunk($chunk_id)->sitemap_string;
202 * Fetches all sitemap chunk timestamps keyed by chunk ID.
205 * An array containing chunk creation timestamps keyed by chunk ID.
207 protected function fetchSitemapChunkInfo() {
209 ->query('SELECT id, sitemap_created FROM {simple_sitemap}')
210 ->fetchAllAssoc('id');
214 * Fetches a single sitemap chunk by ID.
220 * A sitemap chunk object.
222 protected function fetchSitemapChunk($id) {
223 return $this->db->query('SELECT * FROM {simple_sitemap} WHERE id = :id',
224 [':id' => $id])->fetchObject();
228 * Generates the XML sitemap and saves it to the db.
230 * @param string $from
231 * Can be 'form', 'backend', 'drush' or 'nobatch'.
232 * This decides how the batch process is to be run.
234 * @return bool|\Drupal\simple_sitemap\Simplesitemap
236 public function generateSitemap($from = 'form') {
238 $this->batch->setBatchSettings([
239 'base_url' => $this->getSetting('base_url', ''),
240 'batch_process_limit' => $this->getSetting('batch_process_limit', NULL),
241 'max_links' => $this->getSetting('max_links', 2000),
242 'skip_untranslated' => $this->getSetting('skip_untranslated', FALSE),
243 'remove_duplicates' => $this->getSetting('remove_duplicates', TRUE),
244 'excluded_languages' => $this->getSetting('excluded_languages', []),
248 $plugins = $this->urlGeneratorManager->getDefinitions();
250 usort($plugins, function($a, $b) {
251 return $a['weight'] - $b['weight'];
254 foreach ($plugins as $plugin) {
255 if ($plugin['enabled']) {
256 if (!empty($plugin['settings']['instantiate_for_each_data_set'])) {
257 foreach ($this->urlGeneratorManager->createInstance($plugin['id'])->getDataSets() as $data_sets) {
258 $this->batch->addOperation($plugin['id'], $data_sets);
262 $this->batch->addOperation($plugin['id']);
267 $success = $this->batch->start();
268 return $from === 'nobatch' ? $this : $success;
272 * Generates and returns the sitemap index as string.
274 * @param array $chunk_info
275 * Array containing chunk creation timestamps keyed by chunk ID.
280 * @todo Need to make sure response is cached.
282 protected function getSitemapIndex($chunk_info) {
283 return $this->sitemapGenerator
284 ->setSettings(['base_url' => $this->getSetting('base_url', '')])
285 ->generateSitemapIndex($chunk_info);
289 * Returns a 'time ago' string of last timestamp generation.
291 * @return string|false
292 * Formatted timestamp of last sitemap generation, otherwise FALSE.
294 public function getGeneratedAgo() {
295 $chunks = $this->fetchSitemapChunkInfo();
296 if (isset($chunks[SitemapGenerator::FIRST_CHUNK_INDEX]->sitemap_created)) {
297 return $this->dateFormatter
298 ->formatInterval($this->time->getRequestTime() - $chunks[SitemapGenerator::FIRST_CHUNK_INDEX]
305 * Enables sitemap support for an entity type. Enabled entity types show
306 * sitemap settings on their bundle setting forms. If an enabled entity type
307 * features bundles (e.g. 'node'), it needs to be set up with
308 * setBundleSettings() as well.
310 * @param string $entity_type_id
311 * Entity type id like 'node'.
315 public function enableEntityType($entity_type_id) {
316 $enabled_entity_types = $this->getSetting('enabled_entity_types');
317 if (!in_array($entity_type_id, $enabled_entity_types)) {
318 $enabled_entity_types[] = $entity_type_id;
319 $this->saveSetting('enabled_entity_types', $enabled_entity_types);
325 * Disables sitemap support for an entity type. Disabling support for an
326 * entity type deletes its sitemap settings permanently and removes sitemap
327 * settings from entity forms.
329 * @param string $entity_type_id
330 * Entity type id like 'node'.
334 public function disableEntityType($entity_type_id) {
336 // Updating settings.
337 $enabled_entity_types = $this->getSetting('enabled_entity_types');
338 if (FALSE !== ($key = array_search($entity_type_id, $enabled_entity_types))) {
339 unset ($enabled_entity_types[$key]);
340 $this->saveSetting('enabled_entity_types', array_values($enabled_entity_types));
343 // Deleting inclusion settings.
344 $config_names = $this->configFactory->listAll("simple_sitemap.bundle_settings.$entity_type_id.");
345 foreach ($config_names as $config_name) {
346 $this->configFactory->getEditable($config_name)->delete();
349 // Deleting entity overrides.
350 $this->removeEntityInstanceSettings($entity_type_id);
355 * Sets sitemap settings for a non-bundle entity type (e.g. user) or a bundle
356 * of an entity type (e.g. page).
358 * @param string $entity_type_id
359 * Entity type id like 'node' the bundle belongs to.
360 * @param string $bundle_name
361 * Name of the bundle. NULL if entity type has no bundles.
362 * @param array $settings
363 * An array of sitemap settings for this bundle/entity type.
364 * Example: ['index' => TRUE, 'priority' => 0.5, 'changefreq' => 'never', 'include_images' => FALSE].
368 * @todo: enableEntityType automatically
370 public function setBundleSettings($entity_type_id, $bundle_name = NULL, $settings = []) {
371 $bundle_name = empty($bundle_name) ? $entity_type_id : $bundle_name;
373 if (!empty($old_settings = $this->getBundleSettings($entity_type_id, $bundle_name))) {
374 $settings = array_merge($old_settings, $settings);
377 self::supplementDefaultSettings('entity', $settings);
380 $bundle_settings = $this->configFactory
381 ->getEditable("simple_sitemap.bundle_settings.$entity_type_id.$bundle_name");
382 foreach ($settings as $setting_key => $setting) {
383 if ($setting_key === 'index') {
384 $setting = intval($setting);
386 $bundle_settings->set($setting_key, $setting);
388 $bundle_settings->save();
390 // Delete entity overrides which are identical to new bundle setting.
391 $sitemap_entity_types = $this->entityHelper->getSupportedEntityTypes();
392 if (isset($sitemap_entity_types[$entity_type_id])) {
393 $entity_type = $sitemap_entity_types[$entity_type_id];
394 $keys = $entity_type->getKeys();
397 $keys['bundle'] = $entity_type_id === 'menu_link_content' ? 'menu_name' : $keys['bundle'];
399 $query = $this->entityTypeManager->getStorage($entity_type_id)->getQuery();
400 if (!$this->entityHelper->entityTypeIsAtomic($entity_type_id)) {
401 $query->condition($keys['bundle'], $bundle_name);
403 $entity_ids = $query->execute();
405 $query = $this->db->select('simple_sitemap_entity_overrides', 'o')
406 ->fields('o', ['id', 'inclusion_settings'])
407 ->condition('o.entity_type', $entity_type_id);
408 if (!empty($entity_ids)) {
409 $query->condition('o.entity_id', $entity_ids, 'IN');
412 $delete_instances = [];
413 foreach ($query->execute()->fetchAll() as $result) {
415 $instance_settings = unserialize($result->inclusion_settings);
416 foreach ($instance_settings as $setting_key => $instance_setting) {
417 if ($instance_setting != $settings[$setting_key]) {
423 $delete_instances[] = $result->id;
426 if (!empty($delete_instances)) {
427 $this->db->delete('simple_sitemap_entity_overrides')
428 ->condition('id', $delete_instances, 'IN')
439 * Gets sitemap settings for an entity bundle, a non-bundle entity type or for
440 * all entity types and their bundles.
442 * @param string|null $entity_type_id
443 * If set to null, sitemap settings for all entity types and their bundles
445 * @param string|null $bundle_name
447 * @return array|false
448 * Array of sitemap settings for an entity bundle, a non-bundle entity type
449 * or for all entity types and their bundles.
450 * False if entity type does not exist.
452 public function getBundleSettings($entity_type_id = NULL, $bundle_name = NULL) {
453 if (NULL !== $entity_type_id) {
454 $bundle_name = empty($bundle_name) ? $entity_type_id : $bundle_name;
455 $bundle_settings = $this->configFactory
456 ->get("simple_sitemap.bundle_settings.$entity_type_id.$bundle_name")
458 return !empty($bundle_settings) ? $bundle_settings : FALSE;
461 $config_names = $this->configFactory->listAll('simple_sitemap.bundle_settings.');
463 foreach ($config_names as $config_name) {
464 $config_name_parts = explode('.', $config_name);
465 $all_settings[$config_name_parts[2]][$config_name_parts[3]] = $this->configFactory->get($config_name)->get();
467 return $all_settings;
472 * Supplements all missing link setting with default values.
474 * @param string $type
476 * @param array &$settings
477 * @param array $overrides
479 public static function supplementDefaultSettings($type, &$settings, $overrides = []) {
480 foreach (self::$allowedLinkSettings[$type] as $allowed_link_setting) {
481 if (!isset($settings[$allowed_link_setting])
482 && isset(self::$linkSettingDefaults[$allowed_link_setting])) {
483 $settings[$allowed_link_setting] = isset($overrides[$allowed_link_setting])
484 ? $overrides[$allowed_link_setting]
485 : self::$linkSettingDefaults[$allowed_link_setting];
491 * Overrides entity bundle/entity type sitemap settings for a single entity.
493 * @param string $entity_type_id
495 * @param array $settings
499 public function setEntityInstanceSettings($entity_type_id, $id, $settings) {
500 $entity = $this->entityTypeManager->getStorage($entity_type_id)->load($id);
501 $bundle_settings = $this->getBundleSettings(
502 $entity_type_id, $this->entityHelper->getEntityInstanceBundleName($entity)
504 if (!empty($bundle_settings)) {
506 // Check if overrides are different from bundle setting before saving.
508 foreach ($settings as $key => $setting) {
509 if (!isset($bundle_settings[$key]) || $setting != $bundle_settings[$key]) {
514 // Save overrides for this entity if something is different.
516 $this->db->merge('simple_sitemap_entity_overrides')
518 'entity_type' => $entity_type_id,
521 'entity_type' => $entity_type_id,
523 'inclusion_settings' => serialize(array_merge($bundle_settings, $settings)),])
526 // Else unset override.
528 $this->removeEntityInstanceSettings($entity_type_id, $id);
538 * Gets sitemap settings for an entity instance which overrides the sitemap
539 * settings of its bundle, or bundle settings, if they are not overridden.
541 * @param string $entity_type_id
544 * @return array|false
546 public function getEntityInstanceSettings($entity_type_id, $id) {
547 $results = $this->db->select('simple_sitemap_entity_overrides', 'o')
548 ->fields('o', ['inclusion_settings'])
549 ->condition('o.entity_type', $entity_type_id)
550 ->condition('o.entity_id', $id)
554 if (!empty($results)) {
555 return unserialize($results);
558 $entity = $this->entityTypeManager->getStorage($entity_type_id)
560 return $this->getBundleSettings(
562 $this->entityHelper->getEntityInstanceBundleName($entity)
568 * Removes sitemap settings for an entity that overrides the sitemap settings
571 * @param string $entity_type_id
572 * @param string|null $entity_ids
576 public function removeEntityInstanceSettings($entity_type_id, $entity_ids = NULL) {
577 $query = $this->db->delete('simple_sitemap_entity_overrides')
578 ->condition('entity_type', $entity_type_id);
579 if (NULL !== $entity_ids) {
580 $entity_ids = !is_array($entity_ids) ? [$entity_ids] : $entity_ids;
581 $query->condition('entity_id', $entity_ids, 'IN');
588 * Checks if an entity bundle (or a non-bundle entity type) is set to be
589 * indexed in the sitemap settings.
591 * @param string $entity_type_id
592 * @param string|null $bundle_name
596 public function bundleIsIndexed($entity_type_id, $bundle_name = NULL) {
597 $settings = $this->getBundleSettings($entity_type_id, $bundle_name);
598 return !empty($settings['index']);
602 * Checks if an entity type is enabled in the sitemap settings.
604 * @param string $entity_type_id
608 public function entityTypeIsEnabled($entity_type_id) {
609 return in_array($entity_type_id, $this->getSetting('enabled_entity_types', []));
613 * Stores a custom path along with its sitemap settings to configuration.
615 * @param string $path
616 * @param array $settings
620 * @todo Validate $settings and throw exceptions
622 public function addCustomLink($path, $settings = []) {
623 if (!$this->pathValidator->isValid($path)) {
627 if ($path[0] !== '/') {
632 $custom_links = $this->getCustomLinks(FALSE);
633 foreach ($custom_links as $key => $link) {
634 if ($link['path'] === $path) {
639 $link_key = isset($link_key) ? $link_key : count($custom_links);
640 $custom_links[$link_key] = ['path' => $path] + $settings;
641 $this->configFactory->getEditable('simple_sitemap.custom')
642 ->set('links', $custom_links)->save();
647 * Returns an array of custom paths and their sitemap settings.
649 * @param bool $supplement_default_settings
652 public function getCustomLinks($supplement_default_settings = TRUE) {
653 $custom_links = $this->configFactory
654 ->get('simple_sitemap.custom')
657 if ($supplement_default_settings) {
658 foreach ($custom_links as $i => $link_settings) {
659 self::supplementDefaultSettings('custom', $link_settings);
660 $custom_links[$i] = $link_settings;
664 return $custom_links !== NULL ? $custom_links : [];
668 * Returns settings for a custom path added to the sitemap settings.
670 * @param string $path
672 * @return array|false
674 public function getCustomLink($path) {
675 foreach ($this->getCustomLinks() as $key => $link) {
676 if ($link['path'] === $path) {
684 * Removes a custom path from the sitemap settings.
686 * @param string $path
690 public function removeCustomLink($path) {
691 $custom_links = $this->getCustomLinks(FALSE);
692 foreach ($custom_links as $key => $link) {
693 if ($link['path'] === $path) {
694 unset($custom_links[$key]);
695 $custom_links = array_values($custom_links);
696 $this->configFactory->getEditable('simple_sitemap.custom')
697 ->set('links', $custom_links)->save();
705 * Removes all custom paths from the sitemap settings.
709 public function removeCustomLinks() {
710 $this->configFactory->getEditable('simple_sitemap.custom')
711 ->set('links', [])->save();