Commit 3442174e authored by Pawel G's avatar Pawel G
Browse files

Making sitemap work with potentially all content entities and removing plugin architecture.

parent 4ce4b620
......@@ -23,9 +23,8 @@ content:
* menu links
* users
* custom links
The above functionalities are implemented as Drupal 8 plugins and it is easy to
add support to custom entity types through implementing your own plugins.
* commerce products
* possibly other contributed entities
To learn about XML sitemaps, see https://en.wikipedia.org/wiki/Sitemaps.
......@@ -57,7 +56,7 @@ https://en.wikipedia.org/wiki/Sitemaps to learn more about this parameter.
Inclusion and priority settings of bundles can be overridden on a per-entity
basis. Just head over to a bundle instance edit form (node/1/edit) and override
the bundle settings there. ATM this only works for nodes and taxonomy terms.
the bundle settings there.
If you wish for the sitemap to reflect the new configuration instantly, check
'Regenerate sitemap after clicking save'. This setting only appears if a change
......@@ -94,18 +93,6 @@ It is possible to hook into link generation by implementing
hook_simple_sitemap_links_alter(&$links){} in a custom module and altering the
link array.
Need to support a non-core entity type? In case you would like to support an
entity type provided by a contrib module:
* Create a feature request in simple_sitemap's issue queue and submit a plugin
(as patch), or wait until a patch is submitted by the community or a
maintainer.
* As soon as a patch is found working, create an issue in the contrib module's
queue and submit a slightly altered patch created against that module.
* Link the contrib module's issue on the simple_sitemap issue created earlier.
HOW CAN YOU CONTRIBUTE?
-----------------------
......
......@@ -28,7 +28,7 @@ function simple_sitemap_form_alter(&$form, $form_state, $form_id) {
$f = new Form($form, $form_state, $form_id);
// Do not alter the form if it is irrelevant to sitemap generation.
if (empty($f->entityType))
if (empty($f->entityCategory))
return;
$sitemap = \Drupal::service('simple_sitemap.generator');
......@@ -37,7 +37,7 @@ function simple_sitemap_form_alter(&$form, $form_state, $form_id) {
$entity_types = $sitemap->getConfig('entity_types');
// Do not alter the form, if sitemap is disabled for entity type.
if ($f->entityType == 'bundle_instance' && empty($entity_types[$f->entityTypeId][$f->bundleName]['index']))
if ($f->entityCategory == 'instance' && empty($entity_types[$f->entityTypeId][$f->bundleName]['index']))
return;
// Setting default form values.
......@@ -51,10 +51,10 @@ function simple_sitemap_form_alter(&$form, $form_state, $form_id) {
}
// Overwriting defaults if settings found for entity.
if ($f->entityType == 'bundle_instance') {
if (isset($entity_types[$f->entityTypeId][$f->bundleName]['entities'][$f->entityId]['index'])) {
$index = $entity_types[$f->entityTypeId][$f->bundleName]['entities'][$f->entityId]['index'];
$priority = $entity_types[$f->entityTypeId][$f->bundleName]['entities'][$f->entityId]['priority'];
if ($f->entityCategory == 'instance') {
if (isset($entity_types[$f->entityTypeId][$f->bundleName]['entities'][$f->instanceId]['index'])) {
$index = $entity_types[$f->entityTypeId][$f->bundleName]['entities'][$f->instanceId]['index'];
$priority = $entity_types[$f->entityTypeId][$f->bundleName]['entities'][$f->instanceId]['priority'];
}
}
......@@ -62,7 +62,7 @@ function simple_sitemap_form_alter(&$form, $form_state, $form_id) {
'#type' => 'details',
'#group' => isset($form['additional_settings']) ? 'additional_settings' : 'advanced',
'#title' => t('Simple XML sitemap'),
'#description' => $f->entityType == 'bundle_instance' ? t('Settings for this specific entity can be overridden here.') : '',
'#description' => $f->entityCategory == 'instance' ? t('Settings for this specific entity can be overridden here.') : '',
);
// Attach some js magic to forms.
......@@ -76,22 +76,22 @@ function simple_sitemap_form_alter(&$form, $form_state, $form_id) {
'#type' => 'radios',
'#default_value' => $index,
'#options' => [
0 => $f->entityType == 'bundle_instance' ? t('Do not index this entity') : t('Do not index entities of this type'),
1 => $f->entityType == 'bundle_instance' ? t('Index this entity') : t('Index entities of this type'),
0 => $f->entityCategory == 'instance' ? t('Do not index this entity') : t('Do not index entities of this type'),
1 => $f->entityCategory == 'instance' ? t('Index this entity') : t('Index entities of this type'),
]
);
if ($f->entityType == 'bundle_instance' && isset($bundle_index)) {
if ($f->entityCategory == 'instance' && isset($bundle_index)) {
$form['simple_sitemap']['simple_sitemap_index_content']['#options'][$bundle_index] .= ' <em>(' . t('Default') . ')</em>';
}
$form['simple_sitemap']['simple_sitemap_priority'] = array(
'#type' => 'select',
'#title' => t('Priority'),
'#description' => $f->entityType == 'bundle_instance' ? t('The priority this entity will have in the eyes of search engine bots.') : t('The priority entities of this bundle will have in the eyes of search engine bots.'),
'#description' => $f->entityCategory == 'instance' ? t('The priority this entity will have in the eyes of search engine bots.') : t('The priority entities of this bundle will have in the eyes of search engine bots.'),
'#default_value' => $priority,
'#options' => Form::getPrioritySelectValues(),
);
if ($f->entityType == 'bundle_instance' && isset($bundle_priority)) {
if ($f->entityCategory == 'instance' && isset($bundle_priority)) {
$form['simple_sitemap']['simple_sitemap_priority']['#options'][(string)$bundle_priority] .= ' (' . t('Default') . ')';
}
......@@ -139,7 +139,7 @@ function simple_sitemap_entity_form_submit($form, &$form_state) {
// Get current entity type sitemap settings.
$entity_types = $sitemap->getConfig('entity_types');
switch ($f->entityType) {
switch ($f->entityCategory) {
case 'custom':
case 'bundle':
......@@ -148,18 +148,18 @@ function simple_sitemap_entity_form_submit($form, &$form_state) {
$entity_types[$f->entityTypeId][$f->bundleName]['priority'] = $values['simple_sitemap_priority'];
break;
case 'bundle_instance':
$f->entityId = !empty($f->entityId) ? $f->entityId : Form::getNewEntityId($form_state);
case 'instance':
$f->instanceId = !empty($f->instanceId) ? $f->instanceId : Form::getNewEntityId($form_state);
// Delete overrides if they are identical to bundle settings.
if ($values['simple_sitemap_index_content'] == $entity_types[$f->entityTypeId][$f->bundleName]['index']
&& $values['simple_sitemap_priority'] == $entity_types[$f->entityTypeId][$f->bundleName]['priority']) {
unset($entity_types[$f->entityTypeId][$f->bundleName]['entities'][$f->entityId]);
unset($entity_types[$f->entityTypeId][$f->bundleName]['entities'][$f->instanceId]);
}
// Else save overrides for this entity.
else {
$entity_types[$f->entityTypeId][$f->bundleName]['entities'][$f->entityId]['index'] = $values['simple_sitemap_index_content'];
$entity_types[$f->entityTypeId][$f->bundleName]['entities'][$f->entityId]['priority'] = $values['simple_sitemap_priority'];
$entity_types[$f->entityTypeId][$f->bundleName]['entities'][$f->instanceId]['index'] = $values['simple_sitemap_index_content'];
$entity_types[$f->entityTypeId][$f->bundleName]['entities'][$f->instanceId]['priority'] = $values['simple_sitemap_priority'];
}
break;
}
......
services:
plugin.manager.simple_sitemap:
class: Drupal\simple_sitemap\SimplesitemapManager
parent: default_plugin_manager
simple_sitemap.generator:
class: Drupal\simple_sitemap\Simplesitemap
arguments: ['@config.factory']
<?php
/**
* @file
* Contains \Drupal\simple_sitemap\Annotation\LinkGenerator.
*/
namespace Drupal\simple_sitemap\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a LinkGenerator item annotation object.
*
* @see \Drupal\simple_sitemap\Plugin\SimplesitemapManager
* @see plugin_api
*
* @Annotation
*/
class LinkGenerator extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
}
......@@ -38,7 +38,7 @@ class Batch {
$config = \Drupal::config('simple_sitemap.settings')->get('settings');
$this->batchInfo = array(
'from' => $from,
'batch_process_limit' => $config['batch_process_limit'],
'batch_process_limit' => !empty($config['batch_process_limit']) ? $config['batch_process_limit'] : NULL,
'max_links' => $config['max_links'],
'remove_duplicates' => $config['remove_duplicates'],
'entity_types' => \Drupal::config('simple_sitemap.settings')->get('entity_types'),
......@@ -94,7 +94,7 @@ class Batch {
foreach ($operations as $operation) {
$this->batch['operations'][] = array(
__CLASS__ . '::generateBundleUrls',
array($operation['query'], $operation['entity_info'], $this->batchInfo)
array($operation['entity_info'], $this->batchInfo)
);
};
break;
......@@ -114,8 +114,8 @@ class Batch {
*/
public static function finishGeneration($success, $results, $operations) {
if ($success) {
if (!empty($results['generate']) || is_null(db_query('SELECT MAX(id) FROM {simple_sitemap}')->fetchField())) {
$remove_sitemap = empty($results['chunk_count']);
$remove_sitemap = empty($results['chunk_count']);
if (!empty($results['generate']) || $remove_sitemap/*is_null(db_query('SELECT MAX(id) FROM {simple_sitemap}')->fetchField())*/) { //todo test
SitemapGenerator::generateSitemap($results['generate'], $remove_sitemap);
}
Cache::invalidateTags(array('simple_sitemap'));
......@@ -131,96 +131,116 @@ class Batch {
return $batch_info['from'] != 'nobatch';
}
private static function needsInitialization($batch_info, $context) {
return self::isBatch($batch_info) && empty($context['sandbox']);
private static function needsInitialization($context) {
return empty($context['sandbox']);
}
/**
* Batch callback function which generates urls to entity paths.
*
* @param object $query
* @param array $entity_info
* @param array $batch_info
* @param array &$context
*
* @see https://api.drupal.org/api/drupal/core!includes!form.inc/group/batch/8
*/
public static function generateBundleUrls($query, $entity_info, $batch_info, &$context) {
public static function generateBundleUrls($entity_info, $batch_info, &$context) {
$languages = \Drupal::languageManager()->getLanguages();
$default_language_id = Simplesitemap::getDefaultLangId();
$query = \Drupal::entityQuery($entity_info['entity_type_name']);
if (!empty($entity_info['keys']['id'])) {
$query->sort($entity_info['keys']['id'], 'ASC');
}
if (!empty($entity_info['keys']['bundle'])) {
$query->condition($entity_info['keys']['bundle'], $entity_info['bundle_name']);
}
if (!empty($entity_info['keys']['status'])) {
$query->condition($entity_info['keys']['status'], 1);
}
// Initialize batch if not done yet.
if (self::needsInitialization($batch_info, $context)) {
self::InitializeBatch($query['query']->countQuery()->execute()->fetchField(), $context);
if (self::needsInitialization($context)) {
$count_query = clone $query;
self::InitializeBatch($batch_info, $count_query->count()->execute(), $context);
}
$id_field = self::getIdField($query);
// Creating a query limited to n=batch_process_limit entries.
if (self::isBatch($batch_info)) {
$query['query']->condition($id_field, $context['sandbox']['current_id'], '>')->orderBy($id_field);
if (!empty($batch_info['batch_process_limit']))
$query['query']->range(0, $batch_info['batch_process_limit']);
$query->range($context['sandbox']['progress'], $batch_info['batch_process_limit']);
}
$result = $query['query']->execute()->fetchAll();
foreach ($result as $row) {
if (self::isBatch($batch_info)) {
self::SetCurrentId($row->$id_field, $context);
}
$results = $query->execute();
if (!empty($results)) {
$entities = entity_load_multiple($entity_info['entity_type_name'], $results);
// Overriding entity settings if it has been overridden on entity edit page...
$bundle_name = !empty($entity_info['bundle_name']) ? $entity_info['bundle_name'] : NULL;
$bundle_entity_type = !empty($entity_info['bundle_entity_type']) ? $entity_info['bundle_entity_type'] : NULL;
if (!empty($bundle_name) && !empty($bundle_entity_type)
&& isset($batch_info['entity_types'][$bundle_entity_type][$bundle_name]['entities'][$row->$id_field]['index'])) {
// Skipping entity if it has been excluded on entity edit page.
if (!$batch_info['entity_types'][$bundle_entity_type][$bundle_name]['entities'][$row->$id_field]['index']) {
continue;
foreach ($entities as $entity_id => $entity) {
if (self::isBatch($batch_info)) {
self::SetCurrentId($entity_id, $context); //todo: move outside of this loop
}
// Otherwise overriding priority settings for this entity.
$priority = $batch_info['entity_types'][$bundle_entity_type][$bundle_name]['entities'][$row->$id_field]['priority'];
}
$route_parameters = self::getRouteParameters($query, $row, $entity_info, $id_field);
$options = self::getOptions($query, $row);
$route_name = self::getRouteName($query, $row, $entity_info);
if (!$route_name) {
//todo: register error
continue;
}
$url_object = Url::fromRoute($route_name, $route_parameters, $options);
// Overriding entity settings if it has been overridden on entity edit page...
$bundle_name = !empty($entity_info['bundle_name']) ? $entity_info['bundle_name'] : NULL;
$bundle_entity_type = !empty($entity_info['bundle_entity_type']) ? $entity_info['bundle_entity_type'] : NULL;
// Do not include path if anonymous users do not have access to it.
if (!$url_object->access($batch_info['anonymous_user_account']))
continue;
$bundle_entity_type = $bundle_entity_type == 'menu_link_content' ? 'menu' : $bundle_entity_type; // Menu fix
// Do not include path if it already exists.
$path = $url_object->getInternalPath();
if ($batch_info['remove_duplicates'] && self::pathProcessed($path, $context))
continue;
if (!empty($bundle_name) && !empty($bundle_entity_type)
&& isset($batch_info['entity_types'][$bundle_entity_type][$bundle_name]['entities'][$entity_id]['index'])) {
$urls = array();
foreach ($languages as $language) {
if ($language->getId() === $default_language_id) {
$urls[$default_language_id] = $url_object->toString();
// Skipping entity if it has been excluded on entity edit page.
if (!$batch_info['entity_types'][$bundle_entity_type][$bundle_name]['entities'][$entity_id]['index']) {
continue;
}
// Otherwise overriding priority settings for this entity.
$priority = $batch_info['entity_types'][$bundle_entity_type][$bundle_name]['entities'][$entity_id]['priority'];
}
// Loading url object for menu links.
if ($entity_info['entity_type_name'] == 'menu_link_content') {
if (!$entity->isEnabled())
continue;
$url_object = $entity->getUrlObject();
}
// Loading url object for other entities.
else {
$options['language'] = $language;
$urls[$language->getId()] = Url::fromRoute($route_name, $route_parameters, $options)
->toString();
$route_name = 'entity.' . $entity_info['entity_type_name'] . '.canonical';
$route_parameters = array($entity_info['entity_type_name'] => $entity_id);
$url_object = Url::fromRoute($route_name, $route_parameters);
}
}
$url_object->setOption('absolute', TRUE);
$context['results']['generate'][] = array(
'path' => $path,
'urls' => $urls,
'options' => $url_object->getOptions(),
'lastmod' => !empty($query['field_info']['lastmod']) ? date_iso8601($row->{$query['field_info']['lastmod']}) : NULL,
'priority' => isset($priority) ? $priority : (isset($entity_info['bundle_settings']['priority']) ? $entity_info['bundle_settings']['priority'] : NULL),
);
$priority = NULL;
// Do not include path if anonymous users do not have access to it.
if (!$url_object->access($batch_info['anonymous_user_account']))
continue;
// Do not include path if it already exists.
$path = $url_object->getInternalPath();
if ($batch_info['remove_duplicates'] && self::pathProcessed($path, $context))
continue;
$urls = array();
foreach ($languages as $language) {
if ($language->getId() === $default_language_id) {
$urls[$default_language_id] = $url_object->toString();
}
else {
$url_object->setOption('language', $language);
$urls[$language->getId()] = $url_object->toString();
}
}
$context['results']['generate'][] = array(
'path' => $path,
'urls' => $urls,
'options' => $url_object->getOptions(),
'lastmod' => method_exists($entity, 'getChangedTime') ? date_iso8601($entity->getChangedTime()) : NULL,
'priority' => isset($priority) ? $priority : (isset($entity_info['bundle_settings']['priority']) ? $entity_info['bundle_settings']['priority'] : NULL),
);
$priority = NULL;
}
}
if (self::isBatch($batch_info)) {
self::setProgressInfo($context);
}
......@@ -242,8 +262,8 @@ class Batch {
$default_language_id = Simplesitemap::getDefaultLangId();
// Initialize batch if not done yet.
if (self::needsInitialization($batch_info, $context)) {
self::InitializeBatch(count($custom_paths), $context);
if (self::needsInitialization($context)) {
self::InitializeBatch($batch_info, count($custom_paths), $context);
}
foreach($custom_paths as $i => $custom_path) {
......@@ -272,8 +292,8 @@ class Batch {
$urls[$default_language_id] = $url_object->toString();
}
else {
$options['language'] = $language;
$urls[$language->getId()] = Url::fromUserInput($user_input, $options)->toString();
$url_object->setOption('language', $language);
$urls[$language->getId()] = $url_object->toString();
}
}
......@@ -299,58 +319,13 @@ class Batch {
return FALSE;
}
private static function InitializeBatch($max, &$context) {
$context['sandbox']['progress'] = 0;
$context['sandbox']['current_id'] = 0;
$context['sandbox']['max'] = $max;
private static function InitializeBatch($batch_info, $max, &$context) {
$context['results']['generate'] = !empty($context['results']['generate']) ? $context['results']['generate'] : array();
$context['results']['processed_paths'] = !empty($context['results']['processed_paths']) ? $context['results']['processed_paths'] : array();
}
private static function getIdField($query) {
// Getting id field name from plugin info.
$fields = $query['query']->getFields();
if (isset($query['field_info']['entity_id']) && isset($fields[$query['field_info']['entity_id']])) {
return $query['field_info']['entity_id'];
}
return FALSE;
}
private static function getRouteParameters($query, $row, $entity_info, $id_field) {
// Setting route parameters if they exist in the database (menu links).
if (!empty($query['field_info']['route_parameters'])) {
$route_params_field = $query['field_info']['route_parameters'];
return unserialize($row->$route_params_field);
}
elseif (!empty($entity_info['entity_type_name'])) {
return array($entity_info['entity_type_name'] => $row->$id_field);
}
else {
return array();
}
}
private static function getOptions($query, $row) {
// Setting options if they exist in the database (menu links)
if (!empty($query['field_info']['options'])) {
$options_field = $query['field_info']['options'];
}
$options = isset($options_field) && !empty($options = unserialize($row->$options_field)) ? $options : array();
$options['absolute'] = TRUE;
return $options;
}
private static function getRouteName($query, $row, $entity_info) {
// Setting route name if it exists in the database (menu links)
if (!empty($query['field_info']['route_name'])) {
$route_name_field = $query['field_info']['route_name'];
return $row->$route_name_field;
}
elseif (!empty($entity_info['entity_type_name'])) {
return 'entity.' . $entity_info['entity_type_name'] . '.canonical';
}
else {
return FALSE;
if (self::isBatch($batch_info)) {
$context['sandbox']['progress'] = 0;
$context['sandbox']['current_id'] = 0;
$context['sandbox']['max'] = $max;
$context['results']['processed_paths'] = !empty($context['results']['processed_paths']) ? $context['results']['processed_paths'] : array();
}
}
......
......@@ -15,12 +15,11 @@ class Form {
const PRIORITY_HIGHEST = 10;
const PRIORITY_DIVIDER = 10;
public $entityType;
public $entityCategory;
public $entityTypeId;
public $bundleName;
public $entityId;
public $instanceId;
private $plugins;
private $formState;
private $formId;
......@@ -30,37 +29,33 @@ class Form {
function __construct(&$form, $form_state, $form_id) {
$this->formId = $form_id;
$this->formState = $form_state;
$this->entityCategory = NULL;
// Get all simple_sitemap plugins.
$manager = \Drupal::service('plugin.manager.simple_sitemap');
$this->plugins = $manager->getDefinitions();
// First look for a plugin declaring usage of this form, if this fails,
// check if this is a bundle, or bundle instance form and gather the form
// entity's sitemap settings from the database.
if (!$this->getEntityDataFromCustomPlugin()) {
$this->getEntityDataFromForm();
if (!$this->getEntityDataFromFormId()) {
$this->getEntityDataFromFormEntity();
}
}
/**
* Checks if a plugin defines this form to be used for its entity settings and
* sets the entity settings and collects those settings.
* This is a temporary solution to be able to set sitemap settings
* for all 'user' entities on the form 'user_admin_settings', as there is no
* general setting page where non-bundle entity types can be set up.
*
* @return bool
* TRUE if there is a plugin declaring usage of this form, FALSE otherwise.
* @todo: Remove this and create a general setting page for all entity types.
*/
private function getEntityDataFromCustomPlugin() {
foreach($this->plugins as $plugin) {
if (isset($plugin['form_id']) && $plugin['form_id'] === $this->formId) {
$this->entityType = 'custom';
$this->entityTypeId = $plugin['id'];
$this->bundleName = $plugin['id'];
private function getEntityDataFromFormId() {
switch ($this->formId) {
case 'user_admin_settings':
$this->entityCategory = 'bundle';
$this->entityTypeId = 'user';
$this->bundleName = 'user';
$this->instanceId = NULL;
return TRUE;
}
default:
return FALSE;
}
return FALSE;
}
/**
......@@ -70,29 +65,51 @@ class Form {
* @return bool
* TRUE if this is a bundle or bundle instance form, FALSE otherwise.
*/
private function getEntityDataFromForm() {
private function getEntityDataFromFormEntity() {
$form_entity = $this->getFormEntity();
if ($form_entity !== FALSE) {
$entity_type = $form_entity->getEntityType();
if (!empty($entity_type->getBundleEntityType())) {
$this->entityType = 'bundle_instance';
$this->entityTypeId = $entity_type->getBundleEntityType();
$this->bundleName = $form_entity->bundle();
$this->entityId = $form_entity->id();
return TRUE;
$form_entity_type = $form_entity->getEntityType();
$entity_type_id = $form_entity->getEntityTypeId(); //todo: Change to $form_entity_type->id()?
$sitemap_entity_types = Simplesitemap::getSitemapEntityTypes();
$bundle_entity_type = $form_entity_type->getBundleEntityType();
$entity_bundle = $form_entity->bundle();
if (isset($sitemap_entity_types[$entity_type_id])) {
$this->entityCategory = 'instance';
}
else {
$entity_type_id = $form_entity->getEntityTypeId();
if (isset($this->plugins[$entity_type_id])) {
if (!isset($this->plugins[$entity_type_id]['form_id'])
|| $this->plugins[$entity_type_id]['form_id'] === $this->formId) {
$this->entityType = 'bundle';
$this->entityTypeId = $entity_type_id;
$this->bundleName = $form_entity->id();
return TRUE;
foreach ($sitemap_entity_types as $sitemap_entity) {
if ($sitemap_entity->getBundleEntityType() == $entity_type_id) {
$this->entityCategory = 'bundle';
break;
}
}
}
// Menu fixes.
if (is_null($this->entityCategory) && $entity_type_id == 'menu') {
$this->entityCategory = 'bundle';
}
if ($entity_type_id == 'menu_link_content') {
$bundle_entity_type = 'menu';
}
switch ($this->entityCategory) {
case 'bundle':
$this->entityTypeId = $form_entity->getEntityTypeId();
$this->bundleName = $form_entity->id();
$this->instanceId = NULL;
break;
case 'instance':
$this->entityTypeId = !empty($bundle_entity_type) ? $bundle_entity_type : $entity_bundle;
$this->bundleName = $entity_bundle == 'menu_link_content' && method_exists($form_entity, 'getMenuName') ? $form_entity->getMenuName() : $entity_bundle; // menu fix
$this->instanceId = $form_entity->id();
break;
default:
return FALSE;
}
return TRUE;
}
return FALSE;
}
......