Simplesitemap.php 32.1 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
use Drupal\simple_sitemap\Plugin\simple_sitemap\SitemapGenerator\DefaultSitemapGenerator;
use Drupal\simple_sitemap\Plugin\simple_sitemap\SitemapGenerator\SitemapGeneratorBase;
15
use Drupal\simple_sitemap\Plugin\simple_sitemap\UrlGenerator\UrlGeneratorBase;
16
use Drupal\simple_sitemap\Plugin\simple_sitemap\SitemapGenerator\SitemapGeneratorManager;
17
use Drupal\simple_sitemap\Plugin\simple_sitemap\UrlGenerator\UrlGeneratorManager;
18

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

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

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

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

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

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

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

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

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

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

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

  /**
75
   * @var \Drupal\simple_sitemap\Batch
76
   */
77
  protected $batch;
78

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

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

Pawel G's avatar
Pawel G committed
89 90 91 92 93 94 95 96 97 98
  /**
   * @var UrlGeneratorBase[] $urlGenerators
   */
  protected $urlGenerators = [];

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

Pawel G's avatar
Pawel G committed
99 100 101
  /**
   * @var array
   */
Pawel G's avatar
Pawel G committed
102
  protected static $allowedLinkSettings = [
103
    'entity' => ['index', 'priority', 'changefreq', 'include_images'],
104 105 106
    'custom' => ['priority', 'changefreq'],
  ];

Pawel G's avatar
Pawel G committed
107 108 109 110
  /**
   * @var array
   */
  protected static $linkSettingDefaults = [
111
    'index' => FALSE,
112
    'priority' => 0.5,
113
    'changefreq' => '',
114
    'include_images' => FALSE,
115
  ];
116

117 118
  /**
   * Simplesitemap constructor.
119 120
   * @param \Drupal\simple_sitemap\EntityHelper $entity_helper
   * @param \Drupal\Core\Config\ConfigFactory $config_factory
Pawel G's avatar
Pawel G committed
121
   * @param \Drupal\Core\Database\Connection $database
122
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
123
   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
124 125
   * @param \Drupal\Core\Path\PathValidator $path_validator
   * @param \Drupal\Core\Datetime\DateFormatter $date_formatter
126
   * @param \Drupal\Component\Datetime\Time $time
127
   * @param \Drupal\Core\Extension\ModuleHandler $module_handler
128
   * @param \Drupal\simple_sitemap\Batch $batch
129 130
   * @param \Drupal\simple_sitemap\Plugin\simple_sitemap\UrlGenerator\UrlGeneratorManager $url_generator_manager
   * @param \Drupal\simple_sitemap\Plugin\simple_sitemap\SitemapGenerator\SitemapGeneratorManager $sitemap_generator_manager
131
   */
132
  public function __construct(
133 134
    EntityHelper $entity_helper,
    ConfigFactory $config_factory,
Pawel G's avatar
Pawel G committed
135
    Connection $database,
136
    EntityTypeManagerInterface $entity_type_manager,
137
    EntityTypeBundleInfoInterface $entity_type_bundle_info,
138 139
    PathValidator $path_validator,
    DateFormatter $date_formatter,
140
    Time $time,
141
    ModuleHandler $module_handler,
142
    Batch $batch,
143 144
    UrlGeneratorManager $url_generator_manager,
    SitemapGeneratorManager $sitemap_generator_manager
145
  ) {
146 147
    $this->entityHelper = $entity_helper;
    $this->configFactory = $config_factory;
148
    $this->db = $database;
149
    $this->entityTypeManager = $entity_type_manager;
150
    $this->entityTypeBundleInfo = $entity_type_bundle_info;
151 152
    $this->pathValidator = $path_validator;
    $this->dateFormatter = $date_formatter;
153
    $this->time = $time;
154
    $this->moduleHandler = $module_handler;
155
    $this->batch = $batch;
156 157
    $this->urlGeneratorManager = $url_generator_manager;
    $this->sitemapGeneratorManager = $sitemap_generator_manager;
158 159
  }

160
  /**
161 162
   * Returns a specific sitemap setting or a default value if setting does not
   * exist.
Pawel G's avatar
Pawel G committed
163
   *
164
   * @param string $name
Pawel G's avatar
Pawel G committed
165
   *  Name of the setting, like 'max_links'.
166 167
   *
   * @param mixed $default
Pawel G's avatar
Pawel G committed
168
   *  Value to be returned if the setting does not exist in the configuration.
169 170
   *
   * @return mixed
Pawel G's avatar
Pawel G committed
171
   *  The current setting from configuration or a default value.
Pawel G's avatar
Pawel G committed
172
   */
173 174 175 176 177 178 179 180 181 182 183
  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
184
   *  Setting name, like 'max_links'.
185
   * @param mixed $setting
Pawel G's avatar
Pawel G committed
186
   *  The setting to be saved.
187 188 189 190
   *
   * @return $this
   */
  public function saveSetting($name, $setting) {
Pawel G's avatar
Pawel G committed
191
    $this->configFactory->getEditable('simple_sitemap.settings')
192 193 194 195
      ->set($name, $setting)->save();
    return $this;
  }

Pawel G's avatar
Pawel G committed
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
  /**
   * @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];
  }

224 225 226 227
  /**
   * Returns the whole sitemap, a requested sitemap chunk,
   * or the sitemap index file.
   *
228
   * @param string $variant
229 230
   *
   * @param int $delta
231 232
   *
   * @return string|false
Pawel G's avatar
Pawel G committed
233
   *  If no sitemap ID provided, either a sitemap index is returned, or the
234 235
   *  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
236
   *  Returns false if the sitemap is not retrievable from the database.
237
   */
Pawel G's avatar
Pawel G committed
238 239
  public function getSitemap($variant = self::DEFAULT_SITEMAP_VARIANT, $delta = NULL) {
    $chunk_info = $this->fetchSitemapVariantInfo($variant);
240

241
    if (empty($delta) || !isset($chunk_info[$delta])) {
242

243
      if (isset($chunk_info[SitemapGeneratorBase::INDEX_DELTA])) {
Pawel G's avatar
Pawel G committed
244
        // Return sitemap index if one exists.
245 246
        return $this->fetchSitemapChunk($chunk_info[SitemapGeneratorBase::INDEX_DELTA]->id)
          ->sitemap_string;
247 248
      }
      else {
Pawel G's avatar
Pawel G committed
249
        // Return sitemap chunk if there is only one chunk.
250 251
        return isset($chunk_info[SitemapGeneratorBase::FIRST_CHUNK_DELTA])
          ? $this->fetchSitemapChunk($chunk_info[SitemapGeneratorBase::FIRST_CHUNK_DELTA]->id)
252 253 254 255 256 257
            ->sitemap_string
          : FALSE;
      }
    }
    else {
      // Return specific sitemap chunk.
258
      return $this->fetchSitemapChunk($chunk_info[$delta]->id)->sitemap_string;
259 260 261 262
    }
  }

  /**
Pawel G's avatar
Pawel G committed
263
   * Fetches info about all sitemap variants and their chunks.
264
   *
265
   * @param string|null $variant
266
   *
267
   * @return array
Pawel G's avatar
Pawel G committed
268 269
   *  An array containing all sitemap chunk IDs, deltas and creation timestamps
   * keyed by their variant ID.
270
   */
Pawel G's avatar
Pawel G committed
271
  protected function fetchSitemapVariantInfo($variant = NULL) {
272 273 274
    $query = $this->db->select('simple_sitemap', 's')
      ->fields('s', ['id', 'delta', 'sitemap_created', 'type']);

275 276
    if (NULL !== $variant) {
      $query->condition('s.type', $variant);
277 278 279 280
    }

    $result = $query->execute();

281
    return NULL === $variant ? $result->fetchAllAssoc('type') : $result->fetchAllAssoc('delta');
282 283
  }

284 285 286 287 288 289 290 291 292
  /**
   * Fetches a single sitemap chunk by ID.
   *
   * @param int $id
   *   The chunk ID.
   *
   * @return object
   *   A sitemap chunk object.
   */
293
  protected function fetchSitemapChunk($id) {
294 295 296 297
    return $this->db->query('SELECT * FROM {simple_sitemap} WHERE id = :id',
      [':id' => $id])->fetchObject();
  }

298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314
  /**
   * @return array
   *
   * @todo document
   */
  public function getSitemapTypeDefinitions() {
    $type_definitions = [];
    foreach ($this->configFactory->listAll('simple_sitemap.types.') as $config_name) {
      $type_definitions[explode('.', $config_name)[2]] = $this->configFactory->get($config_name)->get();
    }

    return $type_definitions;
  }

  /**
   * @param $name
   * @param $definition
Pawel G's avatar
Pawel G committed
315
   * @return $this
316 317 318 319 320
   *
   * @todo document
   */
  public function setSitemapTypeDefinition($name, $definition) {
    $type = $this->configFactory->getEditable("simple_sitemap.types.$name");
Pawel G's avatar
Pawel G committed
321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338

    if (empty($type->get('label')) && empty($definition['label'])) {
      $definition['label'] = $name;
    }

    if (empty($type->get('description')) && empty($definition['description'])) {
      $definition['description'] = '';
    }

    if (empty($type->get('sitemap_generator')) && empty($definition['sitemap_generator'])) {
      $definition['sitemap_generator'] = self::DEFAULT_SITEMAP_GENERATOR;
    }

    if (empty($type->get('url_generators')) && empty($definition['url_generators'])) {
      //todo exception
      return $this;
    }

339 340 341 342 343
    foreach ($definition as $key => $value) {
      if (in_array($key, ['label', 'description', 'sitemap_generator', 'url_generators'])) {
        $type->set($key, $value);
      }
      else {
Pawel G's avatar
Pawel G committed
344
        //todo: exception
345 346 347
      }
    }
    $type->save();
Pawel G's avatar
Pawel G committed
348 349

    return $this;
350 351 352
  }

  /**
353
   * @param $type_name
Pawel G's avatar
Pawel G committed
354
   * @return $this
355 356 357
   *
   * @todo document
   */
358 359
  public function removeSitemapTypeDefinition($type_name) {
    if ($type_name !== self::DEFAULT_SITEMAP_TYPE) {
360 361 362 363

      // Remove variants tied to this definition.
      $variants = $this->getSitemapVariants();
      foreach ($variants as $variant_name => $variant_definition) {
364 365
        if ($variant_definition['type'] === $type_name) {
          $remove_variants[] = $variant_name;
366 367
        }
      }
368 369 370
      if (!empty($remove_variants)) {
        $this->removeSitemapVariants($remove_variants);
      }
371 372

      // Remove type definition from configuration.
373
      $this->configFactory->getEditable("simple_sitemap.types.$type_name")->delete();
374 375 376 377
    }
    else {
      //todo: exception
    }
Pawel G's avatar
Pawel G committed
378 379

    return $this;
380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399
  }

  /**
   * @return array
   *
   * @todo document
   * @todo translate label
   */
  public function getSitemapVariants() {
    return $this->configFactory->get('simple_sitemap.variants')->get('variants');
  }

  /**
   * @param $name
   * @param $definition
   * @return $this
   *
   * @todo document
   * @todo exceptions
   */
400
  public function addSitemapVariant($name, $definition = []) {
401
    if (empty($definition['label'])) {
Pawel G's avatar
Pawel G committed
402
      $definition['label'] = $name;
403 404 405
    }

    if (empty($definition['type'])) {
Pawel G's avatar
Pawel G committed
406
      $definition['type'] = self::DEFAULT_SITEMAP_TYPE;
407
    }
408 409 410 411 412 413 414
    else {
      $types = $this->getSitemapTypeDefinitions();
      if (!isset($types[$definition['type']])) {
        // todo: exception
        return $this;
      }
    }
415 416 417

    $variants = array_merge($this->getSitemapVariants(), [$name => ['label' => $definition['label'], 'type' => $definition['type']]]);
    $this->configFactory->getEditable('simple_sitemap.variants')
418 419
      ->set('variants', $variants)
      ->save();
420 421 422 423 424 425 426 427 428 429

    return $this;
  }

  /**
   * @param $name
   * @return $this
   *
   * @todo document
   */
430 431 432 433 434 435 436 437
  public function removeSitemapVariants($variant_names = NULL) {
    if (NULL === $variant_names) {
      $this->removeSitemap();
      $variants = [];
      $save = TRUE;
    }
    else {
      $this->removeSitemap($variant_names);
438

439 440 441 442 443 444 445 446 447 448
      $variants = $this->getSitemapVariants();
      foreach ((array) $variant_names as $variant_name) {
        foreach ($variants as $variant => $variant_definition) {
          if (isset($variants[$variant_name])) {
            unset($variants[$variant_name]);
            $save = TRUE;
          }
        }
      }
    }
449

450
    if (!empty($save)) {
Pawel G's avatar
Pawel G committed
451 452 453
      $this->configFactory->getEditable('simple_sitemap.variants')
        ->set('variants', $variants)->save();
    }
454 455 456 457

    return $this;
  }

458 459 460
  /**
   * @param null $variants
   * @return $this
461
   * @throws \Drupal\Component\Plugin\Exception\PluginException
462
   *
463
   * @todo document
464 465 466
   */
  public function removeSitemap($variants = NULL) {
    $variant_definitions = $this->getSitemapVariants();
467
    // $this->moduleHandler->alter('simple_sitemap_variants', $variant_definitions); //todo Is this necessary?
468 469 470 471 472 473 474 475 476 477 478 479 480
    $remove_variants = [];
    if (NULL === $variants) {
      $remove_variants = $variant_definitions;
    }
    else {
      foreach ((array) $variants as $variant) {
        if (isset($variant_definitions[$variant])) {
          $remove_variants[$variant] = $variant_definitions[$variant];
        }
      }
    }
    if (!empty($remove_variants)) {
      $type_definitions = $this->getSitemapTypeDefinitions();
481

482
      foreach ($remove_variants as $variant_name => $variant_definition) {
Pawel G's avatar
Pawel G committed
483
        $this->getSitemapGenerator($type_definitions[$variant_definition['type']]['sitemap_generator'])
484
          ->setSitemapVariant($variant_name)
485 486
          ->remove()
          ->invalidateCache();
487
      }
488

489 490 491 492
    }
    return $this;
  }

493
  /**
Pawel G's avatar
Pawel G committed
494
   * Generates the XML sitemap and saves it to the database.
495 496
   *
   * @param string $from
Pawel G's avatar
Pawel G committed
497 498
   *  Can be 'form', 'backend', 'drush' or 'nobatch'.
   *  This decides how the batch process is to be run.
Pawel G's avatar
Pawel G committed
499
   *
500
   * @param array|string|null $variants
501
   *
Pawel G's avatar
Pawel G committed
502
   * @return bool|\Drupal\simple_sitemap\Simplesitemap
503
   */
504 505
  public function generateSitemap($from = 'form', $variants = NULL) {
    $variants = NULL === $variants ? NULL : (array) $variants;
506

507
    $settings = [
508
      'base_url' => $this->getSetting('base_url', ''),
509
      'batch_process_limit' => $this->getSetting('batch_process_limit', 1500),
510 511 512 513
      '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', []),
514
    ];
515

516 517
    $this->batch->setBatchMeta(['from' => $from]);

518
    $operations = [];
Pawel G's avatar
Pawel G committed
519

520 521
    $type_definitions = $this->getSitemapTypeDefinitions();
    $this->moduleHandler->alter('simple_sitemap_types', $type_definitions);
Pawel G's avatar
Pawel G committed
522

523 524
    $sitemap_variants = $this->getSitemapVariants();
    $this->moduleHandler->alter('simple_sitemap_variants', $sitemap_variants);
525

526 527 528 529 530 531
    foreach ($sitemap_variants as $variant_name => $variant_definition) {

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

533 534 535 536 537 538 539 540 541 542 543 544 545
      $type = $variant_definition['type'];

      // Adding a remove_sitemap operation for all sitemap variants.
      $operations[] = [
        'operation' => 'removeSitemap',
        'arguments' => [
          'sitemap_generator' => $type_definitions[$type]['sitemap_generator'],
          'variant' => $variant_name,
        ]
      ];

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

Pawel G's avatar
Pawel G committed
547
        foreach ($this->getUrlGenerator($url_generator_id)
548 549 550 551 552
                   ->setSitemapVariant($variant_name)
                   ->getDataSets() as $data_set) {
          if (!empty($data_set)) {
            $operations[] = [
              'operation' => 'generateSitemap',
553
              'arguments' => [
554 555 556 557 558 559
                'url_generator' => $url_generator_id,
                'data_set' => $data_set,
                'variant' => $variant_name,
                'sitemap_generator' => $type_definitions[$type]['sitemap_generator'],
                'settings' => $settings,
              ],
560 561
            ];
          }
562
        }
563
      }
564

565 566
      // Adding generate_index operations for all sitemap variants.
      $operations[] = [
567 568
        'operation' => 'generateIndex',
        'arguments' => [
569 570
          'sitemap_generator' => $type_definitions[$type]['sitemap_generator'],
          'variant' => $variant_name,
571 572 573 574 575
          'settings' => $settings,
        ],
      ];
    }

576 577 578 579
    // Adding operations to and starting batch.
    if (!empty($operations)) {
      foreach ($operations as $operation_data) {
        $this->batch->addOperation($operation_data['operation'], $operation_data['arguments']);
580 581 582 583 584
      }
      $success = $this->batch->start();
    }

    return $from === 'nobatch' ? $this : (isset($success) ? $success : FALSE);
585 586 587 588 589
  }

  /**
   * Returns a 'time ago' string of last timestamp generation.
   *
590
   * @param string|null $variant
591 592
   *
   * @return string|array|false
Pawel G's avatar
Pawel G committed
593
   *  Formatted timestamp of last sitemap generation, otherwise FALSE.
594
   */
595
  public function getGeneratedAgo($variant = NULL) {
Pawel G's avatar
Pawel G committed
596
    $chunks = $this->fetchSitemapVariantInfo($variant);
597
    if ($variant !== NULL) {
598
      return isset($chunks[DefaultSitemapGenerator::FIRST_CHUNK_DELTA]->sitemap_created)
599
        ? $this->dateFormatter
600
          ->formatInterval($this->time->getRequestTime() - $chunks[DefaultSitemapGenerator::FIRST_CHUNK_DELTA]
601 602 603 604 605 606 607 608 609 610 611 612
              ->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;
613 614 615
    }
  }

616 617
  /**
   * Enables sitemap support for an entity type. Enabled entity types show
618 619
   * sitemap settings on their bundle setting forms. If an enabled entity type
   * features bundles (e.g. 'node'), it needs to be set up with
620 621 622
   * setBundleSettings() as well.
   *
   * @param string $entity_type_id
Pawel G's avatar
Pawel G committed
623
   *  Entity type id like 'node'.
624
   *
Pawel G's avatar
Pawel G committed
625
   * @return $this
626 627
   */
  public function enableEntityType($entity_type_id) {
628 629 630 631
    $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);
632
    }
Pawel G's avatar
Pawel G committed
633
    return $this;
634 635 636 637 638 639 640 641
  }

  /**
   * 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
642
   *  Entity type id like 'node'.
643
   *
Pawel G's avatar
Pawel G committed
644
   * @return $this
645 646
   */
  public function disableEntityType($entity_type_id) {
647 648 649

    // Updating settings.
    $enabled_entity_types = $this->getSetting('enabled_entity_types');
650
    if (FALSE !== ($key = array_search($entity_type_id, $enabled_entity_types))) {
651
      unset ($enabled_entity_types[$key]);
652
      $this->saveSetting('enabled_entity_types', array_values($enabled_entity_types));
653 654 655
    }

    // Deleting inclusion settings.
656
    $config_names = $this->configFactory->listAll("simple_sitemap.bundle_settings.$entity_type_id.");
Pawel G's avatar
Pawel G committed
657
    foreach ($config_names as $config_name) {
658
      $this->configFactory->getEditable($config_name)->delete();
659
    }
660 661 662

    // Deleting entity overrides.
    $this->removeEntityInstanceSettings($entity_type_id);
Pawel G's avatar
Pawel G committed
663
    return $this;
664 665 666
  }

  /**
Pawel G's avatar
Pawel G committed
667
   * Sets sitemap settings for a non-bundle entity type (e.g. user) or a bundle
668 669 670
   * of an entity type (e.g. page).
   *
   * @param string $entity_type_id
Pawel G's avatar
Pawel G committed
671
   *  Entity type id like 'node' the bundle belongs to.
672
   * @param string $bundle_name
Pawel G's avatar
Pawel G committed
673
   *  Name of the bundle. NULL if entity type has no bundles.
674
   * @param array $settings
Pawel G's avatar
Pawel G committed
675 676
   *  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
677 678
   *
   * @return $this
679 680
   *
   * @todo: enableEntityType automatically
681
   */
682
  public function setBundleSettings($entity_type_id, $bundle_name = NULL, $settings = ['index' => TRUE]) {
683
    $bundle_name = NULL !== $bundle_name ? $bundle_name : $entity_type_id;
684

Pawel G's avatar
Pawel G committed
685 686 687
    if (!empty($old_settings = $this->getBundleSettings($entity_type_id, $bundle_name))) {
      $settings = array_merge($old_settings, $settings);
    }
688
    self::supplementDefaultSettings('entity', $settings);
689

690
    if ($settings != $old_settings) {
691

692 693 694 695 696 697 698
      // 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();
699

700 701 702 703 704
      // 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();
705

706 707
        // Menu fix.
        $keys['bundle'] = $entity_type_id === 'menu_link_content' ? 'menu_name' : $keys['bundle'];
708

709 710 711 712 713 714 715 716 717 718 719 720
        $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');
        }
721

722 723 724 725 726 727 728 729 730 731 732 733
        $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;
734 735
          }
        }
736 737 738 739
        if (!empty($delete_instances)) {
          $this->db->delete('simple_sitemap_entity_overrides')
            ->condition('id', $delete_instances, 'IN')
            ->execute();
740
        }
741
      }
742 743
      else {
        //todo: log error
744
      }
745
    }
746

Pawel G's avatar
Pawel G committed
747
    return $this;
748 749
  }

750
  /**
751 752
   * Gets sitemap settings for an entity bundle, a non-bundle entity type or for
   * all entity types and their bundles.
753
   *
754 755 756
   * @param string|null $entity_type_id
   *  If set to null, sitemap settings for all entity types and their bundles
   *  are fetched.
757 758 759
   * @param string|null $bundle_name
   *
   * @return array|false
Pawel G's avatar
Pawel G committed
760 761
   *  Array of sitemap settings for an entity bundle, a non-bundle entity type
   *  or for all entity types and their bundles.
762
   *  False if entity type does not exist.
763
   */
764
  public function getBundleSettings($entity_type_id = NULL, $bundle_name = NULL) {
765
    if (NULL !== $entity_type_id) {
766 767

      // Get bundle settings saved in simple_sitemap.bundle_settings.*.* configuration.
768
      $bundle_name = NULL !== $bundle_name ? $bundle_name : $entity_type_id;
769
      $bundle_settings = $this->configFactory
770 771
        ->get("simple_sitemap.bundle_settings.$entity_type_id.$bundle_name")
        ->get();
772 773 774 775 776

      // 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])) {
777
          self::supplementDefaultSettings('entity', $bundle_settings);
778 779 780 781 782
        }
        else {
          $bundle_settings = FALSE;
        }
      }
783
    }
784
    else {
785
      // Get all bundle settings saved in simple_sitemap.bundle_settings.*.* configuration.
Pawel G's avatar
Pawel G committed
786
      $config_names = $this->configFactory->listAll('simple_sitemap.bundle_settings.');
787
      $bundle_settings = [];
Pawel G's avatar
Pawel G committed
788
      foreach ($config_names as $config_name) {
789
        $config_name_parts = explode('.', $config_name);
790 791 792 793 794 795 796 797
        $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])) {
798
              self::supplementDefaultSettings('entity', $bundle_settings[$type_id][$bundle]);
799 800 801
            }
          }
        }
802 803
      }
    }
804
    return $bundle_settings;
805 806 807
  }

  /**
808 809
   * Supplements all missing link setting with default values.
   *
810
   * @param string $type
Pawel G's avatar
Pawel G committed
811 812 813
   *  'entity'|'custom'
   * @param array &$settings
   * @param array $overrides
814
   */
Pawel G's avatar
Pawel G committed
815
  public static function supplementDefaultSettings($type, &$settings, $overrides = []) {
816
    foreach (self::$allowedLinkSettings[$type] as $allowed_link_setting) {
817
      if (!isset($settings[$allowed_link_setting])
Pawel G's avatar
Pawel G committed
818
        && isset(self::$linkSettingDefaults[$allowed_link_setting])) {
819 820 821
        $settings[$allowed_link_setting] = isset($overrides[$allowed_link_setting])
          ? $overrides[$allowed_link_setting]
          : self::$linkSettingDefaults[$allowed_link_setting];
822 823
      }
    }
824 825
  }

Pawel G's avatar
Pawel G committed
826 827 828
  /**
   * Overrides entity bundle/entity type sitemap settings for a single entity.
   *
Pawel G's avatar
Pawel G committed
829 830 831
   * @param string $entity_type_id
   * @param int $id
   * @param array $settings
Pawel G's avatar
Pawel G committed
832
   *
Pawel G's avatar
Pawel G committed
833 834
   * @return $this
   */
835
  public function setEntityInstanceSettings($entity_type_id, $id, $settings) {
836
    $entity = $this->entityTypeManager->getStorage($entity_type_id)->load($id);
837 838 839
    $bundle_settings = $this->getBundleSettings(
      $entity_type_id, $this->entityHelper->getEntityInstanceBundleName($entity)
    );
840
    if (!empty($bundle_settings)) {
841 842 843 844

      // Check if overrides are different from bundle setting before saving.
      $override = FALSE;
      foreach ($settings as $key => $setting) {
845
        if (!isset($bundle_settings[$key]) || $setting != $bundle_settings[$key]) {
846 847 848 849
          $override = TRUE;
          break;
        }
      }
850

Pawel G's avatar
Pawel G committed
851 852
      // Save overrides for this entity if something is different.
      if ($override) {
853 854 855 856 857 858 859
        $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,
860
            'inclusion_settings' => serialize(array_merge($bundle_settings, $settings)),])
861
          ->execute();
862
      }
Pawel G's avatar
Pawel G committed
863 864
      // Else unset override.
      else {
865
        $this->removeEntityInstanceSettings($entity_type_id, $id);
866
      }
867 868 869
    }
    else {
      //todo: log error
870
    }
Pawel G's avatar
Pawel G committed
871
    return $this;
872 873
  }

Pawel G's avatar
Pawel G committed
874
  /**
875
   * Gets sitemap settings for an entity instance which overrides the sitemap
876
   * settings of its bundle, or bundle settings, if they are not overridden.
Pawel G's avatar
Pawel G committed
877
   *
Pawel G's avatar
Pawel G committed
878
   * @param string $entity_type_id
879
   * @param int $id
Pawel G's avatar
Pawel G committed
880
   *
881
   * @return array|false
Pawel G's avatar
Pawel G committed
882
   */
883 884 885 886 887 888 889 890 891
  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)) {
892
      return unserialize($results);
893 894
    }
    else {
895 896 897 898 899 900
      $entity = $this->entityTypeManager->getStorage($entity_type_id)
        ->load($id);
      return $this->getBundleSettings(
        $entity_type_id,
        $this->entityHelper->getEntityInstanceBundleName($entity)
      );
901 902 903
    }
  }

904 905 906 907 908 909 910 911 912 913 914 915
  /**
   * 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);
916
    if (NULL !== $entity_ids) {
917 918 919 920 921 922 923
      $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
924 925 926 927
  /**
   * Checks if an entity bundle (or a non-bundle entity type) is set to be
   * indexed in the sitemap settings.
   *
928 929
   * @param string $entity_type_id
   * @param string|null $bundle_name
Pawel G's avatar
Pawel G committed
930
   *
Pawel G's avatar
Pawel G committed
931 932
   * @return bool
   */
933
  public function bundleIsIndexed($entity_type_id, $bundle_name = NULL) {
934 935 936 937
    $settings = $this->getBundleSettings($entity_type_id, $bundle_name);
    return !empty($settings['index']);
  }

Pawel G's avatar
Pawel G committed
938 939 940
  /**
   * Checks if an entity type is enabled in the sitemap settings.
   *
941
   * @param string $entity_type_id
Pawel G's avatar
Pawel G committed
942
   *
Pawel G's avatar
Pawel G committed
943 944
   * @return bool
   */
945
  public function entityTypeIsEnabled($entity_type_id) {
946
    return in_array($entity_type_id, $this->getSetting('enabled_entity_types', []));
947 948
  }

Pawel G's avatar
Pawel G committed
949
  /**
950
   * Stores a custom path along with its sitemap settings to configuration.
Pawel G's avatar
Pawel G committed
951
   *
Pawel G's avatar
Pawel G committed
952 953
   * @param string $path
   * @param array $settings
Pawel G's avatar
Pawel G committed
954
   *
Pawel G's avatar
Pawel G committed
955
   * @return $this
956 957
   *
   * @todo Validate $settings and throw exceptions
Pawel G's avatar
Pawel G committed
958
   */
959
  public function addCustomLink($path, $settings = []) {
960
    if (!(bool) $this->pathValidator->getUrlIfValidWithoutAccessCheck($path)) {
Pawel G's avatar
Pawel G committed
961 962 963
      // todo: log error.
      return $this;
    }
Pawel G's avatar
Pawel G committed
964
    if ($path[0] !== '/') {
Pawel G's avatar
Pawel G committed
965 966 967
      // todo: log error.
      return $this;
    }
Pawel G's avatar
Pawel G committed
968

969
    $custom_links = $this->getCustomLinks(FALSE);
Pawel G's avatar
Pawel G committed
970
    foreach ($custom_links as $key => $link) {
971
      if ($link['path'] === $path) {
972 973 974 975 976
        $link_key = $key;
        break;
      }
    }
    $link_key = isset($link_key) ? $link_key : count($custom_links);
977
    $custom_links[$link_key] = ['path' => $path] + $settings;
Pawel G's avatar
Pawel G committed
978
    $this->configFactory->getEditable('simple_sitemap.custom')
979
      ->set('links', $custom_links)->save();
980

Pawel G's avatar
Pawel G committed
981
    return $this;
982 983
  }

984 985 986
  /**
   * Returns an array of custom paths and their sitemap settings.
   *
987
   * @param bool $supplement_default_settings
988 989
   * @return array
   */
990
  public function getCustomLinks($supplement_default_settings = TRUE) {
991 992
    $custom_links = $this->configFactory
      ->get('simple_sitemap.custom')
993
      ->get('links');
994 995 996

    if ($supplement_default_settings) {
      foreach ($custom_links as $i => $link_settings) {
Pawel G's avatar
Pawel G committed
997 998
        self::supplementDefaultSettings('custom', $link_settings);
        $custom_links[$i] = $link_settings;
999 1000 1001
      }
    }

1002
    return !empty($custom_links) ? $custom_links : [];
1003 1004
  }

Pawel G's avatar
Pawel G committed
1005 1006 1007
  /**
   * Returns settings for a custom path added to the sitemap settings.
   *
Pawel G's avatar
Pawel G committed
1008
   * @param string $path
Pawel G's avatar
Pawel G committed
1009
   *
Pawel G's avatar
Pawel G committed
1010
   * @return array|false
Pawel G's avatar
Pawel G committed
1011
   */
1012
  public function getCustomLink($path) {
1013
    foreach ($this->getCustomLinks() as $key => $link) {
1014
      if ($link['path'] === $path) {
1015
        return $link;
1016 1017 1018 1019 1020
      }
    }
    return FALSE;
  }

Pawel G's avatar
Pawel G committed
1021
  /**
1022
   * Removes all custom paths from the sitemap settings.
Pawel G's avatar
Pawel G committed
1023
   *
Pawel G's avatar
Pawel G committed
1024 1025
   * @return $this
   */
1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039
  public function removeCustomLinks($paths = NULL) {
    if (NULL === $paths) {
      $custom_links = [];
      $save = TRUE;
    }
    else {
      $custom_links = $this->getCustomLinks(FALSE);
      foreach ((array) $paths as $path) {
        foreach ($custom_links as $key => $link) {
          if ($link['path'] === $path) {
            unset($custom_links[$key]);
            $save = TRUE;
          }
        }
1040 1041
      }
    }
1042 1043 1044 1045
    if (!empty($save)) {
      $this->configFactory->getEditable('simple_sitemap.custom')
        ->set('links', array_values($custom_links))->save();
    }
1046

Pawel G's avatar
Pawel G committed
1047
    return $this;
1048
  }
1049
}