['index', 'priority', 'changefreq', 'include_images'], 'custom' => ['priority', 'changefreq'], ]; /** * @var array */ protected static $linkSettingDefaults = [ 'index' => 1, 'priority' => 0.5, 'changefreq' => '', 'include_images' => 0, ]; /** * Simplesitemap constructor. * @param \Drupal\simple_sitemap\SitemapGenerator $sitemapGenerator * @param \Drupal\simple_sitemap\EntityHelper $entityHelper * @param \Drupal\Core\Config\ConfigFactory $configFactory * @param \Drupal\Core\Database\Connection $database * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager * @param \Drupal\Core\Path\PathValidator $pathValidator * @param \Drupal\Core\Datetime\DateFormatter $dateFormatter * @param \Drupal\Component\Datetime\Time $time * @param \Drupal\simple_sitemap\Batch $batch * @param \Drupal\simple_sitemap\Plugin\simple_sitemap\UrlGenerator\UrlGeneratorManager $urlGeneratorManager */ public function __construct( SitemapGenerator $sitemapGenerator, EntityHelper $entityHelper, ConfigFactory $configFactory, Connection $database, EntityTypeManagerInterface $entityTypeManager, PathValidator $pathValidator, DateFormatter $dateFormatter, Time $time, Batch $batch, UrlGeneratorManager $urlGeneratorManager ) { $this->sitemapGenerator = $sitemapGenerator; $this->entityHelper = $entityHelper; $this->configFactory = $configFactory; $this->db = $database; $this->entityTypeManager = $entityTypeManager; $this->pathValidator = $pathValidator; $this->dateFormatter = $dateFormatter; $this->time = $time; $this->batch = $batch; $this->urlGeneratorManager = $urlGeneratorManager; } /** * Returns a specific sitemap setting or a default value if setting does not * exist. * * @param string $name * Name of the setting, like 'max_links'. * * @param mixed $default * Value to be returned if the setting does not exist in the configuration. * * @return mixed * The current setting from configuration or a default value. */ public function getSetting($name, $default = FALSE) { $setting = $this->configFactory ->get('simple_sitemap.settings') ->get($name); return NULL !== $setting ? $setting : $default; } /** * Stores a specific sitemap setting in configuration. * * @param string $name * Setting name, like 'max_links'. * @param mixed $setting * The setting to be saved. * * @return $this */ public function saveSetting($name, $setting) { $this->configFactory->getEditable('simple_sitemap.settings') ->set($name, $setting)->save(); return $this; } /** * Returns the whole sitemap, a requested sitemap chunk, * or the sitemap index file. * * @param int $chunk_id * * @return string|false * If no sitemap id provided, either a sitemap index is returned, or the * whole sitemap, if the amount of links does not exceed the max links * setting. If a sitemap id is provided, a sitemap chunk is returned. False * if sitemap is not retrievable from the database. */ public function getSitemap($chunk_id = NULL) { $chunk_info = $this->fetchSitemapChunkInfo(); if (NULL === $chunk_id || !isset($chunk_info[$chunk_id])) { if (count($chunk_info) > 1) { // Return sitemap index, if there are multiple sitemap chunks. return $this->getSitemapIndex($chunk_info); } else { // Return sitemap if there is only one chunk. return count($chunk_info) === 1 && isset($chunk_info[SitemapGenerator::FIRST_CHUNK_INDEX]) ? $this->fetchSitemapChunk(SitemapGenerator::FIRST_CHUNK_INDEX) ->sitemap_string : FALSE; } } else { // Return specific sitemap chunk. return $this->fetchSitemapChunk($chunk_id)->sitemap_string; } } /** * Fetches all sitemap chunk timestamps keyed by chunk ID. * * @return array * An array containing chunk creation timestamps keyed by chunk ID. */ protected function fetchSitemapChunkInfo() { return $this->db ->query('SELECT id, sitemap_created FROM {simple_sitemap}') ->fetchAllAssoc('id'); } /** * Fetches a single sitemap chunk by ID. * * @param int $id * The chunk ID. * * @return object * A sitemap chunk object. */ protected function fetchSitemapChunk($id) { return $this->db->query('SELECT * FROM {simple_sitemap} WHERE id = :id', [':id' => $id])->fetchObject(); } /** * Generates the XML sitemap and saves it to the db. * * @param string $from * Can be 'form', 'backend', 'drush' or 'nobatch'. * This decides how the batch process is to be run. * * @return bool|\Drupal\simple_sitemap\Simplesitemap */ public function generateSitemap($from = 'form') { $this->batch->setBatchSettings([ 'base_url' => $this->getSetting('base_url', ''), 'batch_process_limit' => $this->getSetting('batch_process_limit', NULL), 'max_links' => $this->getSetting('max_links', 2000), 'skip_untranslated' => $this->getSetting('skip_untranslated', FALSE), 'remove_duplicates' => $this->getSetting('remove_duplicates', TRUE), 'excluded_languages' => $this->getSetting('excluded_languages', []), 'from' => $from, ]); $plugins = $this->urlGeneratorManager->getDefinitions(); usort($plugins, function($a, $b) { return $a['weight'] - $b['weight']; }); foreach ($plugins as $plugin) { if ($plugin['enabled']) { if (!empty($plugin['settings']['instantiate_for_each_data_set'])) { foreach ($this->urlGeneratorManager->createInstance($plugin['id'])->getDataSets() as $data_sets) { $this->batch->addOperation($plugin['id'], $data_sets); } } else { $this->batch->addOperation($plugin['id']); } } } $success = $this->batch->start(); return $from === 'nobatch' ? $this : $success; } /** * Generates and returns the sitemap index as string. * * @param array $chunk_info * Array containing chunk creation timestamps keyed by chunk ID. * * @return string * The sitemap index. * * @todo Need to make sure response is cached. */ protected function getSitemapIndex($chunk_info) { return $this->sitemapGenerator ->setSettings(['base_url' => $this->getSetting('base_url', '')]) ->generateSitemapIndex($chunk_info); } /** * Returns a 'time ago' string of last timestamp generation. * * @return string|false * Formatted timestamp of last sitemap generation, otherwise FALSE. */ public function getGeneratedAgo() { $chunks = $this->fetchSitemapChunkInfo(); if (isset($chunks[SitemapGenerator::FIRST_CHUNK_INDEX]->sitemap_created)) { return $this->dateFormatter ->formatInterval($this->time->getRequestTime() - $chunks[SitemapGenerator::FIRST_CHUNK_INDEX] ->sitemap_created); } return FALSE; } /** * Enables sitemap support for an entity type. Enabled entity types show * sitemap settings on their bundle setting forms. If an enabled entity type * features bundles (e.g. 'node'), it needs to be set up with * setBundleSettings() as well. * * @param string $entity_type_id * Entity type id like 'node'. * * @return $this */ public function enableEntityType($entity_type_id) { $enabled_entity_types = $this->getSetting('enabled_entity_types'); if (!in_array($entity_type_id, $enabled_entity_types)) { $enabled_entity_types[] = $entity_type_id; $this->saveSetting('enabled_entity_types', $enabled_entity_types); } return $this; } /** * Disables sitemap support for an entity type. Disabling support for an * entity type deletes its sitemap settings permanently and removes sitemap * settings from entity forms. * * @param string $entity_type_id * Entity type id like 'node'. * * @return $this */ public function disableEntityType($entity_type_id) { // Updating settings. $enabled_entity_types = $this->getSetting('enabled_entity_types'); if (FALSE !== ($key = array_search($entity_type_id, $enabled_entity_types))) { unset ($enabled_entity_types[$key]); $this->saveSetting('enabled_entity_types', array_values($enabled_entity_types)); } // Deleting inclusion settings. $config_names = $this->configFactory->listAll("simple_sitemap.bundle_settings.$entity_type_id."); foreach ($config_names as $config_name) { $this->configFactory->getEditable($config_name)->delete(); } // Deleting entity overrides. $this->removeEntityInstanceSettings($entity_type_id); return $this; } /** * Sets sitemap settings for a non-bundle entity type (e.g. user) or a bundle * of an entity type (e.g. page). * * @param string $entity_type_id * Entity type id like 'node' the bundle belongs to. * @param string $bundle_name * Name of the bundle. NULL if entity type has no bundles. * @param array $settings * An array of sitemap settings for this bundle/entity type. * Example: ['index' => TRUE, 'priority' => 0.5, 'changefreq' => 'never', 'include_images' => FALSE]. * * @return $this * * @todo: enableEntityType automatically */ public function setBundleSettings($entity_type_id, $bundle_name = NULL, $settings = []) { $bundle_name = empty($bundle_name) ? $entity_type_id : $bundle_name; if (!empty($old_settings = $this->getBundleSettings($entity_type_id, $bundle_name))) { $settings = array_merge($old_settings, $settings); } else { self::supplementDefaultSettings('entity', $settings); } $bundle_settings = $this->configFactory ->getEditable("simple_sitemap.bundle_settings.$entity_type_id.$bundle_name"); foreach ($settings as $setting_key => $setting) { if ($setting_key === 'index') { $setting = intval($setting); } $bundle_settings->set($setting_key, $setting); } $bundle_settings->save(); // Delete entity overrides which are identical to new bundle setting. $sitemap_entity_types = $this->entityHelper->getSupportedEntityTypes(); if (isset($sitemap_entity_types[$entity_type_id])) { $entity_type = $sitemap_entity_types[$entity_type_id]; $keys = $entity_type->getKeys(); // Menu fix. $keys['bundle'] = $entity_type_id === 'menu_link_content' ? 'menu_name' : $keys['bundle']; $query = $this->entityTypeManager->getStorage($entity_type_id)->getQuery(); if (!$this->entityHelper->entityTypeIsAtomic($entity_type_id)) { $query->condition($keys['bundle'], $bundle_name); } $entity_ids = $query->execute(); $query = $this->db->select('simple_sitemap_entity_overrides', 'o') ->fields('o', ['id', 'inclusion_settings']) ->condition('o.entity_type', $entity_type_id); if (!empty($entity_ids)) { $query->condition('o.entity_id', $entity_ids, 'IN'); } $delete_instances = []; foreach ($query->execute()->fetchAll() as $result) { $delete = TRUE; $instance_settings = unserialize($result->inclusion_settings); foreach ($instance_settings as $setting_key => $instance_setting) { if ($instance_setting != $settings[$setting_key]) { $delete = FALSE; break; } } if ($delete) { $delete_instances[] = $result->id; } } if (!empty($delete_instances)) { $this->db->delete('simple_sitemap_entity_overrides') ->condition('id', $delete_instances, 'IN') ->execute(); } } else { //todo: log error } return $this; } /** * Gets sitemap settings for an entity bundle, a non-bundle entity type or for * all entity types and their bundles. * * @param string|null $entity_type_id * If set to null, sitemap settings for all entity types and their bundles * are fetched. * @param string|null $bundle_name * * @return array|false * Array of sitemap settings for an entity bundle, a non-bundle entity type * or for all entity types and their bundles. * False if entity type does not exist. */ public function getBundleSettings($entity_type_id = NULL, $bundle_name = NULL) { if (NULL !== $entity_type_id) { $bundle_name = empty($bundle_name) ? $entity_type_id : $bundle_name; $bundle_settings = $this->configFactory ->get("simple_sitemap.bundle_settings.$entity_type_id.$bundle_name") ->get(); return !empty($bundle_settings) ? $bundle_settings : FALSE; } else { $config_names = $this->configFactory->listAll('simple_sitemap.bundle_settings.'); $all_settings = []; foreach ($config_names as $config_name) { $config_name_parts = explode('.', $config_name); $all_settings[$config_name_parts[2]][$config_name_parts[3]] = $this->configFactory->get($config_name)->get(); } return $all_settings; } } /** * Supplements all missing link setting with default values. * * @param string $type * 'entity'|'custom' * @param array &$settings * @param array $overrides */ public static function supplementDefaultSettings($type, &$settings, $overrides = []) { foreach (self::$allowedLinkSettings[$type] as $allowed_link_setting) { if (!isset($settings[$allowed_link_setting]) && isset(self::$linkSettingDefaults[$allowed_link_setting])) { $settings[$allowed_link_setting] = isset($overrides[$allowed_link_setting]) ? $overrides[$allowed_link_setting] : self::$linkSettingDefaults[$allowed_link_setting]; } } } /** * Overrides entity bundle/entity type sitemap settings for a single entity. * * @param string $entity_type_id * @param int $id * @param array $settings * * @return $this */ public function setEntityInstanceSettings($entity_type_id, $id, $settings) { $entity = $this->entityTypeManager->getStorage($entity_type_id)->load($id); $bundle_settings = $this->getBundleSettings( $entity_type_id, $this->entityHelper->getEntityInstanceBundleName($entity) ); if (!empty($bundle_settings)) { // Check if overrides are different from bundle setting before saving. $override = FALSE; foreach ($settings as $key => $setting) { if (!isset($bundle_settings[$key]) || $setting != $bundle_settings[$key]) { $override = TRUE; break; } } // Save overrides for this entity if something is different. if ($override) { $this->db->merge('simple_sitemap_entity_overrides') ->key([ 'entity_type' => $entity_type_id, 'entity_id' => $id]) ->fields([ 'entity_type' => $entity_type_id, 'entity_id' => $id, 'inclusion_settings' => serialize(array_merge($bundle_settings, $settings)),]) ->execute(); } // Else unset override. else { $this->removeEntityInstanceSettings($entity_type_id, $id); } } else { //todo: log error } return $this; } /** * Gets sitemap settings for an entity instance which overrides the sitemap * settings of its bundle, or bundle settings, if they are not overridden. * * @param string $entity_type_id * @param int $id * * @return array|false */ public function getEntityInstanceSettings($entity_type_id, $id) { $results = $this->db->select('simple_sitemap_entity_overrides', 'o') ->fields('o', ['inclusion_settings']) ->condition('o.entity_type', $entity_type_id) ->condition('o.entity_id', $id) ->execute() ->fetchField(); if (!empty($results)) { return unserialize($results); } else { $entity = $this->entityTypeManager->getStorage($entity_type_id) ->load($id); return $this->getBundleSettings( $entity_type_id, $this->entityHelper->getEntityInstanceBundleName($entity) ); } } /** * Removes sitemap settings for an entity that overrides the sitemap settings * of its bundle. * * @param string $entity_type_id * @param string|null $entity_ids * * @return $this */ public function removeEntityInstanceSettings($entity_type_id, $entity_ids = NULL) { $query = $this->db->delete('simple_sitemap_entity_overrides') ->condition('entity_type', $entity_type_id); if (NULL !== $entity_ids) { $entity_ids = !is_array($entity_ids) ? [$entity_ids] : $entity_ids; $query->condition('entity_id', $entity_ids, 'IN'); } $query->execute(); return $this; } /** * Checks if an entity bundle (or a non-bundle entity type) is set to be * indexed in the sitemap settings. * * @param string $entity_type_id * @param string|null $bundle_name * * @return bool */ public function bundleIsIndexed($entity_type_id, $bundle_name = NULL) { $settings = $this->getBundleSettings($entity_type_id, $bundle_name); return !empty($settings['index']); } /** * Checks if an entity type is enabled in the sitemap settings. * * @param string $entity_type_id * * @return bool */ public function entityTypeIsEnabled($entity_type_id) { return in_array($entity_type_id, $this->getSetting('enabled_entity_types', [])); } /** * Stores a custom path along with its sitemap settings to configuration. * * @param string $path * @param array $settings * * @return $this * * @todo Validate $settings and throw exceptions */ public function addCustomLink($path, $settings = []) { if (!$this->pathValidator->isValid($path)) { // todo: log error. return $this; } if ($path[0] !== '/') { // todo: log error. return $this; } $custom_links = $this->getCustomLinks(FALSE); foreach ($custom_links as $key => $link) { if ($link['path'] === $path) { $link_key = $key; break; } } $link_key = isset($link_key) ? $link_key : count($custom_links); $custom_links[$link_key] = ['path' => $path] + $settings; $this->configFactory->getEditable('simple_sitemap.custom') ->set('links', $custom_links)->save(); return $this; } /** * Returns an array of custom paths and their sitemap settings. * * @param bool $supplement_default_settings * @return array */ public function getCustomLinks($supplement_default_settings = TRUE) { $custom_links = $this->configFactory ->get('simple_sitemap.custom') ->get('links'); if ($supplement_default_settings) { foreach ($custom_links as $i => $link_settings) { self::supplementDefaultSettings('custom', $link_settings); $custom_links[$i] = $link_settings; } } return $custom_links !== NULL ? $custom_links : []; } /** * Returns settings for a custom path added to the sitemap settings. * * @param string $path * * @return array|false */ public function getCustomLink($path) { foreach ($this->getCustomLinks() as $key => $link) { if ($link['path'] === $path) { return $link; } } return FALSE; } /** * Removes a custom path from the sitemap settings. * * @param string $path * * @return $this */ public function removeCustomLink($path) { $custom_links = $this->getCustomLinks(FALSE); foreach ($custom_links as $key => $link) { if ($link['path'] === $path) { unset($custom_links[$key]); $custom_links = array_values($custom_links); $this->configFactory->getEditable('simple_sitemap.custom') ->set('links', $custom_links)->save(); break; } } return $this; } /** * Removes all custom paths from the sitemap settings. * * @return $this */ public function removeCustomLinks() { $this->configFactory->getEditable('simple_sitemap.custom') ->set('links', [])->save(); return $this; } }