$info) { $hooks[$hook] = ['group' => 'xmlsitemap']; } return $hooks; } /** * Implements hook_help(). */ function xmlsitemap_help($route_name, RouteMatchInterface $route_match) { $output = ''; switch ($route_name) { case 'xmlsitemap.admin_settings': case 'xmlsitemap.entities_settings': case 'entity.xmlsitemap.edit_form': case 'entity.xmlsitemap.delete_form': return; case 'xmlsitemap.admin_search': break; case 'xmlsitemap.admin_search_list': break; case 'help.page.xmlsitemap': $output = '
' . t('XML Sitemap automatically creates a sitemap that conforms to the sitemaps.org specification. This helps search engines keep their search results up to date.') . '
'; $output .= '' . t("You can adjust the settings for your site's sitemap at admin/config/search/xmlsitemap. Your can view your site's sitemap at http://yoursite.com/sitemap.xml.") . '
'; $output .= '' . t("When is necessary you can rebuild your sitemap at admin/config/search/xmlsitemap/rebuild.") . '
'; $output .= '' . t("You can configure all Custom Entities Settings at admin/config/search/xmlsitemap/entities/settings") . '
'; $output .= '' . t('It is highly recommended that you have clean URLs enabled for this project.') . '
'; return $output; case 'xmlsitemap.admin_rebuild': $output .= '' . t("This action rebuilds your site's XML Sitemap and regenerates the cached files, and may be a lengthy process. If you just installed XML Sitemap, this can be helpful to import all your site's content into the sitemap. Otherwise, this should only be used in emergencies.") . '
'; } $currentUser = \Drupal::currentUser(); if (strpos($route_name, 'xmlsitemap') !== FALSE && $currentUser->hasPermission('administer xmlsitemap')) { // Alert the user to any potential problems detected by hook_requirements. $output .= _xmlsitemap_get_blurb(); } return $output; } /** * Implements hook_theme(). */ function xmlsitemap_theme() { return [ 'xmlsitemap_content_settings_table' => [ 'render element' => 'element', 'file' => 'xmlsitemap.module', ], ]; } /** * Menu access callback; determines if the user can use the rebuild links page. * * @return bool * Returns TRUE if current user can access rebuild form. FALSE otherwise. */ function _xmlsitemap_rebuild_form_access() { $rebuild_types = xmlsitemap_get_rebuildable_link_types(); return !empty($rebuild_types) && \Drupal::currentUser()->hasPermission('administer xmlsitemap'); } /** * Implements hook_cron(). * * @todo Use new Queue system. Need to add {sitemap}.queued. * @todo Regenerate one at a time? */ function xmlsitemap_cron() { // If cron sitemap file regeneration is disabled, stop. if (\Drupal::config('xmlsitemap.settings')->get('disable_cron_regeneration')) { return; } // If there were no new or changed links, skip. if (!\Drupal::state()->get('xmlsitemap_regenerate_needed')) { return; } // If the minimum sitemap lifetime hasn't been passed, skip. $lifetime = \Drupal::time()->getRequestTime() - \Drupal::state()->get('xmlsitemap_generated_last'); if ($lifetime < \Drupal::config('xmlsitemap.settings')->get('minimum_lifetime')) { return; } xmlsitemap_xmlsitemap_index_links(\Drupal::config('xmlsitemap.settings')->get('batch_limit')); // Regenerate the sitemap XML files. xmlsitemap_run_unprogressive_batch('xmlsitemap_regenerate_batch'); } /** * Implements hook_modules_installed(). */ function xmlsitemap_modules_installed(array $modules) { Cache::invalidateTags(['xmlsitemap']); } /** * Implements hook_modules_uninstalled(). */ function xmlsitemap_modules_uninstalled(array $modules) { Cache::invalidateTags(['xmlsitemap']); } /** * Implements hook_robotstxt(). */ function xmlsitemap_robotstxt() { if ($sitemap = XmlSitemap::loadByContext()) { $uri = xmlsitemap_sitemap_uri($sitemap); $path = UrlHelper::isExternal($uri['path']) ? $uri['path'] : 'base://' . $uri['path']; $robotstxt[] = 'Sitemap: ' . Url::fromUri($path, $uri['options'])->toString(); return $robotstxt; } } /** * Internal default variables config for xmlsitemap_var(). * * @return array * Array with config variables of xmlsitemap.settings config object. */ function xmlsitemap_config_variables() { return [ 'minimum_lifetime' => 0, 'xsl' => 1, 'prefetch_aliases' => 1, 'chunk_size' => 'auto', 'batch_limit' => 100, 'path' => 'xmlsitemap', 'frontpage_priority' => 1.0, 'frontpage_changefreq' => XMLSITEMAP_FREQUENCY_DAILY, 'lastmod_format' => XMLSITEMAP_LASTMOD_MEDIUM, 'gz' => FALSE, 'disable_cron_regeneration' => FALSE, ]; } /** * Internal default variables state for xmlsitemap_var(). * * @return array * Array with state variables defined by xmlsitemap module. */ function xmlsitemap_state_variables() { return [ 'xmlsitemap_rebuild_needed' => FALSE, 'xmlsitemap_regenerate_needed' => TRUE, 'xmlsitemap_base_url' => '', 'xmlsitemap_generated_last' => 0, 'xmlsitemap_developer_mode' => 0, 'max_chunks' => NULL, 'max_filesize' => NULL, ]; } /** * Internal implementation of variable_get(). */ function xmlsitemap_var($name, $default = NULL) { $defaults = &drupal_static(__FUNCTION__); if (!isset($defaults)) { $defaults = xmlsitemap_config_variables(); $defaults += xmlsitemap_state_variables(); } // @todo Remove when stable. if (!isset($defaults[$name])) { trigger_error("Default variable for $name not found."); } if (\Drupal::state()->get($name, NULL) === NULL) { return \Drupal::config('xmlsitemap.settings')->get($name); } return \Drupal::state()->get($name); } /** * @defgroup xmlsitemap_api XML Sitemap API. * @{ * This is the XML Sitemap API to be used by modules wishing to work with * XML Sitemap and/or link data. */ /** * Load an XML Sitemap array from the database. * * @param mixed $smid * An XML Sitemap ID. * * @return \Drupal\xmlsitemap\XmlSitemapInterface * The XML Sitemap object. */ function xmlsitemap_sitemap_load($smid) { $sitemap = xmlsitemap_sitemap_load_multiple([$smid]); return $sitemap ? reset($sitemap) : FALSE; } /** * Load multiple XML Sitemaps from the database. * * @param array|bool $smids * An array of XML Sitemap IDs, or FALSE to load all XML Sitemaps. * @param array $conditions * An array of conditions in the form 'field' => $value. * * @return \Drupal\xmlsitemap\XmlSitemapInterface[] * An array of XML Sitemap objects. */ function xmlsitemap_sitemap_load_multiple($smids = [], array $conditions = []) { if ($smids !== FALSE) { $conditions['smid'] = $smids; } else { $conditions['smid'] = NULL; } $storage = Drupal::entityTypeManager()->getStorage('xmlsitemap'); /** @var \Drupal\xmlsitemap\XmlSitemapInterface[] $sitemaps */ $sitemaps = $storage->loadMultiple($conditions['smid']); if (count($sitemaps) <= 0) { return []; } return $sitemaps; } /** * Save changes to an XML Sitemap or add a new XML Sitemap. * * @param Drupal\xmlsitemap\XmlSitemapInterface $sitemap * The XML Sitemap array to be saved. If $sitemap->smid is omitted, a new * XML Sitemap will be added. * * @todo Save the sitemap's URL as a column? */ function xmlsitemap_sitemap_save(XmlSitemapInterface $sitemap) { $context = $sitemap->context; if (!isset($context) || !$context) { $sitemap->context = []; } // Make sure context is sorted before saving the hash. $sitemap->setOriginalId($sitemap->isNew() ? NULL : $sitemap->getId()); $sitemap->setId(xmlsitemap_sitemap_get_context_hash($context)); // If the context was changed, we need to perform additional actions. if (!$sitemap->isNew() && $sitemap->getId() != $sitemap->getOriginalId()) { // Rename the files directory so the sitemap does not break. $old_sitemap = xmlsitemap_sitemap_load($sitemap->getOriginalId()); $old_dir = xmlsitemap_get_directory($old_sitemap); $new_dir = xmlsitemap_get_directory($sitemap); xmlsitemap_directory_move($old_dir, $new_dir); // Mark the sitemaps as needing regeneration. \Drupal::state()->set('xmlsitemap_regenerate_needed', TRUE); } $sitemap->save(); return $sitemap; } /** * Delete an XML Sitemap. * * @param string $smid * An XML Sitemap ID. */ function xmlsitemap_sitemap_delete($smid) { xmlsitemap_sitemap_delete_multiple([$smid]); } /** * Delete multiple XML Sitemaps. * * @param array $smids * An array of XML Sitemap IDs. */ function xmlsitemap_sitemap_delete_multiple(array $smids) { if (!empty($smids)) { $sitemaps = xmlsitemap_sitemap_load_multiple($smids); foreach ($sitemaps as $sitemap) { $sitemap->delete(); \Drupal::moduleHandler()->invokeAll('xmlsitemap_sitemap_delete', [$sitemap]); } } } /** * Return the expected file path for a specific sitemap chunk. * * @param Drupal\xmlsitemap\XmlSitemapInterface $sitemap * An XmlSitemapInterface sitemap object. * @param string $chunk * An optional specific chunk in the sitemap. Defaults to the index page. * * @return string * File path for a specific sitemap chunk. */ function xmlsitemap_sitemap_get_file(XmlSitemapInterface $sitemap, $chunk = 'index') { return xmlsitemap_get_directory($sitemap) . "/{$chunk}.xml"; } /** * Find the maximum file size of all a sitemap's XML files. * * @param \Drupal\xmlsitemap\XmlSitemapInterface $sitemap * The XML Sitemap object. * * @return int * Maximum file size in the directory. */ function xmlsitemap_sitemap_get_max_filesize(XmlSitemapInterface $sitemap) { $dir = xmlsitemap_get_directory($sitemap); $sitemap->setMaxFileSize(0); /** @var \Drupal\Core\File\FileSystemInterface $file_system */ $file_system = \Drupal::service('file_system'); $files = $file_system->scanDirectory($dir, '/\.xml$/'); foreach ($files as $file) { $sitemap->setMaxFileSize(max($sitemap->getMaxFileSize(), filesize($file->uri))); } return $sitemap->getMaxFileSize(); } /** * Returns the hash string for a context. * * @param array $context * Context to be hashed. * * @return string * Hash string for the context. */ function xmlsitemap_sitemap_get_context_hash(array &$context) { ksort($context); return Crypt::hashBase64(serialize($context)); } /** * Returns the uri elements of an XML Sitemap. * * @param \Drupal\xmlsitemap\XmlSitemapInterface $sitemap * The sitemap represented by and XmlSitemapInterface object. * * @return array * An array containing the 'path' and 'options' keys used to build the uri of * the XML Sitemap, and matching the signature of url(). */ function xmlsitemap_sitemap_uri(XmlSitemapInterface $sitemap) { $uri['path'] = 'sitemap.xml'; $uri['options'] = \Drupal::moduleHandler()->invokeAll('xmlsitemap_context_url_options', [$sitemap->context]); $context = $sitemap->context; \Drupal::moduleHandler()->alter('xmlsitemap_context_url_options', $uri['options'], $context); $uri['options'] += [ 'absolute' => TRUE, 'base_url' => Settings::get('xmlsitemap_base_url', \Drupal::state()->get('xmlsitemap_base_url')), ]; return $uri; } /** * @} End of "defgroup xmlsitemap_api" */ function xmlsitemap_get_directory(XmlSitemapInterface $sitemap = NULL) { $directory = &drupal_static(__FUNCTION__); if (!isset($directory)) { $directory = \Drupal::config('xmlsitemap.settings')->get('path') ?: 'xmlsitemap'; } if ($sitemap != NULL && !empty($sitemap->id)) { return file_build_uri($directory . '/' . $sitemap->id); } else { return file_build_uri($directory); } } /** * Check that the sitemap files directory exists and is writable. */ function xmlsitemap_check_directory(XmlSitemapInterface $sitemap = NULL) { $directory = xmlsitemap_get_directory($sitemap); /** @var \Drupal\Core\File\FileSystemInterface $filesystem */ $filesystem = \Drupal::service('file_system'); $result = $filesystem->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS); if (!$result) { \Drupal::logger('file system')->error('The directory %directory does not exist or is not writable.', ['%directory' => $directory]); } return $result; } /** * Check all directories. */ function xmlsitemap_check_all_directories() { $directories = []; $sitemaps = xmlsitemap_sitemap_load_multiple(FALSE); foreach ($sitemaps as $sitemap) { $directory = xmlsitemap_get_directory($sitemap); $directories[$directory] = $directory; } /** @var \Drupal\Core\File\FileSystemInterface $filesystem */ $filesystem = \Drupal::service('file_system'); foreach ($directories as $directory) { $result = $filesystem->prepareDirectory($directory, $filesystem::CREATE_DIRECTORY | $filesystem::MODIFY_PERMISSIONS); if ($result) { $directories[$directory] = TRUE; } else { $directories[$directory] = FALSE; } } return $directories; } /** * Clears sitemap directory. * * @param \Drupal\xmlsitemap\XmlSitemapInterface $sitemap * Sitemap entity. * @param bool $delete * If TRUE, delete the path directory afterwards. * * @return bool * Returns TRUE is operation was successful, FALSE otherwise. */ function xmlsitemap_clear_directory(XmlSitemapInterface $sitemap = NULL, $delete = FALSE) { $directory = xmlsitemap_get_directory($sitemap); return _xmlsitemap_delete_recursive($directory, $delete); } /** * Move a directory to a new location. * * @param string $old_dir * A string specifying the filepath or URI of the original directory. * @param string $new_dir * A string specifying the filepath or URI of the new directory. * @param int $replace * Behavior when the destination file already exists. * Replace behavior when the destination file already exists. * * @return bool * TRUE if the directory was moved successfully. FALSE otherwise. */ function xmlsitemap_directory_move($old_dir, $new_dir, $replace = FileSystemInterface::EXISTS_REPLACE) { /** @var \Drupal\Core\File\FileSystemInterface $filesystem */ $filesystem = \Drupal::service('file_system'); $success = $filesystem->prepareDirectory($new_dir, $filesystem::CREATE_DIRECTORY | $filesystem::MODIFY_PERMISSIONS); $old_path = $filesystem->realpath($old_dir); $new_path = $filesystem->realpath($new_dir); if (!is_dir($old_path) || !is_dir($new_path) || !$success) { return FALSE; } $files = $filesystem->scanDirectory($old_dir, '/.*/'); foreach ($files as $file) { $file->uri_new = $new_dir . '/' . basename($file->filename); $success &= (bool) $filesystem->move($file->uri, $file->uri_new, $replace); } // The remove the directory. $success &= $filesystem->rmdir($old_dir); return $success; } /** * Recursively delete all files and folders in the specified filepath. * * This is a backport of Drupal 8's file_unmanaged_delete_recursive(). * * Note that this only deletes visible files with write permission. * * @param string $path * A filepath relative to the Drupal root directory. * @param bool $delete_root * A boolean if TRUE will delete the $path directory afterwards. * * @return bool * TRUE if operation was successful, FALSE otherwise. */ function _xmlsitemap_delete_recursive($path, $delete_root = FALSE) { /** @var \Drupal\Core\File\FileSystemInterface $filesystem */ $filesystem = \Drupal::service('file_system'); // Resolve streamwrapper URI to local path. $path = $filesystem->realpath($path); if (is_dir($path)) { $dir = dir($path); while (($entry = $dir->read()) !== FALSE) { if ($entry === '.' || $entry === '..') { continue; } $entry_path = $path . '/' . $entry; $filesystem->deleteRecursive($entry_path); } $dir->close(); return $delete_root ? $filesystem->rmdir($path) : TRUE; } return $filesystem->delete($path); } /** * Implements hook_entity_type_build(). */ function xmlsitemap_entity_type_build(array &$entity_types) { // Mark some specific core entity types as not supported by XML Sitemap. // If a site wants to undo this, they may use hook_entity_type_alter(). $unsupported_types = [ // Custom blocks. 'block_content', // Comments. 'comment', // Shortcut items. 'shortcut', // Redirects. 'redirect', // Custom Token module. // @see https://www.drupal.org/project/token_custom/issues/3150038 'token_custom', ]; /** @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */ foreach ($unsupported_types as $entity_type_id) { if (isset($entity_types[$entity_type_id])) { $entity_types[$entity_type_id]->set('xmlsitemap', FALSE); } } } /** * Determines if an entity type can be listed in the XML Sitemap as links. * * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type * The entity type. * * @return bool * TRUE if the entity type can be used, or FALSE otherwise. */ function xmlsitemap_is_entity_type_supported(EntityTypeInterface $entity_type) { // If the XML Sitemap status in the entity type annotation has been set then // return that first. This will allow modules to bypass the logic below if // needed. $status = $entity_type->get('xmlsitemap'); if ($status !== NULL) { return $status; } // Skip if the entity type is not a content entity type. if (!($entity_type instanceof ContentEntityTypeInterface)) { return FALSE; } // Skip if the entity type is internal (and not considered public). if ($entity_type->isInternal()) { return FALSE; } // Skip if the entity type does not have a canonical URL. if (!$entity_type->hasLinkTemplate('canonical') && !$entity_type->getUriCallback()) { return FALSE; } // Skip if the entity type as a bundle entity type but does not yet have // any bundles created. if ($entity_type->getBundleEntityType() && !\Drupal::service('entity_type.bundle.info')->getBundleInfo($entity_type->id())) { return FALSE; } return TRUE; } /** * Returns information about supported sitemap link types. * * @param mixed $type * (optional) The link type to return information for. If omitted, * information for all link types is returned. * @param mixed $reset * (optional) Boolean whether to reset the static cache and do nothing. Only * used for tests. * * @return array * Info about sitemap link. * * @see hook_xmlsitemap_link_info() * @see hook_xmlsitemap_link_info_alter() */ function xmlsitemap_get_link_info($type = NULL, $reset = FALSE) { $language = \Drupal::languageManager()->getCurrentLanguage(); $link_info = &drupal_static(__FUNCTION__); if ($reset) { $link_info = NULL; \Drupal::service('cache_tags.invalidator')->invalidateTags(['xmlsitemap']); } if (!isset($link_info)) { $cid = 'xmlsitemap:link_info:' . $language->getId(); if ($cache = \Drupal::cache()->get($cid)) { $link_info = $cache->data; } else { $link_info = []; $entity_types = \Drupal::entityTypeManager()->getDefinitions(); foreach ($entity_types as $key => $entity_type) { if (!xmlsitemap_is_entity_type_supported($entity_type)) { continue; } $link_info[$key] = [ 'label' => $entity_type->getLabel(), 'type' => $entity_type->id(), 'base table' => $entity_type->getBaseTable(), 'bundles' => \Drupal::service('entity_type.bundle.info')->getBundleInfo($entity_type->id()), 'bundle label' => $entity_type->getBundleLabel(), 'entity keys' => [ 'id' => $entity_type->getKey('id'), 'bundle' => $entity_type->getKey('bundle'), ], 'xmlsitemap' => [ // Add in the default callbacks for entity types. 'process callback' => $entity_type->get('xmlsitemap')['process callback'] ?? 'xmlsitemap_xmlsitemap_process_entity_links', 'rebuild callback' => $entity_type->get('xmlsitemap')['process callback'] ?? 'xmlsitemap_rebuild_batch_fetch', ], ]; } $link_info = array_merge($link_info, \Drupal::moduleHandler()->invokeAll('xmlsitemap_link_info')); foreach ($link_info as $key => &$info) { $info += [ 'type' => $key, 'base table' => FALSE, 'bundles' => [], ]; foreach ($info['bundles'] as $bundle => &$bundle_info) { $bundle_info += [ 'xmlsitemap' => [], ]; $bundle_info['xmlsitemap'] += xmlsitemap_link_bundle_load($key, $bundle, FALSE); } } \Drupal::moduleHandler()->alter('xmlsitemap_link_info', $link_info); // Sort the entity types by label. uasort($link_info, function ($a, $b) { // Put frontpage first. if ($a['type'] === 'frontpage') { return -1; } if ($b['type'] === 'frontpage') { return 1; } return strnatcmp($a['label'], $b['label']); }); // Cache by language since this info contains translated strings. // Also include entity type tags since this is tied to entity and bundle // information. \Drupal::cache()->set( $cid, $link_info, Cache::PERMANENT, [ 'xmlsitemap', 'entity_types', 'entity_bundles', ] ); } } if (isset($type)) { return isset($link_info[$type]) ? $link_info[$type] : NULL; } return $link_info; } /** * Returns enabled bundles of an entity type. * * @param string $entity_type * Entity type id. * * @return array * Array with entity bundles info. */ function xmlsitemap_get_link_type_enabled_bundles($entity_type) { $bundles = []; $info = xmlsitemap_get_link_info($entity_type); foreach ($info['bundles'] as $bundle => $bundle_info) { $settings = xmlsitemap_link_bundle_load($entity_type, $bundle); if (!empty($settings['status'])) { $bundles[] = $bundle; } } return $bundles; } /** * Returns statistics about specific entity links. * * @param string $entity_type_id * Entity type id. * @param string $bundle * Bundle id. * * @return array * Array with statistics. */ function xmlsitemap_get_link_type_indexed_status($entity_type_id, $bundle = '') { $info = xmlsitemap_get_link_info($entity_type_id); $database = \Drupal::database(); $entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id); $status['indexed'] = $database->query("SELECT COUNT(id) FROM {xmlsitemap} WHERE type = :entity AND subtype = :bundle", [':entity' => $entity_type_id, ':bundle' => $bundle])->fetchField(); $status['visible'] = $database->query("SELECT COUNT(id) FROM {xmlsitemap} WHERE type = :entity AND subtype = :bundle AND status = 1 AND access = 1", [':entity' => $entity_type_id, ':bundle' => $bundle])->fetchField(); try { $query = \Drupal::entityQuery($entity_type_id); if ($bundle && $entity_type->hasKey('bundle')) { $query->condition($entity_type->getKey('bundle'), $bundle); } // We are only using this for totals, so we can skip the access check. $query->accessCheck(FALSE); $query->addTag('xmlsitemap_link_indexed_status'); $status['total'] = $query->count()->execute(); return $status; } catch (\Exception $e) { $status['total'] = 0; } return $status; } /** * Saves xmlsitemap settings for a specific bundle. * * @param string $entity * Entity type id. * @param string $bundle * Bundle id. * @param array $settings * Settings to be saved. * @param bool $update_links * Update bundle links after settings are saved. */ function xmlsitemap_link_bundle_settings_save($entity, $bundle, array $settings, $update_links = TRUE) { if ($update_links) { $old_settings = xmlsitemap_link_bundle_load($entity, $bundle); if ($settings['status'] != $old_settings['status']) { \Drupal::service('xmlsitemap.link_storage')->updateMultiple(['status' => $settings['status']], [ 'type' => $entity, 'subtype' => $bundle, 'status_override' => 0, ]); } if ($settings['priority'] != $old_settings['priority']) { \Drupal::service('xmlsitemap.link_storage')->updateMultiple(['priority' => $settings['priority']], [ 'type' => $entity, 'subtype' => $bundle, 'priority_override' => 0, ]); } } foreach ($settings as $key => $value) { \Drupal::configFactory()->getEditable("xmlsitemap.settings.{$entity}.{$bundle}")->set($key, $value)->save(); } foreach (\Drupal::languageManager()->getLanguages() as $lang) { \Drupal::cache()->delete('xmlsitemap:link_info:' . $lang->getId()); } xmlsitemap_get_link_info(NULL, TRUE); } /** * Renames a bundle. * * @param string $entity * Entity type id. * @param string $bundle_old * Old bundle name. * @param string $bundle_new * New bundle name. */ function xmlsitemap_link_bundle_rename($entity, $bundle_old, $bundle_new) { if ($bundle_old != $bundle_new) { if (!\Drupal::config("xmlsitemap.settings.{$entity}.{$bundle_old}")->isNew()) { $settings = xmlsitemap_link_bundle_load($entity, $bundle_old); \Drupal::configFactory()->getEditable("xmlsitemap.settings.{$entity}.{$bundle_old}")->delete(); xmlsitemap_link_bundle_settings_save($entity, $bundle_new, $settings, FALSE); \Drupal::service('xmlsitemap.link_storage')->updateMultiple(['subtype' => $bundle_new], ['type' => $entity, 'subtype' => $bundle_old]); } } } /** * Loads link bundle info. * * @param string $entity * Entity type id. * @param string $bundle * Bundle info. * @param bool $load_bundle_info * If TRUE, loads bundle info. * * @return array * Info about a bundle. */ function xmlsitemap_link_bundle_load($entity, $bundle, $load_bundle_info = TRUE) { $info = [ 'entity' => $entity, 'bundle' => $bundle, ]; if ($load_bundle_info) { $entity_info = xmlsitemap_get_link_info($entity); if (isset($entity_info['bundles'][$bundle])) { $info['info'] = $entity_info['bundles'][$bundle]; } } $bundle_settings = \Drupal::config("xmlsitemap.settings.{$entity}.{$bundle}")->get(); if ($bundle_settings) { $info += $bundle_settings; } $info += [ 'status' => XMLSITEMAP_STATUS_DEFAULT, 'priority' => XMLSITEMAP_PRIORITY_DEFAULT, 'changefreq' => 0, ]; return $info; } /** * Deletes all links of a specific bundle. * * @param string $entity * Entity type id. * @param string $bundle * Bundle id. * @param bool $delete_links * If TRUE, deletes bundle links from {xmlsitemap} table. */ function xmlsitemap_link_bundle_delete($entity, $bundle, $delete_links = TRUE) { \Drupal::configFactory()->getEditable("xmlsitemap.settings.{$entity}.{$bundle}")->delete(); if ($delete_links) { \Drupal::service('xmlsitemap.link_storage')->deleteMultiple(['type' => $entity, 'subtype' => $bundle]); } xmlsitemap_get_link_info(NULL, TRUE); } /** * Checks access for a bundle. * * @param string $entity * Entity type id. * @param string $bundle * Bundle id. * * @return bool * If TRUE, access is allowed, FALSE otherwise. * * @deprecated in xmlsitemap:8.x-1.1 and is removed from xmlsitemap:2.0.0. * * @see https://www.drupal.org/project/xmlsitemap/issues/3156088 */ function xmlsitemap_link_bundle_access($entity, $bundle = NULL) { @trigger_error(__FUNCTION__ . ' is deprecated in xmlsitemap:8.x-1.1 and will be removed in xmlsitemap:2.0.0. See https://www.drupal.org/project/xmlsitemap/issues/3156088', E_USER_DEPRECATED); return FALSE; } /** * Get path of a bundle. * * @param string $entity * Entity type id. * @param string $bundle * Bundle id. * * @return mixed * Path of bundle, or FALSE if it does not exist. * * @deprecated in xmlsitemap:8.x-1.1 and is removed from xmlsitemap:2.0.0. * * @see https://www.drupal.org/project/xmlsitemap/issues/3156088 */ function xmlsitemap_get_bundle_path($entity, $bundle) { @trigger_error(__FUNCTION__ . ' is deprecated in xmlsitemap:8.x-1.1 and will be removed in xmlsitemap:2.0.0. See https://www.drupal.org/project/xmlsitemap/issues/3156088', E_USER_DEPRECATED); return FALSE; } /** * Implements hook_entity_bundle_rename(). */ function xmlsitemap_entity_bundle_rename($entity_type_id, $bundle_old, $bundle_new) { xmlsitemap_link_bundle_rename($entity_type_id, $bundle_old, $bundle_new); } /** * Implements hook_entity_bundle_delete(). */ function xmlsitemap_entity_bundle_delete($entity_type_id, $bundle) { xmlsitemap_link_bundle_delete($entity_type_id, $bundle, TRUE); } /** * Determine the frequency of updates to a link. * * @param int $interval * An interval value in seconds. * * @return string * A string representing the update frequency according to the sitemaps.org * protocol. */ function xmlsitemap_get_changefreq($interval, bool $translated = TRUE) { if ($interval <= 0 || !is_numeric($interval)) { return FALSE; } foreach (xmlsitemap_get_changefreq_options() as $value => $frequency) { if ($interval <= $value) { return $translated ? $frequency : $frequency->getUntranslatedString(); } } return $translated ? t('never', [], ['context' => 'At no time']) : 'never'; } /** * Get the current number of sitemap chunks. * * @param int $reset * If TRUE, reset number of chunks. * * @static int $chunks * Number of chunks. * * @return int * Number of chunks. */ function xmlsitemap_get_chunk_count($reset = FALSE) { static $chunks; if (!isset($chunks) || $reset) { $count = max(xmlsitemap_get_link_count($reset), 1); $chunks = ceil($count / xmlsitemap_get_chunk_size($reset)); } return $chunks; } /** * Get the current number of sitemap links. * * @param bool $reset * If TRUE, update current number of sitemap links. * * @static int $count * Current number of sitemap links. * * @return int * Returns current number of sitemap links. */ function xmlsitemap_get_link_count($reset = FALSE) { static $count; if (!isset($count) || $reset) { $count = \Drupal::database()->query("SELECT COUNT(id) FROM {xmlsitemap} WHERE access = 1 AND status = 1")->fetchField(); } return $count; } /** * Get the sitemap chunk size. * * This function is useful with the chunk size is set to automatic as it will * calculate the appropriate value. Use this function instead of @code * xmlsitemap_var('chunk_size') @endcode when the actual value is needed. * * @param bool $reset * A boolean to reset the saved, static result. Defaults to FALSE. * * @return int * An integer with the number of links in each sitemap page. */ function xmlsitemap_get_chunk_size($reset = FALSE) { static $size; if (!isset($size) || $reset) { $size = xmlsitemap_var('chunk_size'); if ($size === 'auto') { // Prevent divide by zero. $count = max(xmlsitemap_get_link_count($reset), 1); $size = min(ceil($count / 10000) * 5000, XMLSITEMAP_MAX_SITEMAP_LINKS); } } return $size; } /** * Recalculate the changefreq of a sitemap link. * * @param array $link * A sitemap link array. */ function xmlsitemap_recalculate_changefreq(array &$link) { $time = \Drupal::time()->getRequestTime(); $link['changefreq'] = round((($link['changefreq'] * $link['changecount']) + ($time - $link['lastmod'])) / ($link['changecount'] + 1)); $link['changecount']++; $link['lastmod'] = $time; } /** * Calculates the average interval between UNIX timestamps. * * @param array $timestamps * An array of UNIX timestamp integers. * * @return int * An integer of the average interval. */ function xmlsitemap_calculate_changefreq(array $timestamps) { sort($timestamps); $count = count($timestamps) - 1; $diff = 0; for ($i = 0; $i < $count; $i++) { $diff += $timestamps[$i + 1] - $timestamps[$i]; } return $count > 0 ? round($diff / $count) : 0; } /** * Submit handler; Set the regenerate needed flag if variables have changed. * * This function needs to be called before system_settings_form_submit() or any * calls to variable_set(). */ function xmlsitemap_form_submit_flag_regenerate(array $form, FormStateInterface $form_state) { $values = $form_state->getValues(); foreach ($values as $variable => $value) { if (\Drupal::config('xmlsitemap.settings')->get($variable) == NULL) { $stored_value = 'not_a_variable'; } else { $stored_value = \Drupal::config('xmlsitemap.settings')->get($variable); } if (is_array($value) && !$form_state->isValueEmpty('array_filter')) { $value = array_keys(array_filter($value)); } if ($stored_value != 'not_a_variable' && $stored_value != $value) { \Drupal::state()->set('xmlsitemap_regenerate_needed', TRUE); \Drupal::messenger()->addWarning(t('XML Sitemap settings have been modified and the files should be regenerated. You can run cron manually to regenerate the cached files.', [ '@run-cron' => Url::fromRoute('system.run_cron', [], ['query' => \Drupal::destination()->getAsArray()])->toString(), ]), FALSE); return; } } } /** * Add a link's XML Sitemap options to the link's form. * * @param array $form * Form array. * @param string $entity_type * Entity type id. * @param string $bundle * Bundle id. * @param int $id * Entity id. * * @todo Add changefreq overridability. */ function xmlsitemap_add_form_link_options(array &$form, $entity_type, $bundle, $id) { if (!$link = \Drupal::service('xmlsitemap.link_storage')->load($entity_type, $id)) { $link = []; } $bundle_info = xmlsitemap_link_bundle_load($entity_type, $bundle); $link += [ 'access' => 1, 'status' => $bundle_info['status'], 'status_default' => $bundle_info['status'], 'status_override' => 0, 'priority' => $bundle_info['priority'], 'priority_default' => $bundle_info['priority'], 'priority_override' => 0, 'changefreq' => $bundle_info['changefreq'], ]; $currentUser = \Drupal::currentUser(); $admin_permission = \Drupal::entityTypeManager()->getDefinition($entity_type)->getAdminPermission(); $form['xmlsitemap'] = [ '#type' => 'details', '#tree' => TRUE, '#title' => t('XML Sitemap'), '#collapsible' => TRUE, '#collapsed' => !$link['status_override'] && !$link['priority_override'], '#access' => $currentUser->hasPermission('administer xmlsitemap') || ($admin_permission && $currentUser->hasPermission($admin_permission)), '#group' => 'advanced', ]; // Show a warning if the link is not accessible and will not be included in // the sitemap. if ($id && !$link['access']) { $form['xmlsitemap']['warning'] = [ '#type' => 'markup', '#prefix' => '', '#suffix' => '
', '#value' => ('This item is not currently visible to anonymous users, so it will not be included in the sitemap.'), ]; } // Status field (inclusion/exclusion) $form['xmlsitemap']['status'] = [ '#type' => 'select', '#title' => t('Inclusion'), '#options' => xmlsitemap_get_status_options($link['status_default']), '#default_value' => $link['status_override'] ? $link['status'] : 'default', ]; $form['xmlsitemap']['status_default'] = [ '#type' => 'value', '#value' => $link['status_default'], ]; $form['xmlsitemap']['status_override'] = [ '#type' => 'value', '#value' => $link['status_override'], ]; // Priority field. $form['xmlsitemap']['priority'] = [ '#type' => 'select', '#title' => t('Priority'), '#options' => xmlsitemap_get_priority_options($link['priority_default']), '#default_value' => $link['priority_override'] ? number_format($link['priority'], 1) : 'default', '#description' => t('The priority of this URL relative to other URLs on your site.'), '#states' => [ 'invisible' => [ 'select[name="xmlsitemap[status]"]' => ['value' => '0'], ], ], ]; $form['xmlsitemap']['changefreq'] = [ '#type' => 'select', '#title' => t('Change frequency'), '#options' => xmlsitemap_get_changefreq_options(), '#default_value' => $link['changefreq'], '#description' => t('Select the frequency of changes.'), '#states' => [ 'invisible' => [ 'select[name="xmlsitemap[status]"]' => ['value' => '0'], ], ], ]; if (!$link['status_default']) { // If the default status is excluded, add a visible state on the include // override option. $form['xmlsitemap']['priority']['#states']['visible'] = [ 'select[name="xmlsitemap[status]"]' => ['value' => '1'], ]; } $form['xmlsitemap']['priority_default'] = [ '#type' => 'value', '#value' => $link['priority_default'], ]; $form['xmlsitemap']['priority_override'] = [ '#type' => 'value', '#value' => $link['priority_override'], ]; array_unshift($form['actions']['submit']['#submit'], 'xmlsitemap_process_form_link_options'); if (!empty($form['actions']['publish']['#submit'])) { array_unshift($form['actions']['publish']['#submit'], 'xmlsitemap_process_form_link_options'); } } /** * Submit callback for the entity form to save. */ function xmlsitemap_process_form_link_options(array $form, FormStateInterface $form_state) { $link = $form_state->getValue('xmlsitemap'); $fields = ['status' => XMLSITEMAP_STATUS_DEFAULT, 'priority' => XMLSITEMAP_PRIORITY_DEFAULT]; foreach ($fields as $field => $default) { if ($link[$field] === 'default') { $link[$field] = isset($link[$field . '_default']) ? $link[$field . '_default'] : $default; $link[$field . '_override'] = 0; } else { $link[$field . '_override'] = 1; } } $form_state->setValue('xmlsitemap', $link); $entity = $form_state->getFormObject()->getEntity(); $entity->xmlsitemap = $form_state->getValue('xmlsitemap'); } /** * Submit callback for link bundle settings. */ function xmlsitemap_link_bundle_settings_form_submit($form, &$form_state) { $entity = $form['xmlsitemap']['#entity']; $bundle = $form['xmlsitemap']['#bundle']; // Handle new bundles by fetching the proper bundle key value from the form // state values. if (empty($bundle)) { $entity_info = $form['xmlsitemap']['#entity_info']; if (isset($entity_info['bundle keys']['bundle'])) { $bundle_key = $entity_info['bundle keys']['bundle']; if ($form_state->hasValue($bundle_key)) { $bundle = $form_state->getValue($bundle_key); $form['xmlsitemap']['#bundle'] = $bundle; } } } xmlsitemap_link_bundle_settings_save($entity, $bundle, $form_state->getValue('xmlsitemap')); $entity_info = $form['xmlsitemap']['#entity_info']; if (!empty($form['xmlsitemap']['#show_message'])) { \Drupal::messenger()->addStatus(t('XML Sitemap settings for the @bundle-label %bundle have been saved.', ['@bundle-label' => mb_strtolower($entity_info['bundle label']), '%bundle' => $entity_info['bundles'][$bundle]['label']])); } // Unset the form values since we have already saved the bundle settings and // we don't want these values to get saved as variables in-case this form // also uses system_settings_form(). $form_state->unsetValue('xmlsitemap'); } /** * Gets xmlsitemap frequency options. * * @return \Drupal\Core\StringTranslation\TranslatableMarkup[] * Frequency options. */ function xmlsitemap_get_changefreq_options() { return [ XMLSITEMAP_FREQUENCY_ALWAYS => t('always', [], ['context' => 'At all times']), XMLSITEMAP_FREQUENCY_HOURLY => t('hourly'), XMLSITEMAP_FREQUENCY_DAILY => t('daily'), XMLSITEMAP_FREQUENCY_WEEKLY => t('weekly'), XMLSITEMAP_FREQUENCY_MONTHLY => t('monthly'), XMLSITEMAP_FREQUENCY_YEARLY => t('yearly'), ]; } /** * Load a language object by its language code. * * @param string $language * A language code. If not provided the default language will be returned. * * @return \Drupal\core\Language\LanguageInterface * A language object. * * @todo Remove when https://www.drupal.org/node/660736 is fixed in Drupal core. */ function xmlsitemap_language_load($language = LanguageInterface::LANGCODE_NOT_SPECIFIED) { $languages = &drupal_static(__FUNCTION__); if (!isset($languages)) { $languages = \Drupal::languageManager()->getLanguages(); $languages[LanguageInterface::LANGCODE_NOT_SPECIFIED] = NULL; } return isset($languages[$language]) ? $languages[$language] : NULL; } /** * @defgroup xmlsitemap_context_api XML Sitemap API for sitemap contexts. * @{ */ /** * Gets info about a context. * * @param string $context * The context. * @param bool $reset * If TRUE, resets context info. * * @return array * Array with info. */ function xmlsitemap_get_context_info($context = NULL, $reset = FALSE) { $language = \Drupal::languageManager()->getCurrentLanguage(); $info = &drupal_static(__FUNCTION__); if ($reset) { $info = NULL; } elseif ($cached = \Drupal::cache()->get('xmlsitemap:context_info:' . $language->getId())) { $info = $cached->data; } if (!isset($info)) { $info = \Drupal::moduleHandler()->invokeAll('xmlsitemap_context_info'); \Drupal::moduleHandler()->alter('xmlsitemap_context_info', $info); ksort($info); // Cache by language since this info contains translated strings. \Drupal::cache()->set('xmlsitemap:context_info:' . $language->getId(), $info, Cache::PERMANENT, ['xmlsitemap']); } if (isset($context)) { return isset($info[$context]) ? $info[$context] : NULL; } return $info; } /** * Get the sitemap context of the current request. * * @return array * Current context. */ function xmlsitemap_get_current_context() { $context = &drupal_static(__FUNCTION__); if (!isset($context)) { $context = \Drupal::moduleHandler()->invokeAll('xmlsitemap_context'); \Drupal::moduleHandler()->alter('xmlsitemap_context', $context); ksort($context); } return $context; } /** * Gets summary about a context. * * @param \Drupal\xmlsitemap\XmlSitemapInterface $sitemap * Sitemap entity. * @param string $context_key * Key for the context. * @param array $context_info * Info about the context. * * @return string * Context summary. */ function _xmlsitemap_sitemap_context_summary(XmlSitemapInterface $sitemap, $context_key, array $context_info) { $context_value = isset($sitemap->context[$context_key]) ? $sitemap->context[$context_key] : NULL; if (!isset($context_value)) { return t('Default'); } elseif (!empty($context_info['summary callback'])) { return $context_info['summary callback']($context_value); } else { return $context_value; } } /** * @} End of "defgroup xmlsitemap_context_api" */ /** * Run a not-progressive batch operation. */ function xmlsitemap_run_unprogressive_batch() { $batch = batch_get(); $lock = \Drupal::lock(); if (!empty($batch)) { // If there is already something in the batch, don't run. return FALSE; } $args = func_get_args(); $batch_callback = array_shift($args); if (!$lock->acquire($batch_callback)) { return FALSE; } // Attempt to increase the execution time. Environment::setTimeLimit(240); // Build the batch array. $batch = call_user_func_array($batch_callback, $args); batch_set($batch); // We need to manually set the progressive variable again. // @todo Remove when https://www.drupal.org/node/638712 is fixed. $batch = & batch_get(); $batch['progressive'] = FALSE; // Run the batch process. if (PHP_SAPI === 'cli' && function_exists('drush_backend_batch_process')) { drush_backend_batch_process(); } else { batch_process(); } $lock->release($batch_callback); return TRUE; } /** * Gets a link from url. * * @param string $url * Url of the link. * @param array $options * Extra options of the url such as 'query'. * * @static string $destination * Destination option. * * @return array * An array representing a link. */ function xmlsitemap_get_operation_link($url, array $options = []) { static $destination; if (!isset($destination)) { $destination = \Drupal::destination()->getAsArray(); } $link = ['href' => $url] + $options; $link += ['query' => $destination]; return $link; } /** * Returns HTML for an administration settings table. * * @param array $variables * An associative array containing: * - build: A render element representing a table of bundle content language * settings for a particular entity type. * * @return string * HTML content. * * @ingroup themable */ function theme_xmlsitemap_content_settings_table(array $variables) { return 'Thank you for helping test the XML Sitemap module rewrite. Please consider helping offset developer free time by donating or if your company is interested in sponsoring the rewrite or a specific feature, please contact the developer. Thank you to the following current sponsors: ' . implode(', ', $sponsors) . ', and all the individuals that have donated. This message will not be seen in the stable versions.