Simplesitemap.php 32 KB
Newer Older
1 2
<?php

Pawel G's avatar
Pawel G committed
3
namespace Drupal\simple_sitemap;
4

Pawel G's avatar
Pawel G committed
5
use Drupal\Core\Database\Connection;
6
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
Pawel G's avatar
Pawel G committed
7
use Drupal\Core\Entity\EntityTypeManagerInterface;
8
use Drupal\Core\Extension\ModuleHandler;
Pawel G's avatar
Pawel G committed
9
use Drupal\Core\Path\PathValidator;
Pawel G's avatar
Pawel G committed
10 11
use Drupal\Core\Config\ConfigFactory;
use Drupal\Core\Datetime\DateFormatter;
12
use Drupal\Component\Datetime\Time;
13 14 15
use Drupal\simple_sitemap\Plugin\simple_sitemap\SitemapGenerator\DefaultSitemapGenerator;
use Drupal\simple_sitemap\Plugin\simple_sitemap\SitemapGenerator\SitemapGeneratorBase;
use Drupal\simple_sitemap\Plugin\simple_sitemap\SitemapGenerator\SitemapGeneratorManager;
16
use Drupal\simple_sitemap\Plugin\simple_sitemap\UrlGenerator\UrlGeneratorBase;
17
use Drupal\simple_sitemap\Plugin\simple_sitemap\UrlGenerator\UrlGeneratorManager;
18 19
use Drupal\simple_sitemap\Plugin\simple_sitemap\SitemapType\SitemapTypeBase;
use Drupal\simple_sitemap\Plugin\simple_sitemap\SitemapType\SitemapTypeManager;
20

21
/**
Pawel G's avatar
Pawel G committed
22
 * Class Simplesitemap
Pawel G's avatar
Pawel G committed
23
 * @package Drupal\simple_sitemap
24 25 26
 */
class Simplesitemap {

Pawel G's avatar
Pawel G committed
27
  const DEFAULT_SITEMAP_TYPE = 'default_hreflang';
Pawel G's avatar
Pawel G committed
28
  const DEFAULT_SITEMAP_GENERATOR = 'default';
Pawel G's avatar
Pawel G committed
29 30
  const DEFAULT_SITEMAP_VARIANT = 'default';

Pawel G's avatar
Pawel G committed
31 32 33
  /**
   * @var \Drupal\simple_sitemap\EntityHelper
   */
34
  protected $entityHelper;
Pawel G's avatar
Pawel G committed
35 36 37 38

  /**
   * @var \Drupal\Core\Config\ConfigFactory
   */
39
  protected $configFactory;
Pawel G's avatar
Pawel G committed
40 41 42 43

  /**
   * @var \Drupal\Core\Database\Connection
   */
44
  protected $db;
Pawel G's avatar
Pawel G committed
45 46 47 48

  /**
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
49
  protected $entityTypeManager;
Pawel G's avatar
Pawel G committed
50

51 52 53 54 55
  /**
   * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
   */
  protected $entityTypeBundleInfo;

Pawel G's avatar
Pawel G committed
56 57 58
  /**
   * @var \Drupal\Core\Path\PathValidator
   */
59
  protected $pathValidator;
Pawel G's avatar
Pawel G committed
60

61 62 63 64 65
  /**
   * @var \Drupal\Core\Datetime\DateFormatter
   */
  protected $dateFormatter;

66 67 68 69 70
  /**
   * @var \Drupal\Component\Datetime\Time
   */
  protected $time;

71
  /**
72
   * @var \Drupal\Core\Extension\ModuleHandler
73
   */
74
  protected $moduleHandler;
75 76

  /**
77
   * @var \Drupal\simple_sitemap\Batch
78
   */
79
  protected $batch;
80

81 82 83 84 85
  /**
   * @var \Drupal\simple_sitemap\Plugin\simple_sitemap\UrlGenerator\UrlGeneratorManager
   */
  protected $urlGeneratorManager;

86 87 88 89 90
  /**
   * @var \Drupal\simple_sitemap\Plugin\simple_sitemap\SitemapGenerator\SitemapGeneratorManager
   */
  protected $sitemapGeneratorManager;

91 92 93 94 95
  /**
   * @var \Drupal\simple_sitemap\Plugin\simple_sitemap\SitemapType\SitemapTypeManager
   */
  protected $sitemapTypeManager;

Pawel G's avatar
Pawel G committed
96 97 98 99 100 101 102 103 104 105
  /**
   * @var UrlGeneratorBase[] $urlGenerators
   */
  protected $urlGenerators = [];

  /**
   * @var SitemapGeneratorBase[] $sitemapGenerators
   */
  protected $sitemapGenerators = [];

106 107 108 109 110
  /**
   * @var SitemapTypeBase[] $sitemapTypes
   */
  protected $sitemapTypes = [];

Pawel G's avatar
Pawel G committed
111 112 113
  /**
   * @var array
   */
Pawel G's avatar
Pawel G committed
114
  protected static $allowedLinkSettings = [
115
    'entity' => ['index', 'priority', 'changefreq', 'include_images'],
116 117 118
    'custom' => ['priority', 'changefreq'],
  ];

Pawel G's avatar
Pawel G committed
119 120 121 122
  /**
   * @var array
   */
  protected static $linkSettingDefaults = [
123
    'index' => FALSE,
124
    'priority' => 0.5,
125
    'changefreq' => '',
126
    'include_images' => FALSE,
127
  ];
128

129 130
  /**
   * Simplesitemap constructor.
131 132
   * @param \Drupal\simple_sitemap\EntityHelper $entity_helper
   * @param \Drupal\Core\Config\ConfigFactory $config_factory
Pawel G's avatar
Pawel G committed
133
   * @param \Drupal\Core\Database\Connection $database
134
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
135
   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
136 137
   * @param \Drupal\Core\Path\PathValidator $path_validator
   * @param \Drupal\Core\Datetime\DateFormatter $date_formatter
138
   * @param \Drupal\Component\Datetime\Time $time
139
   * @param \Drupal\Core\Extension\ModuleHandler $module_handler
140
   * @param \Drupal\simple_sitemap\Batch $batch
141 142
   * @param \Drupal\simple_sitemap\Plugin\simple_sitemap\UrlGenerator\UrlGeneratorManager $url_generator_manager
   * @param \Drupal\simple_sitemap\Plugin\simple_sitemap\SitemapGenerator\SitemapGeneratorManager $sitemap_generator_manager
143
   * @param \Drupal\simple_sitemap\Plugin\simple_sitemap\SitemapType\SitemapTypeManager $sitemap_type_manager
144
   */
145
  public function __construct(
146 147
    EntityHelper $entity_helper,
    ConfigFactory $config_factory,
Pawel G's avatar
Pawel G committed
148
    Connection $database,
149
    EntityTypeManagerInterface $entity_type_manager,
150
    EntityTypeBundleInfoInterface $entity_type_bundle_info,
151 152
    PathValidator $path_validator,
    DateFormatter $date_formatter,
153
    Time $time,
154
    ModuleHandler $module_handler,
155
    Batch $batch,
156
    UrlGeneratorManager $url_generator_manager,
157 158
    SitemapGeneratorManager $sitemap_generator_manager,
    SitemapTypeManager $sitemap_type_manager
159
  ) {
160 161
    $this->entityHelper = $entity_helper;
    $this->configFactory = $config_factory;
162
    $this->db = $database;
163
    $this->entityTypeManager = $entity_type_manager;
164
    $this->entityTypeBundleInfo = $entity_type_bundle_info;
165 166
    $this->pathValidator = $path_validator;
    $this->dateFormatter = $date_formatter;
167
    $this->time = $time;
168
    $this->moduleHandler = $module_handler;
169
    $this->batch = $batch;
170 171
    $this->urlGeneratorManager = $url_generator_manager;
    $this->sitemapGeneratorManager = $sitemap_generator_manager;
172
    $this->sitemapTypeManager = $sitemap_type_manager;
173 174
  }

175
  /**
176 177
   * Returns a specific sitemap setting or a default value if setting does not
   * exist.
Pawel G's avatar
Pawel G committed
178
   *
179
   * @param string $name
Pawel G's avatar
Pawel G committed
180
   *  Name of the setting, like 'max_links'.
181 182
   *
   * @param mixed $default
Pawel G's avatar
Pawel G committed
183
   *  Value to be returned if the setting does not exist in the configuration.
184 185
   *
   * @return mixed
Pawel G's avatar
Pawel G committed
186
   *  The current setting from configuration or a default value.
Pawel G's avatar
Pawel G committed
187
   */
188 189 190 191 192 193 194 195 196 197 198
  public function getSetting($name, $default = FALSE) {
    $setting = $this->configFactory
      ->get('simple_sitemap.settings')
      ->get($name);
    return NULL !== $setting ? $setting : $default;
  }

  /**
   * Stores a specific sitemap setting in configuration.
   *
   * @param string $name
Pawel G's avatar
Pawel G committed
199
   *  Setting name, like 'max_links'.
200
   * @param mixed $setting
Pawel G's avatar
Pawel G committed
201
   *  The setting to be saved.
202 203 204 205
   *
   * @return $this
   */
  public function saveSetting($name, $setting) {
Pawel G's avatar
Pawel G committed
206
    $this->configFactory->getEditable('simple_sitemap.settings')
207 208 209 210
      ->set($name, $setting)->save();
    return $this;
  }

Pawel G's avatar
Pawel G committed
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238
  /**
   * @param $sitemap_generator_id
   * @return \Drupal\simple_sitemap\Plugin\simple_sitemap\SitemapGenerator\SitemapGeneratorBase
   * @throws \Drupal\Component\Plugin\Exception\PluginException
   */
  protected function getSitemapGenerator($sitemap_generator_id) {
    if (!isset($this->sitemapGenerators[$sitemap_generator_id])) {
      $this->sitemapGenerators[$sitemap_generator_id]
        = $this->sitemapGeneratorManager->createInstance($sitemap_generator_id);
    }

    return $this->sitemapGenerators[$sitemap_generator_id];
  }

  /**
   * @param $url_generator_id
   * @return \Drupal\simple_sitemap\Plugin\simple_sitemap\UrlGenerator\UrlGeneratorBase
   * @throws \Drupal\Component\Plugin\Exception\PluginException
   */
  protected function getUrlGenerator($url_generator_id) {
    if (!isset($this->urlGenerators[$url_generator_id])) {
      $this->urlGenerators[$url_generator_id]
        = $this->urlGeneratorManager->createInstance($url_generator_id);
    }

    return $this->urlGenerators[$url_generator_id];
  }

239 240 241 242 243 244 245 246 247 248 249
  /**
   * @return array
   */
  public function getSitemapTypes() {
    if (empty($this->sitemapTypes)) {
      $this->sitemapTypes = $this->sitemapTypeManager->getDefinitions();
    }

    return $this->sitemapTypes;
  }

250 251 252 253
  /**
   * Returns the whole sitemap, a requested sitemap chunk,
   * or the sitemap index file.
   *
254
   * @param string $variant
255 256
   *
   * @param int $delta
257 258
   *
   * @return string|false
Pawel G's avatar
Pawel G committed
259
   *  If no sitemap ID provided, either a sitemap index is returned, or the
260 261
   *  whole sitemap variant, if the amount of links does not exceed the max
   *  links setting. If a sitemap ID is provided, a sitemap chunk is returned.
Pawel G's avatar
Pawel G committed
262
   *  Returns false if the sitemap is not retrievable from the database.
263
   */
Pawel G's avatar
Pawel G committed
264 265
  public function getSitemap($variant = self::DEFAULT_SITEMAP_VARIANT, $delta = NULL) {
    $chunk_info = $this->fetchSitemapVariantInfo($variant);
266

267
    if (empty($delta) || !isset($chunk_info[$delta])) {
268

269
      if (isset($chunk_info[SitemapGeneratorBase::INDEX_DELTA])) {
Pawel G's avatar
Pawel G committed
270
        // Return sitemap index if one exists.
271 272
        return $this->fetchSitemapChunk($chunk_info[SitemapGeneratorBase::INDEX_DELTA]->id)
          ->sitemap_string;
273 274
      }
      else {
Pawel G's avatar
Pawel G committed
275
        // Return sitemap chunk if there is only one chunk.
276 277
        return isset($chunk_info[SitemapGeneratorBase::FIRST_CHUNK_DELTA])
          ? $this->fetchSitemapChunk($chunk_info[SitemapGeneratorBase::FIRST_CHUNK_DELTA]->id)
278 279 280 281 282 283
            ->sitemap_string
          : FALSE;
      }
    }
    else {
      // Return specific sitemap chunk.
284
      return $this->fetchSitemapChunk($chunk_info[$delta]->id)->sitemap_string;
285 286 287 288
    }
  }

  /**
Pawel G's avatar
Pawel G committed
289
   * Fetches info about all sitemap variants and their chunks.
290
   *
291
   * @param string|null $variant
292
   *
293
   * @return array
Pawel G's avatar
Pawel G committed
294 295
   *  An array containing all sitemap chunk IDs, deltas and creation timestamps
   * keyed by their variant ID.
296
   */
Pawel G's avatar
Pawel G committed
297
  protected function fetchSitemapVariantInfo($variant = NULL) {
298 299 300
    $query = $this->db->select('simple_sitemap', 's')
      ->fields('s', ['id', 'delta', 'sitemap_created', 'type']);

301 302
    if (NULL !== $variant) {
      $query->condition('s.type', $variant);
303 304 305 306
    }

    $result = $query->execute();

307
    return NULL === $variant ? $result->fetchAllAssoc('type') : $result->fetchAllAssoc('delta');
308 309
  }

310 311 312 313 314 315 316 317 318
  /**
   * Fetches a single sitemap chunk by ID.
   *
   * @param int $id
   *   The chunk ID.
   *
   * @return object
   *   A sitemap chunk object.
   */
319
  protected function fetchSitemapChunk($id) {
320 321 322 323
    return $this->db->query('SELECT * FROM {simple_sitemap} WHERE id = :id',
      [':id' => $id])->fetchObject();
  }

324
  /**
Pawel G's avatar
Pawel G committed
325
   * @param null $sitemap_type
326 327 328 329 330
   * @return array
   *
   * @todo document
   * @todo translate label
   */
Pawel G's avatar
Pawel G committed
331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354
  public function getSitemapVariants($sitemap_type = NULL, $attach_type_info = TRUE) {
    if (NULL === $sitemap_type) {
      $variants = [];
      foreach ($this->configFactory->listAll('simple_sitemap.variants.') as $config_name) {
        $config_name_parts = explode('.', $config_name);
        $saved_variants = $this->configFactory->get($config_name)->get('variants');
        $saved_variants = $attach_type_info ? $this->attachSitemapTypeToVariants($saved_variants, $config_name_parts[2]) : $saved_variants;
        $variants = array_merge($variants, (is_array($saved_variants) ? $saved_variants : []));
      }
      return $variants;
    }
    else {
      $variants = $this->configFactory->get("simple_sitemap.variants.$sitemap_type")->get('variants');
      $variants = is_array($variants) ? $variants : [];
      return $attach_type_info ? $this->attachSitemapTypeToVariants($variants, $sitemap_type) : $variants;
    }
  }

  protected function attachSitemapTypeToVariants(array $variants, $type) {
    return array_map(function($variant) use ($type) { return $variant + ['type' => $type]; }, $variants);
  }

  protected function detachSitemapTypeFromVariants(array $variants) {
    return array_map(function($variant) { unset($variant['type']); return $variant; }, $variants);
355 356 357 358 359 360 361 362 363 364
  }

  /**
   * @param $name
   * @param $definition
   * @return $this
   *
   * @todo document
   * @todo exceptions
   */
365
  public function addSitemapVariant($name, $definition = []) {
366
    if (empty($definition['label'])) {
Pawel G's avatar
Pawel G committed
367
      $definition['label'] = $name;
368 369 370
    }

    if (empty($definition['type'])) {
Pawel G's avatar
Pawel G committed
371
      $definition['type'] = self::DEFAULT_SITEMAP_TYPE;
372
    }
373
    else {
374
      $types = $this->getSitemapTypes();
375 376 377 378
      if (!isset($types[$definition['type']])) {
        // todo: exception
      }
    }
Pawel G's avatar
Pawel G committed
379 380 381 382 383 384 385 386 387 388
    $all_variants = $this->getSitemapVariants();
    if (isset($all_variants[$name])) {
      //todo: exception
    }
    else {
      $variants = array_merge($this->getSitemapVariants($definition['type'], FALSE), [$name => ['label' => $definition['label']]]);
      $this->configFactory->getEditable('simple_sitemap.variants.' . $definition['type'])
        ->set('variants', $variants)
        ->save();
    }
389 390 391 392

    return $this;
  }

Pawel G's avatar
Pawel G committed
393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411
  public function removeSitemapVariants($variant_names = NULL) {
    $this->removeSitemap($variant_names);
    if (NULL === $variant_names) {
      foreach ($this->configFactory->listAll('simple_sitemap.variants.') as $config_name) {
        $this->configFactory->getEditable($config_name)->delete();
      }
    }
    else {
      $remove_variants = [];
      $variants = $this->getSitemapVariants();
      foreach ((array) $variant_names as $variant_name) {
        if (isset($variants[$variant_name])) {
          $remove_variants[$variants[$variant_name]['type']][$variant_name] = $variant_name;
        }
      }

      foreach ($remove_variants as $type => $variants_per_type) {
        $this->configFactory->getEditable("simple_sitemap.variants.$type")
          ->set('variants', array_diff_key($this->getSitemapVariants($type, FALSE), $variants_per_type))
Pawel G's avatar
Pawel G committed
412
          ->save();
413 414
      }
    }
415

416 417 418
    return $this;
  }

419 420 421
  /**
   * @param null $variants
   * @return $this
422
   * @throws \Drupal\Component\Plugin\Exception\PluginException
423
   *
424
   * @todo document
425 426
   */
  public function removeSitemap($variants = NULL) {
Pawel G's avatar
Pawel G committed
427 428 429 430 431 432
    $saved_variants = $this->getSitemapVariants();
    $this->moduleHandler->alter('simple_sitemap_variants', $saved_variants);
    $remove_variants = NULL !== $variants
      ? array_intersect_key($saved_variants, array_flip((array) $variants))
      : $saved_variants;

433
    if (!empty($remove_variants)) {
434
      $type_definitions = $this->getSitemapTypes();
Pawel G's avatar
Pawel G committed
435
      $this->moduleHandler->alter('simple_sitemap_types', $type_definitions);
436
      foreach ($remove_variants as $variant_name => $variant_definition) {
437
        $this->getSitemapGenerator($type_definitions[$variant_definition['type']]['sitemapGenerator'])
438
          ->setSitemapVariant($variant_name)
439 440
          ->remove()
          ->invalidateCache();
441 442
      }
    }
Pawel G's avatar
Pawel G committed
443

444 445 446
    return $this;
  }

447
  /**
Pawel G's avatar
Pawel G committed
448
   * Generates the XML sitemap and saves it to the database.
449 450
   *
   * @param string $from
Pawel G's avatar
Pawel G committed
451 452
   *  Can be 'form', 'backend', 'drush' or 'nobatch'.
   *  This decides how the batch process is to be run.
Pawel G's avatar
Pawel G committed
453
   *
454
   * @param array|string|null $variants
455
   *
Pawel G's avatar
Pawel G committed
456
   * @return bool|\Drupal\simple_sitemap\Simplesitemap
457
   */
458 459
  public function generateSitemap($from = 'form', $variants = NULL) {
    $variants = NULL === $variants ? NULL : (array) $variants;
460

461
    $settings = [
462
      'base_url' => $this->getSetting('base_url', ''),
463
      'batch_process_limit' => $this->getSetting('batch_process_limit', 1500),
464 465 466 467
      'max_links' => $this->getSetting('max_links', 2000),
      'skip_untranslated' => $this->getSetting('skip_untranslated', FALSE),
      'remove_duplicates' => $this->getSetting('remove_duplicates', TRUE),
      'excluded_languages' => $this->getSetting('excluded_languages', []),
468
    ];
469

470 471
    $this->batch->setBatchMeta(['from' => $from]);

472
    $operations = [];
Pawel G's avatar
Pawel G committed
473

474
    $type_definitions = $this->getSitemapTypes();
475
    $this->moduleHandler->alter('simple_sitemap_types', $type_definitions);
Pawel G's avatar
Pawel G committed
476

477 478
    $sitemap_variants = $this->getSitemapVariants();
    $this->moduleHandler->alter('simple_sitemap_variants', $sitemap_variants);
479

480 481 482 483 484 485
    foreach ($sitemap_variants as $variant_name => $variant_definition) {

      // Skipping unwanted sitemap variants.
      if (NULL !== $variants && !in_array($variant_name, $variants)) {
        continue;
      }
486

487 488 489 490 491 492
      $type = $variant_definition['type'];

      // Adding a remove_sitemap operation for all sitemap variants.
      $operations[] = [
        'operation' => 'removeSitemap',
        'arguments' => [
493
          'sitemap_generator' => $type_definitions[$type]['sitemapGenerator'],
494 495 496 497 498
          'variant' => $variant_name,
        ]
      ];

      // Adding generate_sitemap operations for all data sets.
499
      foreach ($type_definitions[$type]['urlGenerators'] as $url_generator_id) {
500

Pawel G's avatar
Pawel G committed
501
        foreach ($this->getUrlGenerator($url_generator_id)
502 503 504 505 506
                   ->setSitemapVariant($variant_name)
                   ->getDataSets() as $data_set) {
          if (!empty($data_set)) {
            $operations[] = [
              'operation' => 'generateSitemap',
507
              'arguments' => [
508 509 510
                'url_generator' => $url_generator_id,
                'data_set' => $data_set,
                'variant' => $variant_name,
511
                'sitemap_generator' => $type_definitions[$type]['sitemapGenerator'],
512 513
                'settings' => $settings,
              ],
514 515
            ];
          }
516
        }
517
      }
518

519 520
      // Adding generate_index operations for all sitemap variants.
      $operations[] = [
521 522
        'operation' => 'generateIndex',
        'arguments' => [
523
          'sitemap_generator' => $type_definitions[$type]['sitemapGenerator'],
524
          'variant' => $variant_name,
525 526 527 528 529
          'settings' => $settings,
        ],
      ];
    }

530 531 532 533
    // Adding operations to and starting batch.
    if (!empty($operations)) {
      foreach ($operations as $operation_data) {
        $this->batch->addOperation($operation_data['operation'], $operation_data['arguments']);
534 535 536 537 538
      }
      $success = $this->batch->start();
    }

    return $from === 'nobatch' ? $this : (isset($success) ? $success : FALSE);
539 540 541 542 543
  }

  /**
   * Returns a 'time ago' string of last timestamp generation.
   *
544
   * @param string|null $variant
545 546
   *
   * @return string|array|false
Pawel G's avatar
Pawel G committed
547
   *  Formatted timestamp of last sitemap generation, otherwise FALSE.
548
   */
549
  public function getGeneratedAgo($variant = NULL) {
Pawel G's avatar
Pawel G committed
550
    $chunks = $this->fetchSitemapVariantInfo($variant);
551
    if ($variant !== NULL) {
552
      return isset($chunks[DefaultSitemapGenerator::FIRST_CHUNK_DELTA]->sitemap_created)
553
        ? $this->dateFormatter
554
          ->formatInterval($this->time->getRequestTime() - $chunks[DefaultSitemapGenerator::FIRST_CHUNK_DELTA]
555 556 557 558 559 560 561 562 563 564 565 566
              ->sitemap_created)
        : FALSE;
    }
    else {
      $time_strings = [];
//      foreach ($chunks as $sitemap_type => $type_chunks) {
//        $time_strings[$sitemap_type] = isset($type_chunks[DefaultSitemapGenerator::FIRST_DELTA_INDEX]->sitemap_created)
//          ? $type_chunks[DefaultSitemapGenerator::FIRST_DELTA_INDEX]->sitemap_created
//          : FALSE;
//    }
      // todo: Implement.
      return $time_strings;
567 568 569
    }
  }

570 571
  /**
   * Enables sitemap support for an entity type. Enabled entity types show
572 573
   * sitemap settings on their bundle setting forms. If an enabled entity type
   * features bundles (e.g. 'node'), it needs to be set up with
574 575 576
   * setBundleSettings() as well.
   *
   * @param string $entity_type_id
Pawel G's avatar
Pawel G committed
577
   *  Entity type id like 'node'.
578
   *
Pawel G's avatar
Pawel G committed
579
   * @return $this
580 581
   */
  public function enableEntityType($entity_type_id) {
582 583 584 585
    $enabled_entity_types = $this->getSetting('enabled_entity_types');
    if (!in_array($entity_type_id, $enabled_entity_types)) {
      $enabled_entity_types[] = $entity_type_id;
      $this->saveSetting('enabled_entity_types', $enabled_entity_types);
586
    }
Pawel G's avatar
Pawel G committed
587
    return $this;
588 589 590 591 592 593 594 595
  }

  /**
   * Disables sitemap support for an entity type. Disabling support for an
   * entity type deletes its sitemap settings permanently and removes sitemap
   * settings from entity forms.
   *
   * @param string $entity_type_id
596
   *  Entity type id like 'node'.
597
   *
Pawel G's avatar
Pawel G committed
598
   * @return $this
599 600
   */
  public function disableEntityType($entity_type_id) {
601 602 603

    // Updating settings.
    $enabled_entity_types = $this->getSetting('enabled_entity_types');
604
    if (FALSE !== ($key = array_search($entity_type_id, $enabled_entity_types))) {
605
      unset ($enabled_entity_types[$key]);
606
      $this->saveSetting('enabled_entity_types', array_values($enabled_entity_types));
607 608 609
    }

    // Deleting inclusion settings.
610
    $config_names = $this->configFactory->listAll("simple_sitemap.bundle_settings.$entity_type_id.");
Pawel G's avatar
Pawel G committed
611
    foreach ($config_names as $config_name) {
612
      $this->configFactory->getEditable($config_name)->delete();
613
    }
614 615 616

    // Deleting entity overrides.
    $this->removeEntityInstanceSettings($entity_type_id);
Pawel G's avatar
Pawel G committed
617
    return $this;
618 619 620
  }

  /**
Pawel G's avatar
Pawel G committed
621
   * Sets sitemap settings for a non-bundle entity type (e.g. user) or a bundle
622 623 624
   * of an entity type (e.g. page).
   *
   * @param string $entity_type_id
Pawel G's avatar
Pawel G committed
625
   *  Entity type id like 'node' the bundle belongs to.
626
   * @param string $bundle_name
Pawel G's avatar
Pawel G committed
627
   *  Name of the bundle. NULL if entity type has no bundles.
628
   * @param array $settings
Pawel G's avatar
Pawel G committed
629 630
   *  An array of sitemap settings for this bundle/entity type.
   *  Example: ['index' => TRUE, 'priority' => 0.5, 'changefreq' => 'never', 'include_images' => FALSE].
Pawel G's avatar
Pawel G committed
631 632
   *
   * @return $this
633 634
   *
   * @todo: enableEntityType automatically
635
   */
636
  public function setBundleSettings($entity_type_id, $bundle_name = NULL, $settings = ['index' => TRUE]) {
637
    $bundle_name = NULL !== $bundle_name ? $bundle_name : $entity_type_id;
638

Pawel G's avatar
Pawel G committed
639 640 641
    if (!empty($old_settings = $this->getBundleSettings($entity_type_id, $bundle_name))) {
      $settings = array_merge($old_settings, $settings);
    }
642
    self::supplementDefaultSettings('entity', $settings);
643

644
    if ($settings != $old_settings) {
645

646 647 648 649 650 651 652
      // Save new bundle settings to configuration.
      $bundle_settings = $this->configFactory
        ->getEditable("simple_sitemap.bundle_settings.$entity_type_id.$bundle_name");
      foreach ($settings as $setting_key => $setting) {
        $bundle_settings->set($setting_key, $setting);
      }
      $bundle_settings->save();
653

654 655 656 657 658
      // Delete entity overrides which are identical to new bundle settings.
      $sitemap_entity_types = $this->entityHelper->getSupportedEntityTypes();
      if (isset($sitemap_entity_types[$entity_type_id])) {
        $entity_type = $sitemap_entity_types[$entity_type_id];
        $keys = $entity_type->getKeys();
659

660 661
        // Menu fix.
        $keys['bundle'] = $entity_type_id === 'menu_link_content' ? 'menu_name' : $keys['bundle'];
662

663 664 665 666 667 668 669 670 671 672 673 674
        $query = $this->entityTypeManager->getStorage($entity_type_id)->getQuery();
        if (!$this->entityHelper->entityTypeIsAtomic($entity_type_id)) {
          $query->condition($keys['bundle'], $bundle_name);
        }
        $entity_ids = $query->execute();

        $query = $this->db->select('simple_sitemap_entity_overrides', 'o')
          ->fields('o', ['id', 'inclusion_settings'])
          ->condition('o.entity_type', $entity_type_id);
        if (!empty($entity_ids)) {
          $query->condition('o.entity_id', $entity_ids, 'IN');
        }
675

676 677 678 679 680 681 682 683 684 685 686 687
        $delete_instances = [];
        foreach ($query->execute()->fetchAll() as $result) {
          $delete = TRUE;
          $instance_settings = unserialize($result->inclusion_settings);
          foreach ($instance_settings as $setting_key => $instance_setting) {
            if ($instance_setting != $settings[$setting_key]) {
              $delete = FALSE;
              break;
            }
          }
          if ($delete) {
            $delete_instances[] = $result->id;
688 689
          }
        }
690 691 692 693
        if (!empty($delete_instances)) {
          $this->db->delete('simple_sitemap_entity_overrides')
            ->condition('id', $delete_instances, 'IN')
            ->execute();
694
        }
695
      }
696 697
      else {
        //todo: log error
698
      }
699
    }
700

Pawel G's avatar
Pawel G committed
701
    return $this;
702 703
  }

704
  /**
705 706
   * Gets sitemap settings for an entity bundle, a non-bundle entity type or for
   * all entity types and their bundles.
707
   *
708 709 710
   * @param string|null $entity_type_id
   *  If set to null, sitemap settings for all entity types and their bundles
   *  are fetched.
711 712 713
   * @param string|null $bundle_name
   *
   * @return array|false
Pawel G's avatar
Pawel G committed
714 715
   *  Array of sitemap settings for an entity bundle, a non-bundle entity type
   *  or for all entity types and their bundles.
716
   *  False if entity type does not exist.
717
   */
718
  public function getBundleSettings($entity_type_id = NULL, $bundle_name = NULL) {
719
    if (NULL !== $entity_type_id) {
720 721

      // Get bundle settings saved in simple_sitemap.bundle_settings.*.* configuration.
722
      $bundle_name = NULL !== $bundle_name ? $bundle_name : $entity_type_id;
723
      $bundle_settings = $this->configFactory
724 725
        ->get("simple_sitemap.bundle_settings.$entity_type_id.$bundle_name")
        ->get();
726 727 728 729 730

      // If not found and entity type is enabled, return default bundle settings.
      if (empty($bundle_settings)) {
        if ($this->entityTypeIsEnabled($entity_type_id)
          && isset($this->entityTypeBundleInfo->getBundleInfo($entity_type_id)[$bundle_name])) {
731
          self::supplementDefaultSettings('entity', $bundle_settings);
732 733 734 735 736
        }
        else {
          $bundle_settings = FALSE;
        }
      }
737
    }
738
    else {
739
      // Get all bundle settings saved in simple_sitemap.bundle_settings.*.* configuration.
Pawel G's avatar
Pawel G committed
740
      $config_names = $this->configFactory->listAll('simple_sitemap.bundle_settings.');
741
      $bundle_settings = [];
Pawel G's avatar
Pawel G committed
742
      foreach ($config_names as $config_name) {
743
        $config_name_parts = explode('.', $config_name);
744 745 746 747 748 749 750 751
        $bundle_settings[$config_name_parts[2]][$config_name_parts[3]] = $this->configFactory->get($config_name)->get();
      }

      // Supplement default bundle settings for all bundles not found in simple_sitemap.bundle_settings.*.* configuration.
      foreach ($this->entityHelper->getSupportedEntityTypes() as $type_id => $type_definition) {
        if ($this->entityTypeIsEnabled($type_id)) {
          foreach($this->entityTypeBundleInfo->getBundleInfo($type_id) as $bundle => $bundle_definition) {
            if (!isset($bundle_settings[$type_id][$bundle])) {
752
              self::supplementDefaultSettings('entity', $bundle_settings[$type_id][$bundle]);
753 754 755
            }
          }
        }
756 757
      }
    }
758
    return $bundle_settings;
759 760 761
  }

  /**
762 763
   * Supplements all missing link setting with default values.
   *
764
   * @param string $type
Pawel G's avatar
Pawel G committed
765 766 767
   *  'entity'|'custom'
   * @param array &$settings
   * @param array $overrides
768
   */
Pawel G's avatar
Pawel G committed
769
  public static function supplementDefaultSettings($type, &$settings, $overrides = []) {
770
    foreach (self::$allowedLinkSettings[$type] as $allowed_link_setting) {
771
      if (!isset($settings[$allowed_link_setting])
Pawel G's avatar
Pawel G committed
772
        && isset(self::$linkSettingDefaults[$allowed_link_setting])) {
773 774 775
        $settings[$allowed_link_setting] = isset($overrides[$allowed_link_setting])
          ? $overrides[$allowed_link_setting]
          : self::$linkSettingDefaults[$allowed_link_setting];
776 777
      }
    }
778 779
  }

Pawel G's avatar
Pawel G committed
780 781 782
  /**
   * Overrides entity bundle/entity type sitemap settings for a single entity.
   *
Pawel G's avatar
Pawel G committed
783 784 785
   * @param string $entity_type_id
   * @param int $id
   * @param array $settings
Pawel G's avatar
Pawel G committed
786
   *
Pawel G's avatar
Pawel G committed
787 788
   * @return $this
   */
789
  public function setEntityInstanceSettings($entity_type_id, $id, $settings) {
790
    $entity = $this->entityTypeManager->getStorage($entity_type_id)->load($id);
791 792 793
    $bundle_settings = $this->getBundleSettings(
      $entity_type_id, $this->entityHelper->getEntityInstanceBundleName($entity)
    );
794
    if (!empty($bundle_settings)) {
795 796 797 798

      // Check if overrides are different from bundle setting before saving.
      $override = FALSE;
      foreach ($settings as $key => $setting) {
799
        if (!isset($bundle_settings[$key]) || $setting != $bundle_settings[$key]) {
800 801 802 803
          $override = TRUE;
          break;
        }
      }
804

Pawel G's avatar
Pawel G committed
805 806
      // Save overrides for this entity if something is different.
      if ($override) {
807 808 809 810 811 812 813
        $this->db->merge('simple_sitemap_entity_overrides')
          ->key([
            'entity_type' => $entity_type_id,
            'entity_id' => $id])
          ->fields([
            'entity_type' => $entity_type_id,
            'entity_id' => $id,
814
            'inclusion_settings' => serialize(array_merge($bundle_settings, $settings)),])
815
          ->execute();
816
      }
Pawel G's avatar
Pawel G committed
817 818
      // Else unset override.
      else {
819
        $this->removeEntityInstanceSettings($entity_type_id, $id);
820
      }
821 822 823
    }
    else {
      //todo: log error
824
    }
Pawel G's avatar
Pawel G committed
825
    return $this;
826 827
  }

Pawel G's avatar
Pawel G committed
828
  /**
829
   * Gets sitemap settings for an entity instance which overrides the sitemap
830
   * settings of its bundle, or bundle settings, if they are not overridden.
Pawel G's avatar
Pawel G committed
831
   *
Pawel G's avatar
Pawel G committed
832
   * @param string $entity_type_id
833
   * @param int $id
Pawel G's avatar
Pawel G committed
834
   *
835
   * @return array|false
Pawel G's avatar
Pawel G committed
836
   */
837 838 839 840 841 842 843 844 845
  public function getEntityInstanceSettings($entity_type_id, $id) {
    $results = $this->db->select('simple_sitemap_entity_overrides', 'o')
      ->fields('o', ['inclusion_settings'])
      ->condition('o.entity_type', $entity_type_id)
      ->condition('o.entity_id', $id)
      ->execute()
      ->fetchField();

    if (!empty($results)) {
846
      return unserialize($results);
847 848
    }
    else {
849 850 851 852 853 854
      $entity = $this->entityTypeManager->getStorage($entity_type_id)
        ->load($id);
      return $this->getBundleSettings(
        $entity_type_id,
        $this->entityHelper->getEntityInstanceBundleName($entity)
      );
855 856 857
    }
  }

858 859 860 861 862 863 864 865 866 867 868 869
  /**
   * Removes sitemap settings for an entity that overrides the sitemap settings
   * of its bundle.
   *
   * @param string $entity_type_id
   * @param string|null $entity_ids
   *
   * @return $this
   */
  public function removeEntityInstanceSettings($entity_type_id, $entity_ids = NULL) {
    $query = $this->db->delete('simple_sitemap_entity_overrides')
      ->condition('entity_type', $entity_type_id);
870
    if (NULL !== $entity_ids) {
871 872 873 874 875 876 877
      $entity_ids = !is_array($entity_ids) ? [$entity_ids] : $entity_ids;
      $query->condition('entity_id', $entity_ids, 'IN');
    }
    $query->execute();
    return $this;
  }

Pawel G's avatar
Pawel G committed
878 879 880 881
  /**
   * Checks if an entity bundle (or a non-bundle entity type) is set to be
   * indexed in the sitemap settings.
   *
882 883
   * @param string $entity_type_id
   * @param string|null $bundle_name
Pawel G's avatar
Pawel G committed
884
   *
Pawel G's avatar
Pawel G committed
885 886
   * @return bool
   */
887
  public function bundleIsIndexed($entity_type_id, $bundle_name = NULL) {
888 889 890 891
    $settings = $this->getBundleSettings($entity_type_id, $bundle_name);
    return !empty($settings['index']);
  }

Pawel G's avatar
Pawel G committed
892 893 894
  /**
   * Checks if an entity type is enabled in the sitemap settings.
   *
895
   * @param string $entity_type_id
Pawel G's avatar
Pawel G committed
896
   *
Pawel G's avatar
Pawel G committed
897 898
   * @return bool
   */
899
  public function entityTypeIsEnabled($entity_type_id) {
900
    return in_array($entity_type_id, $this->getSetting('enabled_entity_types', []));
901 902
  }

Pawel G's avatar
Pawel G committed
903
  /**
904
   * Stores a custom path along with its sitemap settings to configuration.
Pawel G's avatar
Pawel G committed
905
   *
Pawel G's avatar
Pawel G committed
906 907
   * @param string $path
   * @param array $settings
Pawel G's avatar
Pawel G committed
908
   *
Pawel G's avatar
Pawel G committed
909
   * @return $this
910 911
   *
   * @todo Validate $settings and throw exceptions
Pawel G's avatar
Pawel G committed
912
   */
913
  public function addCustomLink($path, $settings = []) {
914
    if (!(bool) $this->pathValidator->getUrlIfValidWithoutAccessCheck($path)) {
Pawel G's avatar
Pawel G committed
915 916 917
      // todo: log error.
      return $this;
    }
Pawel G's avatar
Pawel G committed
918
    if ($path[0] !== '/') {
Pawel G's avatar
Pawel G committed
919 920 921
      // todo: log error.
      return $this;
    }
Pawel G's avatar
Pawel G committed
922