xmlsitemap.module 85.2 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 34
use Drupal\xmlsitemap\Entity\XmlSitemap;
use Drupal\xmlsitemap\XmlSitemapInterface;
35
use Symfony\Component\HttpFoundation\Request;
36
use Symfony\Component\HttpFoundation\Response;
37
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
38

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

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

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

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

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

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

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

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

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

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

120 121 122
  return $hooks;
}

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

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

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

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

143
    case 'xmlsitemap.admin_rebuild':
144 145 146
      $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>';
  }

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

  return $output;
}

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

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

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

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

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

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

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

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

232
/**
233
 * Internal default variables config for xmlsitemap_var().
234 235 236
 *
 * @return array
 *   Array with config variables of xmlsitemap.settings config object.
237
 */
238
function xmlsitemap_config_variables() {
239
  return [
240
    'minimum_lifetime' => 0,
241 242 243 244 245 246 247 248 249
    '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,
250
    'disable_cron_regeneration' => FALSE,
251
  ];
252 253
}

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

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

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

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

Darren Oh's avatar
Darren Oh committed
293
/**
294
 * @defgroup xmlsitemap_api XML sitemap API.
295
 * @{
296 297
 * 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
298
 */
299 300 301 302

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

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

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

340
  return $sitemaps;
341 342
}

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

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

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

  return $sitemap;
375 376 377
}

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

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

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

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

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

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

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

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

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

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

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

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

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

  return $directories;
}

543 544 545 546 547 548 549 550 551 552 553
/**
 * 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.
 */
554
function xmlsitemap_clear_directory(XmlSitemapInterface $sitemap = NULL, $delete = FALSE) {
555 556 557 558 559 560
  if ($directory = xmlsitemap_get_directory($sitemap)) {
    return _xmlsitemap_delete_recursive($directory, $delete);
  }
  else {
    return FALSE;
  }
561 562
}

563 564 565
/**
 * Move a directory to a new location.
 *
566
 * @param string $old_dir
567
 *   A string specifying the filepath or URI of the original directory.
568
 * @param string $new_dir
569
 *   A string specifying the filepath or URI of the new directory.
570 571
 * @param int $replace
 *   Behavior when the destination file already exists.
572 573
 *   Replace behavior when the destination file already exists.
 *
574
 * @return bool
575 576
 *   TRUE if the directory was moved successfully. FALSE otherwise.
 */
577 578 579 580 581
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);
582

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

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

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

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

619
  // Resolve streamwrapper URI to local path.
620
  $path = $filesystem->realpath($path);
621 622 623 624 625 626 627
  if (is_dir($path)) {
    $dir = dir($path);
    while (($entry = $dir->read()) !== FALSE) {
      if ($entry == '.' || $entry == '..') {
        continue;
      }
      $entry_path = $path . '/' . $entry;
628
      $filesystem->deleteRecursive($entry_path);
629 630
    }
    $dir->close();
631
    return $delete_root ? $filesystem->rmdir($path) : TRUE;
632
  }
633
  return $filesystem->delete($path);
Darren Oh's avatar
Darren Oh committed
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 703
/**
 * 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
704
/**
705 706
 * Returns information about supported sitemap link types.
 *
707
 * @param mixed $type
708 709
 *   (optional) The link type to return information for. If omitted,
 *   information for all link types is returned.
710
 * @param mixed $reset
711 712 713
 *   (optional) Boolean whether to reset the static cache and do nothing. Only
 *   used for tests.
 *
714 715 716
 * @return array
 *   Info about sitemap link.
 *
717 718
 * @see hook_xmlsitemap_link_info()
 * @see hook_xmlsitemap_link_info_alter()
Darren Oh's avatar
Darren Oh committed
719
 */
720
function xmlsitemap_get_link_info($type = NULL, $reset = FALSE) {
721
  $language = \Drupal::languageManager()->getCurrentLanguage();
722
  $link_info = &drupal_static(__FUNCTION__);
723

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

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

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

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

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

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

790
      // Cache by language since this info contains translated strings.
791 792 793 794 795 796 797 798 799 800 801 802
      // 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',
        ]
      );
803
    }
Darren Oh's avatar
Darren Oh committed
804
  }
805 806 807 808 809 810

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

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

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

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