...
 
Commits (3)
id: bing
label: 'Bing'
url: http://www.bing.com/ping?sitemap=[sitemap]
status: true
id: google
label: 'Google'
url: http://www.google.com/ping?sitemap=[sitemap]
status: true
simple_sitemap_engines.simple_sitemap_engine.*:
type: config_entity
label: 'Search engine'
mapping:
id:
type: string
label: 'Search engine ID'
label:
type: label
label: 'Label'
url:
type: string
label: 'Submission URL'
status:
type: boolean
label: 'Enabled'
last_submitted:
type: integer
label: 'Last submitted'
simple_sitemap_engines.settings:
type: config_object
label: 'Sitemap search engine submission settings'
mapping:
enabled:
type: boolean
label: 'Sitemap submission enabled'
submission_interval:
type: integer
label: 'Sitemap submission frequency'
name: 'Simple XML Sitemap (Search engines)'
type: module
description: 'Submits sitemaps to search engines.'
configure: simple_sitemap_engines.settings
package: SEO
core: 8.x
dependencies:
- simple_sitemap:simple_sitemap
simple_sitemap_engines:
route_name: entity.simple_sitemap_engine.status
title: 'Search engines'
base_route: simple_sitemap.settings
weight: 5
simple_sitemap_engines.status:
route_name: entity.simple_sitemap_engine.status
title: 'Status'
parent_id: simple_sitemap_engines
weight: 1
simple_sitemap_engines.settings:
route_name: simple_sitemap_engines.settings
title: 'Settings'
parent_id: simple_sitemap_engines
weight: 2
<?php
/**
* @file
* Submits sitemaps to search engines.
*/
/**
* Implements hook_cron().
*
* If the sitemap submission interval has elapsed, adds each search engine to
* the submission queue to be processed.
*
* @see Drupal\simple_sitemap_engines\Plugin\QueueWorker\SitemapSubmitter
*/
function simple_sitemap_engines_cron() {
/** @var \Drupal\Core\Config\Config $config */
$config = \Drupal::config('simple_sitemap_engines.settings');
if ($config->get('enabled')) {
$interval = (int) $config->get('submission_interval') * 60 * 60;
$request_time = \Drupal::service('datetime.time')->getRequestTime();
$state = \Drupal::state();
if ($interval === 0
|| $state->get('simple_sitemap_engines_last_submitted', 0) + $interval <= $request_time) {
/** @var \Drupal\Core\Queue\QueueInterface $queue */
$queue = \Drupal::queue('simple_sitemap_engine_submit');
$state->set('simple_sitemap_engines_last_submitted', $request_time);
foreach (\Drupal::entityTypeManager()
->getStorage('simple_sitemap_engine')
->loadByProperties(['status' => TRUE]) as $id => $engine) {
$queue->createItem($id);
}
}
}
}
entity.simple_sitemap_engine.status:
path: '/admin/config/search/simplesitemap/engines'
defaults:
_entity_list: 'simple_sitemap_engine'
_title: 'Simple XML Sitemap Settings'
requirements:
_permission: 'administer sitemap settings'
simple_sitemap_engines.settings:
path: '/admin/config/search/simplesitemap/engines/settings'
defaults:
_form: '\Drupal\simple_sitemap_engines\Form\SimplesitemapEnginesForm'
_title: 'Simple XML Sitemap Settings'
requirements:
_permission: 'administer sitemap settings'
<?php
namespace Drupal\simple_sitemap_engines\Controller;
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\simple_sitemap\Form\FormHelper;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Search engine entity list builder.
*/
class SearchEngineListBuilder extends ConfigEntityListBuilder {
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected $dateFormatter;
/**
* SearchEngineListBuilder constructor.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The entity storage class.
* @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
* The date formatter service.
*/
public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, DateFormatterInterface $date_formatter) {
parent::__construct($entity_type, $storage);
$this->dateFormatter = $date_formatter;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('entity_type.manager')->getStorage($entity_type->id()),
$container->get('date.formatter')
);
}
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header['label'] = $this->t('Name');
$header['url'] = $this->t('Submission URL');
$header['status'] = $this->t('Status');
$header['last_submitted'] = $this->t('Last submitted');
return $header;
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
/** @var \Drupal\simple_sitemap_engines\Entity\SearchEngine $entity */
$row['label'] = $entity->label();
$row['url'] = $entity->url;
$row['status'] = $entity->status ? $this->t('Enabled') : $this->t('Disabled');
$row['last_submitted'] = $entity->last_submitted
? $this->dateFormatter->format($entity->last_submitted, 'short')
: $this->t('Never');
return $row;
}
public function render() {
return ['simple_sitemap_engines' => [
'#prefix' => FormHelper::getDonationText(),
'#title' => $this->t('Submission status'),
'#type' => 'fieldset',
'table' => parent::render(),
]];
}
}
<?php
namespace Drupal\simple_sitemap_engines\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
/**
* Defines the the search engine entity class.
*
* @ConfigEntityType(
* id = "simple_sitemap_engine",
* label = @Translation("Search engine"),
* admin_permission = "administer sitemap settings",
* entity_keys = {
* "id" = "id",
* "label" = "label",
* },
* handlers = {
* "list_builder" = "Drupal\simple_sitemap_engines\Controller\SearchEngineListBuilder",
* },
* links = {
* "collection" = "/admin/config/search/simplesitemap/engines/list",
* },
* config_export = {
* "id",
* "label",
* "url",
* "status",
* "last_submitted",
* }
* )
*/
class SearchEngine extends ConfigEntityBase {
/**
* The search engine ID.
*
* @var string
*/
public $id;
/**
* The search engine label.
*
* @var string
*/
public $label;
/**
* The search engine submission URL.
*
* When submitting to search engines, '[sitemap]' will be replaced with the
* full URL to the sitemap.xml.
*
* @var string
*/
public $url;
/**
* The search engine enabled state.
*
* @var bool
*/
public $status;
/**
* Timestamp when the sitemap was last submitted to this search engine.
*
* @var int
*/
public $last_submitted;
/**
* Implements magic __toString() to simplify checkbox list building.
*
* @return string
* The search engine label.
*/
public function __toString() {
return $this->label();
}
}
<?php
namespace Drupal\simple_sitemap_engines\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Datetime\DateFormatter;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\simple_sitemap\Form\FormHelper;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Form for managing search engine submission settings.
*/
class SimplesitemapEnginesForm extends ConfigFormBase {
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatter
*/
protected $dateFormatter;
/**
* SimplesitemapEnginesForm constructor.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Core\Datetime\DateFormatter $date_formatter
* The date formatter service.
*/
public function __construct(ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_type_manager, DateFormatter $date_formatter) {
parent::__construct($config_factory);
$this->entityTypeManager = $entity_type_manager;
$this->dateFormatter = $date_formatter;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
$container->get('entity_type.manager'),
$container->get('date.formatter')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'simple_sitemap_engines_settings_form';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['simple_sitemap_engines.settings'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('simple_sitemap_engines.settings');
$engines = $this->entityTypeManager->getStorage('simple_sitemap_engine')->loadMultiple();
// Construct a non-associative array containing the enabled search engines.
$engine_statuses = array_column($engines, 'status', 'id');
$enabled_engines = array_keys(array_filter($engine_statuses));
$form['engine_group'] = [
'#type' => 'fieldset',
'#title' => $this->t('Search engines'),
'#prefix' => FormHelper::getDonationText(),
];
$form['engine_group']['engines'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Submit the sitemap to the following engines'),
'#options' => $engines,
'#default_value' => $enabled_engines,
];
$form['settings'] = [
'#type' => 'fieldset',
'#title' => $this->t('Submission settings'),
];
$form['settings']['enabled'] = [
'#type' => 'checkbox',
'#title' => $this->t('Submit the sitemap to search engines'),
'#default_value' => $config->get('enabled'),
];
$form['settings']['submission_interval'] = [
'#type' => 'select',
'#title' => $this->t('Submission interval'),
'#options' => FormHelper::getCronIntervalOptions(),
'#default_value' => $config->get('submission_interval'),
'#states' => [
'visible' => [':input[name="enabled"]' => ['checked' => TRUE]],
],
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$engines = $this->entityTypeManager->getStorage('simple_sitemap_engine')->loadMultiple();
foreach ($form_state->getValue('engines') as $engine => $enabled) {
$engines[$engine]->status = (bool) $enabled;
$engines[$engine]->save();
}
$config = $this->config('simple_sitemap_engines.settings');
$config->set('enabled', $form_state->getValue('enabled'));
$config->set('submission_interval', $form_state->getValue('submission_interval'));
$config->save();
}
}
<?php
namespace Drupal\simple_sitemap_engines\Plugin\QueueWorker;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Queue\QueueWorkerBase;
use Drupal\simple_sitemap\SimplesitemapManager;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\RequestException;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Process a queue of search engines to submit sitemaps.
*
* @QueueWorker(
* id = "simple_sitemap_engine_submit",
* title = @Translation("Sitemap search engine submission"),
* cron = {"time" = 30}
* )
*
* @see simple_sitemap_engines_cron()
*/
class SitemapSubmitter extends QueueWorkerBase implements ContainerFactoryPluginInterface {
/**
* The search engine entity storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $engineStorage;
/**
* The HTTP client service.
*
* @var \GuzzleHttp\ClientInterface
*/
protected $httpClient;
/**
* The sitemap manager service.
*
* @var \Drupal\simple_sitemap\SimplesitemapManager
*/
protected $sitemapManager;
/**
* The simple sitemap logger.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* Constructs a new class instance.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityStorageInterface $engine_storage
* The search engine entity storage.
* @param \GuzzleHttp\ClientInterface $http_client
* The HTTP client service.
* @param \Drupal\simple_sitemap\SimplesitemapManager $sitemap_manager
* The sitemap manager service.
* @param \Psr\Log\LoggerInterface $logger
* The simple sitemap logger.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityStorageInterface $engine_storage, ClientInterface $http_client, SimplesitemapManager $sitemap_manager, LoggerInterface $logger) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->engineStorage = $engine_storage;
$this->httpClient = $http_client;
$this->sitemapManager = $sitemap_manager;
$this->logger = $logger;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager')->getStorage('simple_sitemap_engine'),
$container->get('http_client'),
$container->get('simple_sitemap.manager'),
$container->get('logger.factory')->get('simple_sitemap')
);
}
/**
* {@inheritdoc}
*/
public function processItem($id) {
/** @var \Drupal\simple_sitemap_engines\Entity\SearchEngine $engine */
if ($engine = $this->engineStorage->load($id)) {
// Gather URLs for all sitemap variants.
$sitemap_urls = [];
foreach ($this->sitemapManager->getSitemapTypes() as $type_name => $type_definition) {
$sitemap_generator = $this->sitemapManager->getSitemapGenerator($type_definition['sitemapGenerator']);
$variants = $this->sitemapManager->getSitemapVariants($type_name, FALSE);
if (!empty($variants)) {
foreach ($variants as $id => $variant) {
$sitemap_urls[$variant['label']] = $sitemap_generator->setSitemapVariant($id)->getSitemapUrl();
}
}
}
// Submit all URLs.
foreach ($sitemap_urls as $variant => $sitemap_url) {
$submit_url = str_replace('[sitemap]', $sitemap_url, $engine->url);
try {
$this->httpClient->request('GET', $submit_url);
// Log if submission was successful.
$this->logger->info('Sitemap %sitemap submitted to @url', ['%sitemap' => $variant, '@url' => $submit_url]);
// Record last submission time. This is purely informational; the
// variable that determines when the next submission should be run is
// stored in the global state.
$engine->last_submitted = time();
}
catch (RequestException $e) {
// Catch and log exceptions so this submission gets removed from the
// queue whether or not it succeeded.
// If the error was caused by network failure, it's fine to just wait
// until next time the submission is queued to try again.
// If the error was caused by a malformed URL, keeping the submission
// in the queue to retry is pointless since it will always fail.
watchdog_exception('simple_sitemap', $e);
}
}
$engine->save();
}
}
}
<?php
namespace Drupal\Tests\simple_sitemap_engines\Kernel;
use Drupal\KernelTests\KernelTestBase;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\RequestException;
use Prophecy\Argument;
// phpcs:disable Drupal.Arrays.Array.LongLineDeclaration
/**
* Tests search engine sitemap submission.
*
* @group simple_sitemap_engines
*/
class SubmitSitemapTest extends KernelTestBase {
/**
* The modules to enable.
*
* @var array
*/
public static $modules = ['system', 'simple_sitemap', 'simple_sitemap_engines'];
/**
* The cron service.
*
* @var \Drupal\Core\Cron
*/
protected $cron;
/**
* The search engine entity storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $engineStorage;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('simple_sitemap_engine');
$this->installConfig('simple_sitemap');
$this->installConfig('simple_sitemap_engines');
$this->cron = \Drupal::service('cron');
$this->engineStorage = \Drupal::entityTypeManager()->getStorage('simple_sitemap_engine');
$this->queue = \Drupal::queue('simple_sitemap_engine_submit');
// Disable all search engines but one so tests will not fail if additional
// search engines are added in the future.
$engines = $this->engineStorage->loadMultiple();
foreach ($engines as $id => $engine) {
if ($id != 'google') {
$engine->status = FALSE;
$engine->save();
}
}
}
/**
* Tests sitemap submission URLs and last submission status.
*/
public function testSubmission() {
// Create a mock HTTP client.
$http_client = $this->prophesize(ClientInterface::class);
// Make mock HTTP requests always succeed.
$http_client->request('GET', Argument::any())->willReturn(TRUE);
// Replace the default HTTP client service with the mock.
$this->container->set('http_client', $http_client->reveal());
// Run cron to trigger submission.
$this->cron->run();
$google = $this->engineStorage->load('google');
$bing = $this->engineStorage->load('bing');
// Check that Google was marked as submitted and Bing was not.
$this->assertNotEmpty($google->last_submitted);
$this->assertEmpty($bing->last_submitted);
// Check that exactly 1 HTTP request was sent to the correct URL.
$http_client->request('GET', 'http://www.google.com/ping?sitemap=http://localhost/default/sitemap.xml')->shouldBeCalled();
$http_client->request('GET', Argument::any())->shouldBeCalledTimes(1);
}
/**
* Tests that sitemaps are not submitted every time cron runs.
*/
public function testNoDoubleSubmission() {
// Create a mock HTTP client.
$http_client = $this->prophesize(ClientInterface::class);
// Make mock HTTP requests always succeed.
$http_client->request('GET', Argument::any())->willReturn(TRUE);
// Replace the default HTTP client service with the mock.
$this->container->set('http_client', $http_client->reveal());
// Run cron to trigger submission.
$this->cron->run();
// Check that Google was submitted and store its last submitted time.
$google = $this->engineStorage->load('google');
$http_client->request('GET', 'http://www.google.com/ping?sitemap=http://localhost/default/sitemap.xml')->shouldBeCalledTimes(1);
$this->assertNotEmpty($google->last_submitted);
$google_last_submitted = $google->last_submitted;
// Make sure enough time passes between cron runs to guarantee that they
// do not run within the same second, since timestamps are compared below.
sleep(2);
$this->cron->run();
$google = $this->engineStorage->load('google');
// Check that the last submitted time was not updated on the second cron
// run.
$this->assertEquals($google->last_submitted, $google_last_submitted);
// Check that no duplicate request was sent.
$http_client->request('GET', 'http://www.google.com/ping?sitemap=http://localhost/default/sitemap.xml')->shouldBeCalledTimes(1);
}
/**
* Tests that failed sitemap submissions are handled properly.
*/
public function testFailedSubmission() {
// Create a mock HTTP client.
$http_client = $this->prophesize(ClientInterface::class);
// Make mock HTTP requests always fail.
$http_client->request('GET', Argument::any())->willThrow(RequestException::class);
// Replace the default HTTP client service with the mock.
$this->container->set('http_client', $http_client->reveal());
// Run cron to trigger submission.
$this->cron->run();
$google = $this->engineStorage->load('google');
// Check that one request was attempted.
$http_client->request('GET', Argument::any())->shouldBeCalledTimes(1);
// Check the last submission time is still empty.
$this->assertEmpty($google->last_submitted);
// Check that the submission was removed from the queue despite failure.
$this->assertEquals(0, $this->queue->numberOfItems());
}
}