xmlsitemap.module 83.3 KB
Newer Older
Darren Oh's avatar
Darren Oh committed
1 2 3
<?php

/**
4
 * @file
5
 * @defgroup xmlsitemap XML sitemap
Darren Oh's avatar
Darren Oh committed
6 7 8
 */

/**
9 10 11
 * @file
 * Main file for the xmlsitemap module.
 */
12

13
use Drupal\Component\Utility\Crypt;
14
use Drupal\Component\Utility\Environment;
15
use Drupal\Component\Utility\UrlHelper;
16 17
use Drupal\Core\Cache\Cache;
use Drupal\Core\Database\Query\AlterableInterface;
18
use Drupal\Core\Database\Query\Condition;
19
use Drupal\Core\Entity\ContentEntityFormInterface;
20
use Drupal\Core\Entity\ContentEntityInterface;
21
use Drupal\Core\Entity\ContentEntityTypeInterface;
22
use Drupal\Core\Entity\EntityInterface;
23
use Drupal\Core\Entity\EntityTypeInterface;
24
use Drupal\Core\Entity\Query\QueryInterface;
25
use Drupal\Core\File\Exception\FileException;
26
use Drupal\Core\File\FileSystemInterface;
27
use Drupal\Core\Form\FormStateInterface;
28
use Drupal\Core\Language\LanguageInterface;
29 30
use Drupal\Core\Link;
use Drupal\Core\Routing\RouteMatchInterface;
31
use Drupal\Core\Site\Settings;
32
use Drupal\Core\Url;
33
use Drupal\xmlsitemap\Controller\XmlSitemapController;
34 35
use Drupal\xmlsitemap\Entity\XmlSitemap;
use Drupal\xmlsitemap\XmlSitemapInterface;
36
use Symfony\Component\HttpFoundation\Request;
37

38 39 40
/**
 * The maximum number of links in one sitemap chunk file.
 */
41
const XMLSITEMAP_MAX_SITEMAP_LINKS = 50000;
42

43 44 45
/**
 * The maximum filesize of a sitemap chunk file.
 */
46
const XMLSITEMAP_MAX_SITEMAP_FILESIZE = 52528800;
47

48
/**
49
 * Xmlsitemap Frequencies.
50
 */
51
const XMLSITEMAP_FREQUENCY_YEARLY = 31449600;
52
// 60 * 60 * 24 * 7 * 52.
53
const XMLSITEMAP_FREQUENCY_MONTHLY = 2419200;
54
// 60 * 60 * 24 * 7 * 4.
55
const XMLSITEMAP_FREQUENCY_WEEKLY = 604800;
56
// 60 * 60 * 24 * 7.
57
const XMLSITEMAP_FREQUENCY_DAILY = 86400;
58
// 60 * 60 * 24.
59
const XMLSITEMAP_FREQUENCY_HOURLY = 3600;
60
// 60 * 60.
61
const XMLSITEMAP_FREQUENCY_ALWAYS = 60;
62

63 64 65
/**
 * Short lastmod timestamp format.
 */
66
const XMLSITEMAP_LASTMOD_SHORT = 'Y-m-d';
67 68 69 70

/**
 * Medium lastmod timestamp format.
 */
71
const XMLSITEMAP_LASTMOD_MEDIUM = 'Y-m-d\TH:i\Z';
72 73 74 75

/**
 * Long lastmod timestamp format.
 */
76
const XMLSITEMAP_LASTMOD_LONG = 'c';
77

78 79 80
/**
 * The default inclusion status for link types in the sitemaps.
 */
81
const XMLSITEMAP_STATUS_DEFAULT = 0;
82 83 84 85

/**
 * The default priority for link types in the sitemaps.
 */
86
const XMLSITEMAP_PRIORITY_DEFAULT = 0.5;
87

88 89 90 91
/**
 * Implements hook_hook_info().
 */
function xmlsitemap_hook_info() {
92
  $hooks = [
93 94 95 96 97 98
    'xmlsitemap_link_info',
    'xmlsitemap_link_info_alter',
    'xmlsitemap_link_alter',
    'xmlsitemap_index_links',
    'xmlsitemap_context_info',
    'xmlsitemap_context_info_alter',
99
    'xmlsitemap_context_url_options',
100
    'xmlsitemap_context',
101 102
    'xmlsitemap_element_alter',
    'xmlsitemap_root_attributes_alter',
103 104
    'xmlsitemap_sitemap_insert',
    'xmlsitemap_sitemap_update',
105 106 107 108
    'xmlsitemap_sitemap_operations',
    'xmlsitemap_sitemap_delete',
    'xmlsitemap_sitemap_link_url_options_alter',
    'query_xmlsitemap_generate_alter',
109
    'query_xmlsitemap_link_bundle_access_alter',
110
    'form_xmlsitemap_sitemap_edit_form_alter',
111
    'xmlsitemap_rebuild_clear',
112
  ];
113 114 115

  $hooks = array_combine($hooks, $hooks);
  foreach ($hooks as $hook => $info) {
116
    $hooks[$hook] = ['group' => 'xmlsitemap'];
117
  }
118

119 120 121
  return $hooks;
}

122 123
/**
 * Implements hook_help().
Darren Oh's avatar
Darren Oh committed
124
 */
125
function xmlsitemap_help($route_name, RouteMatchInterface $route_match) {
126 127
  $output = '';

128
  switch ($route_name) {
129
    case 'help.page.xmlsitemap':
130 131
    case 'xmlsitemap.admin_settings':
    case 'xmlsitemap.entities_settings':
132 133
    case 'entity.xmlsitemap.edit_form':
    case 'entity.xmlsitemap.delete_form':
134
      return;
135

136
    case 'xmlsitemap.admin_search':
137
      break;
138

139
    case 'xmlsitemap.admin_search_list':
140
      break;
141

142
    case 'xmlsitemap.admin_rebuild':
143 144 145
      $output .= '<p>' . t("This action rebuilds your site's XML sitemap and regenerates the cached files, and may be a lengthy process. If you just installed XML sitemap, this can be helpful to import all your site's content into the sitemap. Otherwise, this should only be used in emergencies.") . '</p>';
  }

146
  $currentUser = \Drupal::currentUser();
147
  if (strpos($route_name, 'xmlsitemap') !== FALSE && $currentUser->hasPermission('administer xmlsitemap')) {
148
    // Alert the user to any potential problems detected by hook_requirements.
149
    $output .= _xmlsitemap_get_blurb();
Darren Oh's avatar
Darren Oh committed
150
  }
151 152 153 154

  return $output;
}

155 156 157 158
/**
 * Implements hook_theme().
 */
function xmlsitemap_theme() {
159 160
  return [
    'xmlsitemap_content_settings_table' => [
161 162
      'render element' => 'element',
      'file' => 'xmlsitemap.module',
163 164
    ],
  ];
165 166
}

167 168
/**
 * Menu access callback; determines if the user can use the rebuild links page.
169 170 171
 *
 * @return bool
 *   Returns TRUE if current user can access rebuild form. FALSE otherwise.
172 173 174
 */
function _xmlsitemap_rebuild_form_access() {
  $rebuild_types = xmlsitemap_get_rebuildable_link_types();
175
  return !empty($rebuild_types) && \Drupal::currentUser()->hasPermission('administer xmlsitemap');
176 177
}

Darren Oh's avatar
Darren Oh committed
178
/**
179
 * Implements hook_cron().
180 181 182
 *
 * @todo Use new Queue system. Need to add {sitemap}.queued.
 * @todo Regenerate one at a time?
Darren Oh's avatar
Darren Oh committed
183
 */
184
function xmlsitemap_cron() {
185 186 187 188 189
  // If cron sitemap file regeneration is disabled, stop.
  if (\Drupal::config('xmlsitemap.settings')->get('disable_cron_regeneration')) {
    return;
  }

190
  // If there were no new or changed links, skip.
191
  if (!\Drupal::state()->get('xmlsitemap_regenerate_needed')) {
192 193 194
    return;
  }

195
  // If the minimum sitemap lifetime hasn't been passed, skip.
196
  $lifetime = \Drupal::time()->getRequestTime() - \Drupal::state()->get('xmlsitemap_generated_last');
197
  if ($lifetime < \Drupal::config('xmlsitemap.settings')->get('minimum_lifetime')) {
198 199
    return;
  }
200
  xmlsitemap_xmlsitemap_index_links(\Drupal::config('xmlsitemap.settings')->get('batch_limit'));
201
  // Regenerate the sitemap XML files.
202
  xmlsitemap_run_unprogressive_batch('xmlsitemap_regenerate_batch');
Darren Oh's avatar
Darren Oh committed
203 204
}

205
/**
206
 * Implements hook_modules_installed().
207
 */
208
function xmlsitemap_modules_installed(array $modules) {
209
  Cache::invalidateTags(['xmlsitemap']);
210 211 212
}

/**
213
 * Implements hook_modules_uninstalled().
214
 */
215
function xmlsitemap_modules_uninstalled(array $modules) {
216
  Cache::invalidateTags(['xmlsitemap']);
217 218
}

Darren Oh's avatar
Darren Oh committed
219
/**
220
 * Implements hook_robotstxt().
Darren Oh's avatar
Darren Oh committed
221
 */
222
function xmlsitemap_robotstxt() {
223
  if ($sitemap = XmlSitemap::loadByContext()) {
224
    $uri = xmlsitemap_sitemap_uri($sitemap);
225 226
    $path = UrlHelper::isExternal($uri['path']) ? $uri['path'] : 'base://' . $uri['path'];
    $robotstxt[] = 'Sitemap: ' . Url::fromUri($path, $uri['options'])->toString();
227
    return $robotstxt;
228
  }
Darren Oh's avatar
Darren Oh committed
229 230
}

231
/**
232
 * Internal default variables config for xmlsitemap_var().
233 234 235
 *
 * @return array
 *   Array with config variables of xmlsitemap.settings config object.
236
 */
237
function xmlsitemap_config_variables() {
238
  return [
239
    'minimum_lifetime' => 0,
240 241 242 243 244 245 246 247 248
    'xsl' => 1,
    'prefetch_aliases' => 1,
    'chunk_size' => 'auto',
    'batch_limit' => 100,
    'path' => 'xmlsitemap',
    'frontpage_priority' => 1.0,
    'frontpage_changefreq' => XMLSITEMAP_FREQUENCY_DAILY,
    'lastmod_format' => XMLSITEMAP_LASTMOD_MEDIUM,
    'gz' => FALSE,
249
    'disable_cron_regeneration' => FALSE,
250
  ];
251 252
}

253 254
/**
 * Internal default variables state for xmlsitemap_var().
255 256 257
 *
 * @return array
 *   Array with state variables defined by xmlsitemap module.
258 259
 */
function xmlsitemap_state_variables() {
260
  return [
261 262 263 264
    'xmlsitemap_rebuild_needed' => FALSE,
    'xmlsitemap_regenerate_needed' => TRUE,
    'xmlsitemap_base_url' => '',
    'xmlsitemap_generated_last' => 0,
265 266
    'xmlsitemap_developer_mode' => 0,
    'max_chunks' => NULL,
267
    'max_filesize' => NULL,
268
  ];
269 270
}

271 272 273 274 275 276
/**
 * Internal implementation of variable_get().
 */
function xmlsitemap_var($name, $default = NULL) {
  $defaults = &drupal_static(__FUNCTION__);
  if (!isset($defaults)) {
277 278
    $defaults = xmlsitemap_config_variables();
    $defaults += xmlsitemap_state_variables();
279 280
  }

281 282
  // @todo Remove when stable.
  if (!isset($defaults[$name])) {
283
    trigger_error("Default variable for $name not found.");
284 285
  }

286 287 288 289
  if (\Drupal::state()->get($name, NULL) === NULL) {
    return \Drupal::config('xmlsitemap.settings')->get($name);
  }
  return \Drupal::state()->get($name);
290 291
}

Darren Oh's avatar
Darren Oh committed
292
/**
293
 * @defgroup xmlsitemap_api XML sitemap API.
294
 * @{
295 296
 * This is the XML sitemap API to be used by modules wishing to work with
 * XML sitemap and/or link data.
Darren Oh's avatar
Darren Oh committed
297
 */
298 299 300 301

/**
 * Load an XML sitemap array from the database.
 *
302
 * @param mixed $smid
303
 *   An XML sitemap ID.
304
 *
305
 * @return \Drupal\xmlsitemap\XmlSitemapInterface
306
 *   The XML sitemap object.
307 308
 */
function xmlsitemap_sitemap_load($smid) {
309
  $sitemap = xmlsitemap_sitemap_load_multiple([$smid]);
310
  return $sitemap ? reset($sitemap) : FALSE;
Darren Oh's avatar
Darren Oh committed
311 312 313
}

/**
314 315
 * Load multiple XML sitemaps from the database.
 *
316
 * @param array|bool $smids
317
 *   An array of XML sitemap IDs, or FALSE to load all XML sitemaps.
318
 * @param array $conditions
319
 *   An array of conditions in the form 'field' => $value.
320
 *
321
 * @return \Drupal\xmlsitemap\XmlSitemapInterface[]
322
 *   An array of XML sitemap objects.
Darren Oh's avatar
Darren Oh committed
323
 */
324
function xmlsitemap_sitemap_load_multiple($smids = [], array $conditions = []) {
325 326 327
  if ($smids !== FALSE) {
    $conditions['smid'] = $smids;
  }
328
  else {
329
    $conditions['smid'] = NULL;
330
  }
331
  $storage = Drupal::entityTypeManager()->getStorage('xmlsitemap');
332

333
  /** @var \Drupal\xmlsitemap\XmlSitemapInterface[] $sitemaps */
334
  $sitemaps = $storage->loadMultiple($conditions['smid']);
335
  if (count($sitemaps) <= 0) {
336
    return [];
337
  }
338

339
  return $sitemaps;
340 341
}

Darren Oh's avatar
Darren Oh committed
342
/**
343
 * Save changes to an XML sitemap or add a new XML sitemap.
344
 *
345
 * @param Drupal\xmlsitemap\XmlSitemapInterface $sitemap
346
 *   The XML sitemap array to be saved. If $sitemap->smid is omitted, a new
347 348 349
 *   XML sitemap will be added.
 *
 * @todo Save the sitemap's URL as a column?
Darren Oh's avatar
Darren Oh committed
350
 */
351
function xmlsitemap_sitemap_save(XmlSitemapInterface $sitemap) {
352
  $context = $sitemap->context;
353
  if (!isset($context) || !$context) {
354
    $sitemap->context = [];
355 356
  }

357
  // Make sure context is sorted before saving the hash.
358
  $sitemap->setOriginalId($sitemap->isNew() ? NULL : $sitemap->getId());
359
  $sitemap->setId(xmlsitemap_sitemap_get_context_hash($context));
360
  // If the context was changed, we need to perform additional actions.
361
  if (!$sitemap->isNew() && $sitemap->getId() != $sitemap->getOriginalId()) {
362
    // Rename the files directory so the sitemap does not break.
363
    $old_sitemap = xmlsitemap_sitemap_load($sitemap->getOriginalId());
364
    $old_dir = xmlsitemap_get_directory($old_sitemap);
365 366 367 368
    $new_dir = xmlsitemap_get_directory($sitemap);
    xmlsitemap_directory_move($old_dir, $new_dir);

    // Mark the sitemaps as needing regeneration.
369
    \Drupal::state()->set('xmlsitemap_regenerate_needed', TRUE);
370
  }
371
  $sitemap->save();
372 373

  return $sitemap;
374 375 376
}

/**
377
 * Delete an XML sitemap.
378
 *
379
 * @param string $smid
380
 *   An XML sitemap ID.
381
 */
382
function xmlsitemap_sitemap_delete($smid) {
383
  xmlsitemap_sitemap_delete_multiple([$smid]);
384 385
}

Darren Oh's avatar
Darren Oh committed
386
/**
387
 * Delete multiple XML sitemaps.
388
 *
389
 * @param array $smids
390
 *   An array of XML sitemap IDs.
Darren Oh's avatar
Darren Oh committed
391
 */
392 393 394 395
function xmlsitemap_sitemap_delete_multiple(array $smids) {
  if (!empty($smids)) {
    $sitemaps = xmlsitemap_sitemap_load_multiple($smids);
    foreach ($sitemaps as $sitemap) {
396
      $sitemap->delete();
397
      \Drupal::moduleHandler()->invokeAll('xmlsitemap_sitemap_delete', [$sitemap]);
398
    }
399
  }
400
}
401

402 403 404
/**
 * Return the expected file path for a specific sitemap chunk.
 *
405
 * @param Drupal\xmlsitemap\XmlSitemapInterface $sitemap
406
 *   An XmlSitemapInterface sitemap object.
407
 * @param string $chunk
408
 *   An optional specific chunk in the sitemap. Defaults to the index page.
409 410 411
 *
 * @return string
 *   File path for a specific sitemap chunk.
412
 */
413
function xmlsitemap_sitemap_get_file(XmlSitemapInterface $sitemap, $chunk = 'index') {
414
  return xmlsitemap_get_directory($sitemap) . "/{$chunk}.xml";
Darren Oh's avatar
Darren Oh committed
415 416
}

417 418 419
/**
 * Find the maximum file size of all a sitemap's XML files.
 *
420 421
 * @param \Drupal\xmlsitemap\XmlSitemapInterface $sitemap
 *   The XML sitemap object.
422 423 424
 *
 * @return int
 *   Maximum file size in the directory.
425
 */
426
function xmlsitemap_sitemap_get_max_filesize(XmlSitemapInterface $sitemap) {
427
  $dir = xmlsitemap_get_directory($sitemap);
428
  $sitemap->setMaxFileSize(0);
429 430
  /** @var \Drupal\Core\File\FileSystemInterface $file_system */
  $file_system = \Drupal::service('file_system');
431 432 433 434 435 436 437 438
  try {
    $files = $file_system->scanDirectory($dir, '/\.xml$/');
  }
  catch (FileException $e) {
    // Ignore and return empty array for BC.
    $files = [];
  }
  foreach ($files as $file) {
439
    $sitemap->setMaxFileSize(max($sitemap->getMaxFileSize(), filesize($file->uri)));
440
  }
441
  return $sitemap->getMaxFileSize();
442 443
}

444 445 446 447 448
/**
 * Returns the hash string for a context.
 *
 * @param array $context
 *   Context to be hashed.
449
 *
450 451 452
 * @return string
 *   Hash string for the context.
 */
453
function xmlsitemap_sitemap_get_context_hash(array &$context) {
454
  ksort($context);
455
  return Crypt::hashBase64(serialize($context));
456 457
}

Darren Oh's avatar
Darren Oh committed
458
/**
459
 * Returns the uri elements of an XML sitemap.
460
 *
461 462
 * @param \Drupal\xmlsitemap\XmlSitemapInterface $sitemap
 *   The sitemap represented by and XmlSitemapInterface object.
463 464
 *
 * @return array
465 466
 *   An array containing the 'path' and 'options' keys used to build the uri of
 *   the XML sitemap, and matching the signature of url().
Darren Oh's avatar
Darren Oh committed
467
 */
468
function xmlsitemap_sitemap_uri(XmlSitemapInterface $sitemap) {
469
  $uri['path'] = 'sitemap.xml';
470
  $uri['options'] = \Drupal::moduleHandler()->invokeAll('xmlsitemap_context_url_options', [$sitemap->context]);
471
  $context = $sitemap->context;
472
  \Drupal::moduleHandler()->alter('xmlsitemap_context_url_options', $uri['options'], $context);
473
  $uri['options'] += [
474
    'absolute' => TRUE,
475
    'base_url' => Settings::get('xmlsitemap_base_url', \Drupal::state()->get('xmlsitemap_base_url')),
476
  ];
477 478
  return $uri;
}
479

Darren Oh's avatar
Darren Oh committed
480
/**
481
 * @} End of "defgroup xmlsitemap_api"
482
 */
483
function xmlsitemap_get_directory(XmlSitemapInterface $sitemap = NULL) {
484 485
  $directory = &drupal_static(__FUNCTION__);
  if (!isset($directory)) {
486
    $directory = \Drupal::config('xmlsitemap.settings')->get('path');
Darren Oh's avatar
Darren Oh committed
487
  }
488

489 490 491 492
  if (empty($directory)) {
    return FALSE;
  }
  elseif ($sitemap != NULL && !empty($sitemap->id)) {
493
    return file_build_uri($directory . '/' . $sitemap->id);
494 495 496 497
  }
  else {
    return file_build_uri($directory);
  }
Darren Oh's avatar
Darren Oh committed
498 499 500
}

/**
501
 * Check that the sitemap files directory exists and is writable.
Darren Oh's avatar
Darren Oh committed
502
 */
503
function xmlsitemap_check_directory(XmlSitemapInterface $sitemap = NULL) {
504
  $directory = xmlsitemap_get_directory($sitemap);
505 506 507
  /** @var \Drupal\Core\File\FileSystemInterface $filesystem */
  $filesystem = \Drupal::service('file_system');
  $result = $filesystem->prepareDirectory($directory, $filesystem::CREATE_DIRECTORY | $filesystem::MODIFY_PERMISSIONS);
508
  if (!$result) {
509
    \Drupal::logger('file system')->error('The directory %directory does not exist or is not writable.', ['%directory' => $directory]);
510 511 512 513
  }
  return $result;
}

514 515 516
/**
 * Check all directories.
 */
517
function xmlsitemap_check_all_directories() {
518
  $directories = [];
519 520

  $sitemaps = xmlsitemap_sitemap_load_multiple(FALSE);
521
  foreach ($sitemaps as $sitemap) {
522 523 524 525
    $directory = xmlsitemap_get_directory($sitemap);
    $directories[$directory] = $directory;
  }

526 527 528
  /** @var \Drupal\Core\File\FileSystemInterface $filesystem */
  $filesystem = \Drupal::service('file_system');

529
  foreach ($directories as $directory) {
530
    $result = $filesystem->prepareDirectory($directory, $filesystem::CREATE_DIRECTORY | $filesystem::MODIFY_PERMISSIONS);
531 532 533 534 535 536 537 538 539 540 541
    if ($result) {
      $directories[$directory] = TRUE;
    }
    else {
      $directories[$directory] = FALSE;
    }
  }

  return $directories;
}

542 543 544 545 546 547 548 549 550 551 552
/**
 * Clears sitemap directory.
 *
 * @param \Drupal\xmlsitemap\XmlSitemapInterface $sitemap
 *   Sitemap entity.
 * @param bool $delete
 *   If TRUE, delete the path directory afterwards.
 *
 * @return bool
 *   Returns TRUE is operation was successful, FALSE otherwise.
 */
553
function xmlsitemap_clear_directory(XmlSitemapInterface $sitemap = NULL, $delete = FALSE) {
554 555 556 557 558 559
  if ($directory = xmlsitemap_get_directory($sitemap)) {
    return _xmlsitemap_delete_recursive($directory, $delete);
  }
  else {
    return FALSE;
  }
560 561
}

562 563 564
/**
 * Move a directory to a new location.
 *
565
 * @param string $old_dir
566
 *   A string specifying the filepath or URI of the original directory.
567
 * @param string $new_dir
568
 *   A string specifying the filepath or URI of the new directory.
569 570
 * @param int $replace
 *   Behavior when the destination file already exists.
571 572
 *   Replace behavior when the destination file already exists.
 *
573
 * @return bool
574 575
 *   TRUE if the directory was moved successfully. FALSE otherwise.
 */
576 577 578 579 580
function xmlsitemap_directory_move($old_dir, $new_dir, $replace = FileSystemInterface::EXISTS_REPLACE) {
  /** @var \Drupal\Core\File\FileSystemInterface $filesystem */
  $filesystem = \Drupal::service('file_system');

  $success = $filesystem->prepareDirectory($new_dir, $filesystem::CREATE_DIRECTORY | $filesystem::MODIFY_PERMISSIONS);
581

582 583
  $old_path = $filesystem->realpath($old_dir);
  $new_path = $filesystem->realpath($new_dir);
584 585 586 587
  if (!is_dir($old_path) || !is_dir($new_path) || !$success) {
    return FALSE;
  }

588
  $files = $filesystem->scanDirectory($old_dir, '/.*/');
589 590
  foreach ($files as $file) {
    $file->uri_new = $new_dir . '/' . basename($file->filename);
591
    $success &= (bool) $filesystem->move($file->uri, $file->uri_new, $replace);
592 593 594
  }

  // The remove the directory.
595
  $success &= $filesystem->rmdir($old_dir);
596 597 598
  return $success;
}

599 600 601
/**
 * Recursively delete all files and folders in the specified filepath.
 *
602
 * This is a backport of Drupal 8's file_unmanaged_delete_recursive().
603 604 605
 *
 * Note that this only deletes visible files with write permission.
 *
606
 * @param string $path
607
 *   A filepath relative to the Drupal root directory.
608
 * @param bool $delete_root
609
 *   A boolean if TRUE will delete the $path directory afterwards.
610 611 612
 *
 * @return bool
 *   TRUE if operation was successful, FALSE otherwise.
613 614
 */
function _xmlsitemap_delete_recursive($path, $delete_root = FALSE) {
615 616 617
  /** @var \Drupal\Core\File\FileSystemInterface $filesystem */
  $filesystem = \Drupal::service('file_system');

618
  // Resolve streamwrapper URI to local path.
619
  $path = $filesystem->realpath($path);
620 621 622 623 624 625 626
  if (is_dir($path)) {
    $dir = dir($path);
    while (($entry = $dir->read()) !== FALSE) {
      if ($entry == '.' || $entry == '..') {
        continue;
      }
      $entry_path = $path . '/' . $entry;
627
      $filesystem->deleteRecursive($entry_path);
628 629
    }
    $dir->close();
630
    return $delete_root ? $filesystem->rmdir($path) : TRUE;
631
  }
632
  return $filesystem->delete($path);
Darren Oh's avatar
Darren Oh committed
633 634
}

635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702
/**
 * Implements hook_entity_type_build().
 */
function xmlsitemap_entity_type_build(array &$entity_types) {
  // Mark some specific core entity types as not supported by XML sitemap.
  // If a site wants to undo this, they may use hook_entity_type_alter().
  $unsupported_types = [
    // Custom blocks.
    'block_content',
    // Comments.
    'comment',
    // Shortcut items.
    'shortcut',
    // Custom Token module.
    // @see https://www.drupal.org/project/token_custom/issues/3150038
    'token_custom',
  ];

  /** @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */
  foreach ($unsupported_types as $entity_type_id) {
    if (isset($entity_types[$entity_type_id])) {
      $entity_types[$entity_type_id]->set('xmlsitemap', FALSE);
    }
  }
}

/**
 * Determines if an entity type can be listed in the XML sitemap as links.
 *
 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
 *   The entity type.
 *
 * @return bool
 *   TRUE if the entity type can be used, or FALSE otherwise.
 */
function xmlsitemap_is_entity_type_supported(EntityTypeInterface $entity_type) {
  // If the XML sitemap status in the entity type annotation has been set then
  // return that first. This will allow modules to bypass the logic below if
  // needed.
  $status = $entity_type->get('xmlsitemap');
  if ($status !== NULL) {
    return $status;
  }

  // Skip if the entity type is not a content entity type.
  if (!($entity_type instanceof ContentEntityTypeInterface)) {
    return FALSE;
  }

  // Skip if the entity type is internal (and not considered public).
  if ($entity_type->isInternal()) {
    return FALSE;
  }

  // Skip if the entity type does not have a canonical URL.
  if (!$entity_type->hasLinkTemplate('canonical') && !$entity_type->getUriCallback()) {
    return FALSE;
  }

  // Skip if the entity type as a bundle entity type but does not yet have
  // any bundles created.
  if ($entity_type->getBundleEntityType() && !\Drupal::service('entity_type.bundle.info')->getBundleInfo($entity_type->id())) {
    return FALSE;
  }

  return TRUE;
}

Darren Oh's avatar
Darren Oh committed
703
/**
704 705
 * Returns information about supported sitemap link types.
 *
706
 * @param mixed $type
707 708
 *   (optional) The link type to return information for. If omitted,
 *   information for all link types is returned.
709
 * @param mixed $reset
710 711 712
 *   (optional) Boolean whether to reset the static cache and do nothing. Only
 *   used for tests.
 *
713 714 715
 * @return array
 *   Info about sitemap link.
 *
716 717
 * @see hook_xmlsitemap_link_info()
 * @see hook_xmlsitemap_link_info_alter()
Darren Oh's avatar
Darren Oh committed
718
 */
719
function xmlsitemap_get_link_info($type = NULL, $reset = FALSE) {
720
  $language = \Drupal::languageManager()->getCurrentLanguage();
721
  $link_info = &drupal_static(__FUNCTION__);
722

723 724
  if ($reset) {
    $link_info = NULL;
725
    \Drupal::service('cache_tags.invalidator')->invalidateTags(['xmlsitemap']);
726 727
  }

728
  if (!isset($link_info)) {
729
    $cid = 'xmlsitemap:link_info:' . $language->getId();
730
    if ($cache = \Drupal::cache()->get($cid)) {
731
      $link_info = $cache->data;
732
    }
733
    else {
734
      $link_info = [];
735
      $entity_types = \Drupal::entityTypeManager()->getDefinitions();
736

737
      foreach ($entity_types as $key => $entity_type) {
738 739 740 741
        if (!xmlsitemap_is_entity_type_supported($entity_type)) {
          continue;
        }

742
        $link_info[$key] = [
743 744 745
          'label' => $entity_type->getLabel(),
          'type' => $entity_type->id(),
          'base table' => $entity_type->getBaseTable(),
746
          'bundles' => \Drupal::service('entity_type.bundle.info')->getBundleInfo($entity_type->id()),
747 748 749 750 751
          'bundle label' => $entity_type->getBundleLabel(),
          'entity keys' => [
            'id' => $entity_type->getKey('id'),
            'bundle' => $entity_type->getKey('bundle'),
          ],
752
        ];
753
      }
754

755
      $link_info = array_merge($link_info, \Drupal::moduleHandler()->invokeAll('xmlsitemap_link_info'));
756
      foreach ($link_info as $key => &$info) {
757
        $info += [
758 759
          'type' => $key,
          'base table' => FALSE,
760 761
          'bundles' => [],
        ];
762 763 764 765
        if (!isset($info['xmlsitemap']['process callback']) && !empty($info['entity keys'])) {
          $info['xmlsitemap']['process callback'] = 'xmlsitemap_xmlsitemap_process_entity_links';
        }
        if (!isset($info['xmlsitemap']['rebuild callback']) && !empty($info['entity keys']['id'])) {
766 767 768
          $info['xmlsitemap']['rebuild callback'] = 'xmlsitemap_rebuild_batch_fetch';
        }
        foreach ($info['bundles'] as $bundle => &$bundle_info) {
769 770 771
          $bundle_info += [
            'xmlsitemap' => [],
          ];
772 773
          $bundle_info['xmlsitemap'] += xmlsitemap_link_bundle_load($key, $bundle, FALSE);
        }
774
      }
775
      \Drupal::moduleHandler()->alter('xmlsitemap_link_info', $link_info);
776 777 778 779 780 781 782 783 784 785 786 787 788

      // Sort the entity types by label.
      uasort($link_info, function ($a, $b) {
        // Put frontpage first.
        if ($a['type'] === 'frontpage') {
          return -1;
        }
        if ($b['type'] === 'frontpage') {
          return 1;
        }
        return strnatcmp($a['label'], $b['label']);
      });

789
      // Cache by language since this info contains translated strings.
790 791 792 793 794 795 796 797 798 799 800 801
      // Also include entity type tags since this is tied to entity and bundle
      // information.
      \Drupal::cache()->set(
        $cid,
        $link_info,
        Cache::PERMANENT,
        [
          'xmlsitemap',
          'entity_types',
          'entity_bundles',
        ]
      );
802
    }
Darren Oh's avatar
Darren Oh committed
803
  }
804 805 806 807 808 809

  if (isset($type)) {
    return isset($link_info[$type]) ? $link_info[$type] : NULL;
  }

  return $link_info;
Darren Oh's avatar
Darren Oh committed
810 811
}

812 813 814 815 816 817 818 819 820
/**
 * Returns enabled bundles of an entity type.
 *
 * @param string $entity_type
 *   Entity type id.
 *
 * @return array
 *   Array with entity bundles info.
 */
821
function xmlsitemap_get_link_type_enabled_bundles($entity_type) {
822
  $bundles = [];
823 824
  $info = xmlsitemap_get_link_info($entity_type);
  foreach ($info['bundles'] as $bundle => $bundle_info) {
825
    $settings = xmlsitemap_link_bundle_load($entity_type, $bundle);
826
    if (!empty($settings['status'])) {
827 828 829 830 831 832
      $bundles[] = $bundle;
    }
  }
  return $bundles;
}

833 834 835
/**
 * Returns statistics about specific entity links.
 *
836
 * @param string $entity_type_id
837 838
 *   Entity type id.
 * @param string $bundle
839
 *   Bundle id.
840 841 842 843
 *
 * @return array
 *   Array with statistics.
 */
844 845
function xmlsitemap_get_link_type_indexed_status($entity_type_id, $bundle = '') {
  $info = xmlsitemap_get_link_info($entity_type_id);
846
  $database = \Drupal::database();
847
  $entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id);