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

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

5
use Drupal\simple_sitemap\Form\FormHelper;
Pawel G's avatar
Pawel G committed
6 7 8
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Path\PathValidator;
Pawel G's avatar
Pawel G committed
9 10 11
use Drupal\Core\Entity\Query\QueryFactory;
use Drupal\Core\Config\ConfigFactory;
use Drupal\Core\Datetime\DateFormatter;
12

13
/**
Pawel G's avatar
Pawel G committed
14 15
 * Class Simplesitemap.
 *
Pawel G's avatar
Pawel G committed
16
 * @package Drupal\simple_sitemap
17 18 19
 */
class Simplesitemap {

20
  private $sitemapGenerator;
21
  private $entityHelper;
22 23
  private $configFactory;
  private $db;
24
  private $entityQuery;
25
  private $entityTypeManager;
26
  private $pathValidator;
27 28
  private static $allowed_link_settings = [
    'entity' => ['index', 'priority'],
Pawel G's avatar
Pawel G committed
29
    'custom' => ['priority'],
30
  ];
31

32 33
  /**
   * Simplesitemap constructor.
Pawel G's avatar
Pawel G committed
34
   * @param \Drupal\simple_sitemap\SitemapGenerator $sitemapGenerator
35
   * @param \Drupal\simple_sitemap\EntityHelper $entityHelper
Pawel G's avatar
Pawel G committed
36 37 38 39 40 41
   * @param \Drupal\Core\Config\ConfigFactory $configFactory
   * @param \Drupal\Core\Database\Connection $database
   * @param \Drupal\Core\Entity\Query\QueryFactory $entityQuery
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   * @param \Drupal\Core\Path\PathValidator $pathValidator
   * @param \Drupal\Core\Datetime\DateFormatter $dateFormatter
42
   */
43
  public function __construct(
Pawel G's avatar
Pawel G committed
44
    SitemapGenerator $sitemapGenerator,
45
    EntityHelper $entityHelper,
Pawel G's avatar
Pawel G committed
46
    ConfigFactory $configFactory,
Pawel G's avatar
Pawel G committed
47
    Connection $database,
Pawel G's avatar
Pawel G committed
48
    QueryFactory $entityQuery,
Pawel G's avatar
Pawel G committed
49 50
    EntityTypeManagerInterface $entityTypeManager,
    PathValidator $pathValidator,
51
    DateFormatter $dateFormatter
52 53
  ) {
    $this->sitemapGenerator = $sitemapGenerator;
54
    $this->entityHelper = $entityHelper;
Pawel G's avatar
Pawel G committed
55
    $this->configFactory = $configFactory;
56
    $this->db = $database;
57
    $this->entityQuery = $entityQuery;
58
    $this->entityTypeManager = $entityTypeManager;
59
    $this->pathValidator = $pathValidator;
60
    $this->dateFormatter = $dateFormatter;
61 62
  }

63
  /**
64
   * Fetches all sitemap chunks indexed by chunk ID.
Pawel G's avatar
Pawel G committed
65
   *
66
   * @return string
Pawel G's avatar
Pawel G committed
67
   */
Pawel G's avatar
Pawel G committed
68
  private function fetchSitemapChunks() {
69
    return $this->db
70 71 72 73
      ->query("SELECT * FROM {simple_sitemap}")
      ->fetchAllAssoc('id');
  }

74 75
  /**
   * Enables sitemap support for an entity type. Enabled entity types show
76 77
   * sitemap settings on their bundle setting forms. If an enabled entity type
   * features bundles (e.g. 'node'), it needs to be set up with
78 79 80
   * setBundleSettings() as well.
   *
   * @param string $entity_type_id
Pawel G's avatar
Pawel G committed
81
   *   Entity type id like 'node'.
82
   *
Pawel G's avatar
Pawel G committed
83
   * @return $this
84 85
   */
  public function enableEntityType($entity_type_id) {
86 87 88 89
    $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);
90
    }
Pawel G's avatar
Pawel G committed
91
    return $this;
92 93 94 95 96 97 98 99
  }

  /**
   * 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
100
   *  Entity type id like 'node'.
101
   *
Pawel G's avatar
Pawel G committed
102
   * @return $this
103 104
   */
  public function disableEntityType($entity_type_id) {
105 106 107

    // Updating settings.
    $enabled_entity_types = $this->getSetting('enabled_entity_types');
108
    if (FALSE !== ($key = array_search($entity_type_id, $enabled_entity_types))) {
109
      unset ($enabled_entity_types[$key]);
110
      $this->saveSetting('enabled_entity_types', array_values($enabled_entity_types));
111 112 113 114 115 116
    }

    // Deleting inclusion settings.
    $config_names = $this->configFactory->listAll("simple_sitemap.bundle_settings.$entity_type_id");
    foreach($config_names as $config_name) {
      $this->configFactory->getEditable($config_name)->delete();
117
    }
118 119 120

    // Deleting entity overrides.
    $this->removeEntityInstanceSettings($entity_type_id);
Pawel G's avatar
Pawel G committed
121
    return $this;
122 123 124
  }

  /**
Pawel G's avatar
Pawel G committed
125
   * Sets sitemap settings for a non-bundle entity type (e.g. user) or a bundle
126 127 128
   * of an entity type (e.g. page).
   *
   * @param string $entity_type_id
Pawel G's avatar
Pawel G committed
129
   *   Entity type id like 'node' the bundle belongs to.
130
   * @param string $bundle_name
Pawel G's avatar
Pawel G committed
131
   *   Name of the bundle. NULL if entity type has no bundles.
132
   * @param array $settings
Pawel G's avatar
Pawel G committed
133 134
   *   An array of sitemap settings for this bundle/entity type.
   *   Example: ['index' => TRUE, 'priority' => 0.5].
Pawel G's avatar
Pawel G committed
135 136
   *
   * @return $this
137 138
   */
  public function setBundleSettings($entity_type_id, $bundle_name = NULL, $settings) {
139

140
    $bundle_name = empty($bundle_name) ? $entity_type_id : $bundle_name;
141

142 143 144 145 146 147 148 149 150 151
    foreach($settings as $setting_key => $setting) {
      if ($setting_key == 'index') {
        $setting = intval($setting);
      }
      $this->configFactory
        ->getEditable("simple_sitemap.bundle_settings.$entity_type_id.$bundle_name")
        ->set($setting_key, $setting)
        ->save();
    }
    //todo: Use addLinkSettings()?
152 153

    // Delete entity overrides which are identical to new bundle setting.
154
    $sitemap_entity_types = $this->entityHelper->getSitemapEntityTypes();
155 156 157 158 159 160
    if (isset($sitemap_entity_types[$entity_type_id])) {
      $entity_type = $sitemap_entity_types[$entity_type_id];
      $keys = $entity_type->getKeys();
      $keys['bundle'] = $entity_type_id == 'menu_link_content' ? 'menu_name' : $keys['bundle'];

      $query = $this->entityQuery->get($entity_type_id);
161
      if (!$this->entityHelper->entityTypeIsAtomic($entity_type_id)) {
162
        $query->condition($keys['bundle'], $bundle_name);
163
      }
164 165 166 167 168 169 170 171 172 173 174 175
      $entity_ids = $query->execute();

      $bundle_settings = $this->configFactory
        ->get("simple_sitemap.bundle_settings.$entity_type_id.$bundle_name");

      $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');
      }

176
      foreach($query->execute()->fetchAll() as $result) {
177 178 179 180 181 182 183 184 185 186 187 188 189
        $delete = TRUE;
        $instance_settings = unserialize($result->inclusion_settings);
        foreach ($instance_settings as $setting_key => $instance_setting) {
          if ($instance_setting != $bundle_settings->get($setting_key)) {
            $delete = FALSE;
            break;
          }
        }
        if ($delete) {
          $this->db->delete('simple_sitemap_entity_overrides')
            ->condition('id', $result->id)
            ->execute();
        }
190 191
      }
    }
192 193 194
    else {
      //todo: log error
    }
Pawel G's avatar
Pawel G committed
195
    return $this;
196 197
  }

198
  /**
199 200
   * Gets sitemap settings for an entity bundle, a non-bundle entity type or for
   * all entity types and their bundles.
201
   *
202 203 204
   * @param string|null $entity_type_id
   *  If set to null, sitemap settings for all entity types and their bundles
   *  are fetched.
205 206 207
   * @param string|null $bundle_name
   *
   * @return array|false
208 209
   *  Array of sitemap settings or array of entity types with their settings.
   *  False if entity type does not exist.
210
   */
211 212 213 214 215 216 217
  public function getBundleSettings($entity_type_id = NULL, $bundle_name = NULL) {
    if (!is_null($entity_type_id)) {
      $bundle_name = empty($bundle_name) ? $entity_type_id : $bundle_name;
      $settings = $this->configFactory
        ->get("simple_sitemap.bundle_settings.$entity_type_id.$bundle_name")
        ->get();
      $bundle_settings = !empty($settings) ? $settings : FALSE;
218
    }
219 220 221 222 223 224 225 226 227 228
    else {
      $config_names = $this->configFactory->listAll("simple_sitemap.bundle_settings");
      $bundle_settings = [];
      foreach($config_names as $config_name) {
        $config_name_parts = explode('.', $config_name);
        $bundle_settings[$config_name_parts[2]][$config_name_parts[3]]
          = $this->configFactory->get($config_name)->get();
      }
    }
    return $bundle_settings;
229 230
  }

Pawel G's avatar
Pawel G committed
231 232 233
  /**
   * Overrides entity bundle/entity type sitemap settings for a single entity.
   *
Pawel G's avatar
Pawel G committed
234 235 236
   * @param string $entity_type_id
   * @param int $id
   * @param array $settings
Pawel G's avatar
Pawel G committed
237
   *
Pawel G's avatar
Pawel G committed
238 239
   * @return $this
   */
240
  public function setEntityInstanceSettings($entity_type_id, $id, $settings) {
241

242
    $entity = $this->entityTypeManager->getStorage($entity_type_id)->load($id);
243
    $bundle_name = $this->entityHelper->getEntityInstanceBundleName($entity);
244 245 246 247 248
    $bundle_settings = $this->configFactory
      ->get("simple_sitemap.bundle_settings.$entity_type_id.$bundle_name")
      ->get();

    if (!empty($bundle_settings)) {
249 250 251 252

      // Check if overrides are different from bundle setting before saving.
      $override = FALSE;
      foreach ($settings as $key => $setting) {
253
        if ($setting != $bundle_settings[$key]) {
254 255 256 257
          $override = TRUE;
          break;
        }
      }
Pawel G's avatar
Pawel G committed
258 259
      // Save overrides for this entity if something is different.
      if ($override) {
260 261 262 263 264 265 266 267 268 269
        $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,
            'inclusion_settings' => serialize($settings),
          ])
          ->execute();
270
      }
Pawel G's avatar
Pawel G committed
271 272
      // Else unset override.
      else {
273
        $this->removeEntityInstanceSettings($entity_type_id, $id);
274
      }
275 276 277
    }
    else {
      //todo: log error
278
    }
Pawel G's avatar
Pawel G committed
279
    return $this;
280 281
  }

Pawel G's avatar
Pawel G committed
282
  /**
283
   * Gets sitemap settings for an entity instance which overrides the sitemap
284
   * settings of its bundle, or bundle settings, if they are not overridden.
Pawel G's avatar
Pawel G committed
285
   *
Pawel G's avatar
Pawel G committed
286
   * @param string $entity_type_id
287
   * @param int $id
Pawel G's avatar
Pawel G committed
288
   *
289
   * @return array|false
Pawel G's avatar
Pawel G committed
290
   */
291 292 293 294 295 296 297 298 299 300 301 302
  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)) {
      return unserialize($results);
    }
    else {
303 304 305 306 307 308
      $entity = $this->entityTypeManager->getStorage($entity_type_id)
        ->load($id);
      return $this->getBundleSettings(
        $entity_type_id,
        $this->entityHelper->getEntityInstanceBundleName($entity)
      );
309 310 311
    }
  }

312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331
  /**
   * 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);
    if (!is_null($entity_ids)) {
      $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
332 333 334 335
  /**
   * Checks if an entity bundle (or a non-bundle entity type) is set to be
   * indexed in the sitemap settings.
   *
336 337
   * @param string $entity_type_id
   * @param string|null $bundle_name
Pawel G's avatar
Pawel G committed
338
   *
Pawel G's avatar
Pawel G committed
339 340
   * @return bool
   */
341
  public function bundleIsIndexed($entity_type_id, $bundle_name = NULL) {
342 343 344 345
    $settings = $this->getBundleSettings($entity_type_id, $bundle_name);
    return !empty($settings['index']);
  }

Pawel G's avatar
Pawel G committed
346 347 348
  /**
   * Checks if an entity type is enabled in the sitemap settings.
   *
349
   * @param string $entity_type_id
Pawel G's avatar
Pawel G committed
350
   *
Pawel G's avatar
Pawel G committed
351 352
   * @return bool
   */
353
  public function entityTypeIsEnabled($entity_type_id) {
354
    return in_array($entity_type_id, $this->getSetting('enabled_entity_types', []));
355 356
  }

Pawel G's avatar
Pawel G committed
357
  /**
358
   * Stores a custom path along with its sitemap settings to configuration.
Pawel G's avatar
Pawel G committed
359
   *
Pawel G's avatar
Pawel G committed
360 361
   * @param string $path
   * @param array $settings
Pawel G's avatar
Pawel G committed
362
   *
Pawel G's avatar
Pawel G committed
363
   * @return $this
Pawel G's avatar
Pawel G committed
364
   */
365
  public function addCustomLink($path, $settings) {
Pawel G's avatar
Pawel G committed
366 367 368 369 370 371 372 373
    if (!$this->pathValidator->isValid($path)) {
      // todo: log error.
      return $this;
    }
    if ($path[0] != '/') {
      // todo: log error.
      return $this;
    }
Pawel G's avatar
Pawel G committed
374

375
    $custom_links = $this->getCustomLinks();
Pawel G's avatar
Pawel G committed
376
    foreach ($custom_links as $key => $link) {
377 378 379 380 381 382 383
      if ($link['path'] == $path) {
        $link_key = $key;
        break;
      }
    }
    $link_key = isset($link_key) ? $link_key : count($custom_links);
    $custom_links[$link_key]['path'] = $path;
384 385
    $this->addLinkSettings('custom', $settings, $custom_links[$link_key]); //todo: dirty
    $this->configFactory->getEditable("simple_sitemap.custom")
386
      ->set('links', $custom_links)->save();
Pawel G's avatar
Pawel G committed
387
    return $this;
388 389
  }

390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418
  /**
   *
   */
  private function addLinkSettings($type, $settings, &$target) {
    foreach ($settings as $setting_key => $setting) {
      if (in_array($setting_key, self::$allowed_link_settings[$type])) {
        switch ($setting_key) {
          case 'priority':
            if (!FormHelper::isValidPriority($setting)) {
              // todo: log error.
              continue;
            }
            break;

          // todo: add index check.
        }
        $target[$setting_key] = $setting;
      }
    }
  }

  /**
   * Returns an array of custom paths and their sitemap settings.
   *
   * @return array
   */
  public function getCustomLinks() {
    $custom_links = $this->configFactory
      ->get('simple_sitemap.custom')
419
      ->get('links');
420
    return $custom_links !== NULL ? $custom_links : [];
421 422
  }

Pawel G's avatar
Pawel G committed
423 424 425
  /**
   * Returns settings for a custom path added to the sitemap settings.
   *
Pawel G's avatar
Pawel G committed
426
   * @param string $path
Pawel G's avatar
Pawel G committed
427
   *
Pawel G's avatar
Pawel G committed
428
   * @return array|false
Pawel G's avatar
Pawel G committed
429
   */
430
  public function getCustomLink($path) {
431
    foreach ($this->getCustomLinks() as $key => $link) {
432
      if ($link['path'] == $path) {
433
        return $link;
434 435 436 437 438
      }
    }
    return FALSE;
  }

Pawel G's avatar
Pawel G committed
439 440 441
  /**
   * Removes a custom path from the sitemap settings.
   *
Pawel G's avatar
Pawel G committed
442
   * @param string $path
Pawel G's avatar
Pawel G committed
443
   *
Pawel G's avatar
Pawel G committed
444 445
   * @return $this
   */
446
  public function removeCustomLink($path) {
447
    $custom_links = $this->getCustomLinks();
Pawel G's avatar
Pawel G committed
448
    foreach ($custom_links as $key => $link) {
449 450 451
      if ($link['path'] == $path) {
        unset($custom_links[$key]);
        $custom_links = array_values($custom_links);
452
        $this->configFactory->getEditable("simple_sitemap.custom")
453
          ->set('links', $custom_links)->save();
454
        break;
455 456
      }
    }
Pawel G's avatar
Pawel G committed
457
    return $this;
458 459
  }

Pawel G's avatar
Pawel G committed
460 461
  /**
   * Removes all custom paths from the sitemap settings.
Pawel G's avatar
Pawel G committed
462 463
   *
   * @return $this
Pawel G's avatar
Pawel G committed
464
   */
465
  public function removeCustomLinks() {
466
    $this->configFactory->getEditable("simple_sitemap.custom")
467
      ->set('links', [])->save();
Pawel G's avatar
Pawel G committed
468
    return $this;
469 470
  }

471 472 473 474
  /**
   * Returns the whole sitemap, a requested sitemap chunk,
   * or the sitemap index file.
   *
Pawel G's avatar
Pawel G committed
475
   * @param int $chunk_id
476
   *
Pawel G's avatar
Pawel G committed
477
   * @return string|false
Pawel G's avatar
Pawel G committed
478 479 480 481
   *   If no sitemap id provided, either a sitemap index is returned, or the
   *   whole sitemap, if the amount of links does not exceed the max links
   *   setting. If a sitemap id is provided, a sitemap chunk is returned. False
   *   if sitemap is not retrievable from the database.
482
   */
Pawel G's avatar
Pawel G committed
483 484
  public function getSitemap($chunk_id = NULL) {
    $chunks = $this->fetchSitemapChunks();
485
    if (is_null($chunk_id) || !isset($chunks[$chunk_id])) {
486 487

      // Return sitemap index, if there are multiple sitemap chunks.
Pawel G's avatar
Pawel G committed
488 489
      if (count($chunks) > 1) {
        return $this->getSitemapIndex($chunks);
490
      }
Pawel G's avatar
Pawel G committed
491 492
      // Return sitemap if there is only one chunk.
      else {
Pawel G's avatar
Pawel G committed
493 494
        if (isset($chunks[1])) {
          return $chunks[1]->sitemap_string;
495 496
        }
        return FALSE;
497 498
      }
    }
Pawel G's avatar
Pawel G committed
499 500
    // Return specific sitemap chunk.
    else {
Pawel G's avatar
Pawel G committed
501
      return $chunks[$chunk_id]->sitemap_string;
502
    }
503 504
  }

505 506
  /**
   * Generates the sitemap for all languages and saves it to the db.
507 508
   *
   * @param string $from
Pawel G's avatar
Pawel G committed
509 510
   *   Can be 'form', 'cron', 'drush' or 'nobatch'.
   *   This decides how the batch process is to be run.
511
   */
512
  public function generateSitemap($from = 'form') {
513 514 515 516
    $this->sitemapGenerator
      ->setGenerator($this)
      ->setGenerateFrom($from)
      ->startGeneration();
517 518
  }

519
  /**
520 521
   * Generates and returns the sitemap index as string.
   *
Pawel G's avatar
Pawel G committed
522
   * @param array $chunks
Pawel G's avatar
Pawel G committed
523
   *   Sitemap chunks which to generate the index from.
524
   *
525
   * @return string
Pawel G's avatar
Pawel G committed
526
   *   The sitemap index.
527
   */
Pawel G's avatar
Pawel G committed
528
  private function getSitemapIndex($chunks) {
529 530
    return $this->sitemapGenerator
      ->setGenerator($this)
Pawel G's avatar
Pawel G committed
531
      ->generateSitemapIndex($chunks);
Pawel G's avatar
Pawel G committed
532 533
  }

534
  /**
535 536
   * Returns a specific sitemap setting or a default value if setting does not
   * exist.
537 538
   *
   * @param string $name
Pawel G's avatar
Pawel G committed
539
   *   Name of the setting, like 'max_links'.
540
   *
541
   * @param mixed $default
Pawel G's avatar
Pawel G committed
542
   *   Value to be returned if the setting does not exist in the configuration.
543
   *
544
   * @return mixed
545
   *   The current setting from configuration or a default value.
546
   */
547
  public function getSetting($name, $default = FALSE) {
548 549 550 551
    $setting = $this->configFactory
      ->get('simple_sitemap.settings')
      ->get($name);
    return !is_null($setting) ? $setting : $default;
552
  }
553

554
  /**
555
   * Stores a specific sitemap setting in configuration.
556
   *
Pawel G's avatar
Pawel G committed
557
   * @param string $name
Pawel G's avatar
Pawel G committed
558
   *   Setting name, like 'max_links'.
Pawel G's avatar
Pawel G committed
559
   * @param mixed $setting
Pawel G's avatar
Pawel G committed
560
   *   The setting to be saved.
Pawel G's avatar
Pawel G committed
561 562
   *
   * @return $this
563
   */
564
  public function saveSetting($name, $setting) {
565 566
    $this->configFactory->getEditable("simple_sitemap.settings")
      ->set($name, $setting)->save();
Pawel G's avatar
Pawel G committed
567
    return $this;
568
  }
569

Pawel G's avatar
Pawel G committed
570 571
  /**
   * Returns a 'time ago' string of last timestamp generation.
572
   *
Pawel G's avatar
Pawel G committed
573
   * @return string|false
Pawel G's avatar
Pawel G committed
574
   *   Formatted timestamp of last sitemap generation, otherwise FALSE.
Pawel G's avatar
Pawel G committed
575
   */
576
  public function getGeneratedAgo() {
577
    $chunks = $this->fetchSitemapChunks();
Pawel G's avatar
Pawel G committed
578
    if (isset($chunks[1]->sitemap_created)) {
579
      return $this->dateFormatter
580
        ->formatInterval(REQUEST_TIME - $chunks[1]->sitemap_created);
581 582 583
    }
    return FALSE;
  }
584
}