diff --git a/config/schema/simple_sitemap.schema.yml b/config/schema/simple_sitemap.schema.yml index 1885b41..4f62926 100644 --- a/config/schema/simple_sitemap.schema.yml +++ b/config/schema/simple_sitemap.schema.yml @@ -67,6 +67,11 @@ simple_sitemap.bundle_settings.*.*.*: include_images: label: 'Include images' type: boolean + file_mimetypes: + label: 'File MIME types' + type: sequence + sequence: + type: string simple_sitemap.custom_links.*: label: 'Custom links' diff --git a/simple_sitemap.install b/simple_sitemap.install index b59bd8a..903a2d8 100644 --- a/simple_sitemap.install +++ b/simple_sitemap.install @@ -798,16 +798,20 @@ function simple_sitemap_update_8304() { * Add the link_count field to simple_sitemap table. */ function simple_sitemap_update_8305() { - \Drupal::database()->schema()->addField( - 'simple_sitemap', - 'link_count', [ - 'description' => 'The number of links in the sitemap.', - 'type' => 'int', - 'not null' => TRUE, - 'unsigned' => TRUE, - 'default' => 0, - ] - ); + $database = \Drupal::database(); + if (!$database->schema()->fieldExists('simple_sitemap', 'link_count')) { + $database->schema()->addField( + 'simple_sitemap', + 'link_count', [ + 'description' => 'The number of links in the sitemap.', + 'type' => 'int', + 'not null' => TRUE, + 'unsigned' => TRUE, + 'default' => 0, + + ] + ); + } } /** @@ -945,3 +949,41 @@ function simple_sitemap_update_8408() { return t('The sitemaps need to be regenerated.'); } + +/** + * Adding file MIME type inclusion setting to all existing bundle and entity instance settings. + */ +function simple_sitemap_update_8409() { + // Execute the loos update because of preview's patch. + simple_sitemap_update_8305(); + + // Update existing bundle settings. + $config_factory = \Drupal::service('config.factory'); + $all_bundle_settings = $config_factory->listAll('simple_sitemap.bundle_settings.'); + + foreach ($all_bundle_settings as $bundle_settings) { + $config = $config_factory->get($bundle_settings)->get(); + if (!isset($config['file_mimetypes'])) { + $config_factory->getEditable($bundle_settings) + ->setData($config + ['file_mimetypes' => []]) + ->save(); + } + } + + // Update existing entity override data. + $results = \Drupal::database()->select('simple_sitemap_entity_overrides', 'o') + ->fields('o', ['id', 'inclusion_settings']) + ->execute()->fetchAll(\PDO::FETCH_OBJ); + + foreach ($results as $row) { + $settings = unserialize($row->inclusion_settings); + if (!isset($settings['file_mimetypes'])) { + \Drupal::database()->update('simple_sitemap_entity_overrides') + ->fields(['inclusion_settings' => serialize($settings + ['file_mimetypes' => []])]) + ->condition('id', $row->id) + ->execute(); + } + } + + return t('You may now want to configure your XML sitemap entities to specific file MIME types.'); +} diff --git a/simple_sitemap.services.yml b/simple_sitemap.services.yml index 38a25e1..d55eea4 100644 --- a/simple_sitemap.services.yml +++ b/simple_sitemap.services.yml @@ -66,6 +66,12 @@ services: - '@entity_type.bundle.info' - '@config.factory' + simple_sitemap.mimetypes_helper: + class: Drupal\simple_sitemap\MimeTypesHelper + public: true + arguments: + - '@module_handler' + simple_sitemap.form_helper: class: Drupal\simple_sitemap\Form\FormHelper public: true @@ -73,6 +79,7 @@ services: - '@simple_sitemap.generator' - '@simple_sitemap.settings' - '@simple_sitemap.entity_helper' + - '@simple_sitemap.mimetypes_helper' - '@current_user' - '@class_resolver' diff --git a/src/Entity/EntityHelper.php b/src/Entity/EntityHelper.php index 609f834..977c2f4 100644 --- a/src/Entity/EntityHelper.php +++ b/src/Entity/EntityHelper.php @@ -11,6 +11,7 @@ use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Url; use Drupal\system\Entity\Menu; +use Drupal\file\FileInterface; /** * Helper class for working with entities. @@ -163,7 +164,15 @@ class EntityHelper { * TRUE if entity type is supported, FALSE if not. */ public function supports(EntityTypeInterface $entity_type): bool { - return $entity_type instanceof ContentEntityTypeInterface && $entity_type->hasLinkTemplate('canonical'); + if ( + !$entity_type instanceof ContentEntityTypeInterface + || !method_exists($entity_type, 'getBundleEntityType') + || (!$entity_type->hasLinkTemplate('canonical') && $entity_type instanceof FileInterface) + ) { + return FALSE; + } + + return TRUE; } /** diff --git a/src/Entity/SimpleSitemap.php b/src/Entity/SimpleSitemap.php index 2f2d8f7..21ba38d 100644 --- a/src/Entity/SimpleSitemap.php +++ b/src/Entity/SimpleSitemap.php @@ -367,4 +367,28 @@ class SimpleSitemap extends ConfigEntityBase implements SimpleSitemapInterface { \Drupal::entityTypeManager()->getStorage('simple_sitemap')->purgeContent($variants, $status); } + /** + * Gets file MIME types values formatted. + * + * @param array $mimetypes + * The mimetypes configured. + * + * @return array + * The mimetypes formatted values. + */ + public function getFileMimetypes(array $mimetypes) { + $mimetypes = array_values($mimetypes); + + $values = []; + foreach ($mimetypes as $mimetype) { + if (empty($mimetype)) { + continue; + } + + $values[] = $mimetype; + } + + return $values; + } + } diff --git a/src/Form/FormHelper.php b/src/Form/FormHelper.php index d143e88..166e64e 100644 --- a/src/Form/FormHelper.php +++ b/src/Form/FormHelper.php @@ -10,12 +10,14 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\simple_sitemap\Entity\EntityHelper; +use Drupal\simple_sitemap\MimetypesHelper; use Drupal\simple_sitemap\Form\Handler\EntityFormHandlerInterface; use Drupal\simple_sitemap\Form\Handler\BundleEntityFormHandler; use Drupal\simple_sitemap\Form\Handler\EntityFormHandler; use Drupal\simple_sitemap\Manager\Generator; use Drupal\Core\Session\AccountProxyInterface; use Drupal\simple_sitemap\Settings; +use Drupal\media\Entity\MediaType; /** * Helper class for working with forms. @@ -45,6 +47,11 @@ class FormHelper { */ protected $entityHelper; + /** + * @var \Drupal\simple_sitemap\MimetypesHelper + */ + protected $mimetypesHelper; + /** * Proxy for the current user account. * @@ -82,6 +89,8 @@ class FormHelper { * The simple_sitemap.settings service. * @param \Drupal\simple_sitemap\Entity\EntityHelper $entity_helper * Helper class for working with entities. + * @param \Drupal\simple_sitemap\MimetypesHelper $mimetypesHelper + * Mimetype helper. * @param \Drupal\Core\Session\AccountProxyInterface $current_user * Proxy for the current user account. * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver @@ -91,12 +100,14 @@ class FormHelper { Generator $generator, Settings $settings, EntityHelper $entity_helper, + MimetypesHelper $mimetypesHelper, AccountProxyInterface $current_user, ClassResolverInterface $class_resolver ) { $this->generator = $generator; $this->settings = $settings; $this->entityHelper = $entity_helper; + $this->mimetypesHelper = $mimetypesHelper; $this->currentUser = $current_user; $this->classResolver = $class_resolver; } @@ -413,4 +424,181 @@ class FormHelper { return [0 => t('On every cron run')] + $intervals; } + /** + * Gets the file MIME type field configuration. + * + * @param array $default_value + * The default value configured. + * + * @return array + * File MIME type field config. e.g ['default_value' => [], 'options' => []] + */ + public function getFileMimeTypesFieldConfig(array $default_value) { + $field_config['default_value'] = $default_value; + $field_config['options'] = $this->getFileMimeTypesSelectOptions(); + + // Define default_value the same as options when it's empty + // and there is not - All types - option. + if (empty($field_config['default_value'])) { + if (!isset($field_config['options'][''])) { + $field_config['default_value'] = $field_config['options']; + } else { + $field_config['default_value'] = ['']; + } + } + + return $field_config; + } + + /** + * Gets the values needed to display the file MIME types dropdown setting. + * + * @return array + * File MIME types select value. + */ + private function getFileMimeTypesSelectOptions() { + + return $this->getFileMimeTypesFileOptions(); + } + + /** + * Gets the file MIME types options. + * + * @return array + * File file MIME types options. + */ + private function getFileMimeTypesFileOptions() { + + return $this->filterFileMimeTypesOptions(); + } + + /** + * Gets the media MIME types options. + * + * @return array + * Media MIME types options. + */ + private function getFileMimeTypesMediaOptions() { + /** @var \Drupal\media\Entity\MediaType $entity */ + $entity = $this->getFormEntity(); + + if (!$entity instanceof MediaType) { + return $this->filterFileMimeTypesOptions(); + } + + if (empty($entity->get('source'))) { + return $this->filterFileMimeTypesOptions(); + } + + /** @var \Drupal\media\MediaSourceInterface $source */ + $source = $entity->getSource(); + $field_definition = $source->getSourceFieldDefinition($entity); + + // Get all the options when is creating a Media Type. + if (empty($field_definition)) { + return $this->filterFileMimeTypesOptions(); + } + + $file_extensions_allowed = $field_definition->getSetting('file_extensions'); + $file_extensions_allowed = explode(' ', $file_extensions_allowed); + + return $this->filterFileMimeTypesOptions($file_extensions_allowed); + } + + /** + * Gets all file MIME types options. + * + * @return array + * All the file MIME types options supported. + * + * @todo Drupal 9.x will replace how mimetypes are mapped. + * See https://www.drupal.org/project/drupal/issues/2311679 + */ + private function getAllFileMimeTypesOptions() { + return $this->mimetypesHelper->get(); + } + + /** + * Filter the file MIME types based on allowed types. + * + * @param array $types_allowed + * The file types allowed. + * @param bool $by_extension + * Filter options by extension. + * + * @return array + * The options allowed. + */ + private function filterFileMimeTypesOptions(array $types_allowed = [], $by_extension = TRUE) { + $all_file_types = $this->getAllFileMimeTypesOptions(); + + if (empty($types_allowed)) { + $options = ['' => $this->t('- All types -')]; + $options += array_combine($all_file_types['mimetypes'], $all_file_types['mimetypes']); + return $options; + } + + if ($by_extension) { + return $this->filterFileMimeTypesOptionsByExtension($all_file_types, $types_allowed); + } + + return $this->filterFileMimeTypesOptionsByMimeType($all_file_types, $types_allowed); + } + + /** + * Filter the file MIME types based on allowed extensions. + * + * @param array $all_file_types + * All the file types available. + * @param array $types_allowed + * The file types allowed. + * + * @return array + * Options filtered by allowed extension. + */ + private function filterFileMimeTypesOptionsByExtension(array $all_file_types, array $types_allowed) { + $options = []; + + foreach ($all_file_types['extensions'] as $extension => $mimetype_id) { + if (!in_array($extension, $types_allowed)) { + continue; + } + + $mimetype_name = $all_file_types['mimetypes'][$mimetype_id]; + + if (in_array($extension, $options)) { + continue; + } + + $options[$mimetype_name] = $mimetype_name; + } + + return $options; + } + + /** + * Filter the file MIME types based on allowed mimetypes. + * + * @param array $all_file_types + * All the file types available. + * @param array $types_allowed + * The file types allowed. + * + * @return array + * Options filtered by allowed mimetypes. + */ + private function filterFileMimeTypesOptionsByMimeType(array $all_file_types, array $types_allowed) { + $options = []; + + foreach ($all_file_types['mimetypes'] as $mimetype) { + if (!in_array($mimetype, $types_allowed)) { + continue; + } + + $options[$mimetype] = $mimetype; + } + + return $options; + } + } diff --git a/src/Form/Handler/BundleEntityFormHandler.php b/src/Form/Handler/BundleEntityFormHandler.php index 0c68f07..40ac3b6 100644 --- a/src/Form/Handler/BundleEntityFormHandler.php +++ b/src/Form/Handler/BundleEntityFormHandler.php @@ -41,6 +41,24 @@ class BundleEntityFormHandler extends EntityFormHandlerBase { $variant_form['priority']['#description'] = $this->t('The priority entities of this type will have in the eyes of search engine bots.'); $variant_form['changefreq']['#description'] = $this->t('The frequency with which entities of this type change. Search engine bots may take this as an indication of how often to index them.'); $variant_form['include_images']['#description'] = $this->t('Determines if images referenced by entities of this type should be included in the sitemap.'); + + // File MIME types. + // Only add configurable field to bundle category. + if (in_array($this->entityTypeId, ['media', 'file'])) { + $file_mimetypes_field_config = $this->formHelper->getFileMimeTypesFieldConfig( + isset($this->settings[$variant]['file_mimetypes']) ? $this->settings[$variant]['file_mimetypes'] : [] + ); + + $variant_form['file_mimetypes'] = [ + '#type' => 'select', + '#title' => $this->t('File MIME types'), + '#description' => $this->t('Determines which file MIME type referenced by this @bundle entity should be included in the sitemap.', ['@bundle' => $this->bundleName]), + '#default_value' => $file_mimetypes_field_config['default_value'], + '#options' => $file_mimetypes_field_config['options'], + '#multiple' => TRUE, + ]; + } + } return $form; } diff --git a/src/Manager/EntityManager.php b/src/Manager/EntityManager.php index a4d230f..6638b06 100644 --- a/src/Manager/EntityManager.php +++ b/src/Manager/EntityManager.php @@ -29,6 +29,7 @@ class EntityManager implements SitemapGetterInterface { 'priority' => '0.5', 'changefreq' => '', 'include_images' => FALSE, + 'file_mimetypes' => [], ]; /** diff --git a/src/MimeTypesHelper.php b/src/MimeTypesHelper.php new file mode 100644 index 0000000..78cd3b3 --- /dev/null +++ b/src/MimeTypesHelper.php @@ -0,0 +1,32 @@ +mapping === NULL) { + $mapping = $this->defaultMapping; + // Allow modules to alter the default mapping. + $this->moduleHandler->alter('file_mimetype_mapping', $mapping); + $this->mapping = $mapping; + } + + return $this->mapping; + } + +} diff --git a/src/Plugin/simple_sitemap/UrlGenerator/EntityFileUrlGenerator.php b/src/Plugin/simple_sitemap/UrlGenerator/EntityFileUrlGenerator.php new file mode 100644 index 0000000..6d2efc9 --- /dev/null +++ b/src/Plugin/simple_sitemap/UrlGenerator/EntityFileUrlGenerator.php @@ -0,0 +1,306 @@ +entityHelper = $entity_helper; + $this->entityTypeManager = $entity_type_manager; + $this->anonUser = new AnonymousUserSession(); + $this->connection = $connection; + $this->fileSystem = $file_system; + $this->entityManager = $entity_manager; + } + + /** + * {@inheritDoc} + */ + public static function create( + ContainerInterface $container, + array $configuration, + $plugin_id, + $plugin_definition + ): SimpleSitemapPluginBase { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('simple_sitemap.logger'), + $container->get('simple_sitemap.settings'), + $container->get('simple_sitemap.entity_helper'), + $container->get('entity_type.manager'), + $container->get('database'), + $container->get('file_system'), + $container->get('simple_sitemap.entity_manager') + ); + } + + /** + * {@inheritDoc} + */ + public function getDataSets():array { + $sitemap_entity_types = $this->entityHelper->getSupportedEntityTypes(); + + if (!isset($sitemap_entity_types['file'])) { + return []; + } + + $file_entity_keys = $sitemap_entity_types['file']->getKeys(); + + $bundles = $this->entityManager->setVariants($this->sitemap->id())->getAllBundleSettings(); + + // Get bundles for the sitemap id;. + $bundles = $bundles[$this->sitemap->id()] ?? $bundles[$this->sitemap->id()]; + + $data_sets = []; + + foreach (self::OVERRIDES_ENTITY_TYPE as $entity_type_name) { + if (!isset($sitemap_entity_types[$entity_type_name])) { + continue; + } + + if (!isset($bundles[$entity_type_name])) { + continue; + } + + foreach ($bundles[$entity_type_name] as $bundle_name => $bundle_settings) { + // Skip bundle if it will be generated in a different sitemap variant. + if ( + NULL !== $this->sitemap->id() && isset($bundle_settings['variant']) + && $bundle_settings['variant'] !== $this->sitemap->id() + ) { + $bundle_settings['index'] = FALSE; + } + unset($bundle_settings['variant']); + + if (empty($bundle_settings['index'])) { + continue; + } + + $query = $this->connection->select('file_managed'); + $query->fields('file_managed', ['fid', 'uri']); + + // Only load files configured. + if (!empty($file_entity_keys['bundle'])) { + $query->condition($file_entity_keys['bundle'], $bundle_name); + } + if (!empty($file_entity_keys['status'])) { + $query->condition($file_entity_keys['status'], 1); + } + if (isset($bundle_settings['file_mimetypes']) && !empty($bundle_settings['file_mimetypes'])) { + $query->condition('filemime', $bundle_settings['file_mimetypes'], 'IN'); + } + + foreach ($query->execute() as $file) { + if (in_array($file->fid, array_column($data_sets, 'id'))) { + continue; + } + + // Condiders only existing files. + $file_path = $this->fileSystem->realpath($file->uri); + + if (!$file_path || !file_exists($file_path)) { + continue; + } + + $data_sets[] = [ + 'entity_type' => 'file', + 'id' => $file->fid, + ]; + } + } + } + + return $data_sets; + } + + /** + * {@inheritDoc} + */ + protected function processDataSet($data_set):array { + /** @var \Drupal\file\Entity\File $entity */ + if (empty($entity = $this->entityTypeManager->getStorage($data_set['entity_type'])->load($data_set['id']))) { + return []; + } + + if (!($entity instanceof FileInterface)) { + return []; + } + + $entity_id = $entity->id(); + $entity_type_name = $entity->getEntityTypeId(); + + $entity_settings = $this->entityManager + ->setVariants($this->sitemap->id()) + ->getEntityInstanceSettings($entity_type_name, $entity_id); + + // Get settings for the sitemap id;. + $entity_settings = $entity_settings[$this->sitemap->id()] ?? $entity_settings[$this->sitemap->id()]; + + if (empty($entity_settings['index'])) { + return []; + } + + if (!$entity->access('download', $this->anonUser)) { + return []; + } + + return [ + 'url' => $this->replaceBaseUrlWithCustom($entity->createFileUrl(FALSE)), + 'lastmod' => method_exists($entity, 'getChangedTime') ? date('c', $entity->getChangedTime()) : NULL, + 'priority' => isset($entity_settings['priority']) ? $entity_settings['priority'] : NULL, + 'changefreq' => !empty($entity_settings['changefreq']) ? $entity_settings['changefreq'] : NULL, + + // Additional info useful in hooks. + 'meta' => [ + 'path' => $entity->getFileUri(), + 'entity_info' => [ + 'entity_type' => $entity_type_name, + 'id' => $entity_id, + ], + ], + ]; + } + +} diff --git a/src/Plugin/simple_sitemap/UrlGenerator/EntityUrlGenerator.php b/src/Plugin/simple_sitemap/UrlGenerator/EntityUrlGenerator.php index 91a255b..a35e779 100644 --- a/src/Plugin/simple_sitemap/UrlGenerator/EntityUrlGenerator.php +++ b/src/Plugin/simple_sitemap/UrlGenerator/EntityUrlGenerator.php @@ -14,6 +14,7 @@ use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\simple_sitemap\Settings; use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\file\Entity\File; /** * Provides the entity URL generator. @@ -260,7 +261,15 @@ class EntityUrlGenerator extends EntityUrlGeneratorBase { } $entity_settings = $entity_settings[$this->sitemap->id()]; - $url_object = $entity->toUrl()->setAbsolute(); + + try { + $url_object = $entity->toUrl()->setAbsolute(); + } + catch (\Exception $e) { + if ($entity instanceof File) { + $url_object = Url::fromUri($entity->createFileUrl(FALSE)); + } + } // Do not include external paths. if (!$url_object->isRouted()) {