xmlsitemap.module 83.1 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\FileSystemInterface;
26
use Drupal\Core\Form\FormStateInterface;
27
use Drupal\Core\Language\LanguageInterface;
28 29
use Drupal\Core\Link;
use Drupal\Core\Routing\RouteMatchInterface;
30
use Drupal\Core\Site\Settings;
31
use Drupal\Core\Url;
32
use Drupal\xmlsitemap\Controller\XmlSitemapController;
33 34
use Drupal\xmlsitemap\Entity\XmlSitemap;
use Drupal\xmlsitemap\XmlSitemapInterface;
35
use Symfony\Component\HttpFoundation\Request;
36

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

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

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

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

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

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

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

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

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

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

118 119 120
  return $hooks;
}

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

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

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

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

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

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

  return $output;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

338
  return $sitemaps;
339 340
}

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

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

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

  return $sitemap;
373 374 375
}

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

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

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

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

437 438 439 440 441
/**
 * Returns the hash string for a context.
 *
 * @param array $context
 *   Context to be hashed.
442
 *
443 444 445
 * @return string
 *   Hash string for the context.
 */
446
function xmlsitemap_sitemap_get_context_hash(array &$context) {
447
  ksort($context);
448
  return Crypt::hashBase64(serialize($context));
449 450
}

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

Darren Oh's avatar
Darren Oh committed
473
/**
474
 * @} End of "defgroup xmlsitemap_api"
475
 */
476
function xmlsitemap_get_directory(XmlSitemapInterface $sitemap = NULL) {
477 478
  $directory = &drupal_static(__FUNCTION__);
  if (!isset($directory)) {
479
    $directory = \Drupal::config('xmlsitemap.settings')->get('path') ?: 'xmlsitemap';
Darren Oh's avatar
Darren Oh committed
480
  }
481

482
  if ($sitemap != NULL && !empty($sitemap->id)) {
483
    return file_build_uri($directory . '/' . $sitemap->id);
484 485 486 487
  }
  else {
    return file_build_uri($directory);
  }
Darren Oh's avatar
Darren Oh committed
488 489 490
}

/**
491
 * Check that the sitemap files directory exists and is writable.
Darren Oh's avatar
Darren Oh committed
492
 */
493
function xmlsitemap_check_directory(XmlSitemapInterface $sitemap = NULL) {
494
  $directory = xmlsitemap_get_directory($sitemap);
495 496
  /** @var \Drupal\Core\File\FileSystemInterface $filesystem */
  $filesystem = \Drupal::service('file_system');
497
  $result = $filesystem->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS);
498
  if (!$result) {
499
    \Drupal::logger('file system')->error('The directory %directory does not exist or is not writable.', ['%directory' => $directory]);
500 501 502 503
  }
  return $result;
}

504 505 506
/**
 * Check all directories.
 */
507
function xmlsitemap_check_all_directories() {
508
  $directories = [];
509 510

  $sitemaps = xmlsitemap_sitemap_load_multiple(FALSE);
511
  foreach ($sitemaps as $sitemap) {
512 513 514 515
    $directory = xmlsitemap_get_directory($sitemap);
    $directories[$directory] = $directory;
  }

516 517 518
  /** @var \Drupal\Core\File\FileSystemInterface $filesystem */
  $filesystem = \Drupal::service('file_system');

519
  foreach ($directories as $directory) {
520
    $result = $filesystem->prepareDirectory($directory, $filesystem::CREATE_DIRECTORY | $filesystem::MODIFY_PERMISSIONS);
521 522 523 524 525 526 527 528 529 530 531
    if ($result) {
      $directories[$directory] = TRUE;
    }
    else {
      $directories[$directory] = FALSE;
    }
  }

  return $directories;
}

532 533 534 535 536 537 538 539 540 541 542
/**
 * 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.
 */
543
function xmlsitemap_clear_directory(XmlSitemapInterface $sitemap = NULL, $delete = FALSE) {
544 545
  $directory = xmlsitemap_get_directory($sitemap);
  return _xmlsitemap_delete_recursive($directory, $delete);
546 547
}

548 549 550
/**
 * Move a directory to a new location.
 *
551
 * @param string $old_dir
552
 *   A string specifying the filepath or URI of the original directory.
553
 * @param string $new_dir
554
 *   A string specifying the filepath or URI of the new directory.
555 556
 * @param int $replace
 *   Behavior when the destination file already exists.
557 558
 *   Replace behavior when the destination file already exists.
 *
559
 * @return bool
560 561
 *   TRUE if the directory was moved successfully. FALSE otherwise.
 */
562 563 564 565 566
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);
567

568 569
  $old_path = $filesystem->realpath($old_dir);
  $new_path = $filesystem->realpath($new_dir);
570 571 572 573
  if (!is_dir($old_path) || !is_dir($new_path) || !$success) {
    return FALSE;
  }

574
  $files = $filesystem->scanDirectory($old_dir, '/.*/');
575 576
  foreach ($files as $file) {
    $file->uri_new = $new_dir . '/' . basename($file->filename);
577
    $success &= (bool) $filesystem->move($file->uri, $file->uri_new, $replace);
578 579 580
  }

  // The remove the directory.
581
  $success &= $filesystem->rmdir($old_dir);
582 583 584
  return $success;
}

585 586 587
/**
 * Recursively delete all files and folders in the specified filepath.
 *
588
 * This is a backport of Drupal 8's file_unmanaged_delete_recursive().
589 590 591
 *
 * Note that this only deletes visible files with write permission.
 *
592
 * @param string $path
593
 *   A filepath relative to the Drupal root directory.
594
 * @param bool $delete_root
595
 *   A boolean if TRUE will delete the $path directory afterwards.
596 597 598
 *
 * @return bool
 *   TRUE if operation was successful, FALSE otherwise.
599 600
 */
function _xmlsitemap_delete_recursive($path, $delete_root = FALSE) {
601 602 603
  /** @var \Drupal\Core\File\FileSystemInterface $filesystem */
  $filesystem = \Drupal::service('file_system');

604
  // Resolve streamwrapper URI to local path.
605
  $path = $filesystem->realpath($path);
606 607 608
  if (is_dir($path)) {
    $dir = dir($path);
    while (($entry = $dir->read()) !== FALSE) {
609
      if ($entry === '.' || $entry === '..') {
610 611 612
        continue;
      }
      $entry_path = $path . '/' . $entry;
613
      $filesystem->deleteRecursive($entry_path);
614 615
    }
    $dir->close();
616
    return $delete_root ? $filesystem->rmdir($path) : TRUE;
617
  }
618
  return $filesystem->delete($path);
Darren Oh's avatar
Darren Oh committed
619 620
}

621 622 623 624 625 626 627 628 629 630 631 632 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
/**
 * 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
689
/**
690 691
 * Returns information about supported sitemap link types.
 *
692
 * @param mixed $type
693 694
 *   (optional) The link type to return information for. If omitted,
 *   information for all link types is returned.
695
 * @param mixed $reset
696 697 698
 *   (optional) Boolean whether to reset the static cache and do nothing. Only
 *   used for tests.
 *
699 700 701
 * @return array
 *   Info about sitemap link.
 *
702 703
 * @see hook_xmlsitemap_link_info()
 * @see hook_xmlsitemap_link_info_alter()
Darren Oh's avatar
Darren Oh committed
704
 */
705
function xmlsitemap_get_link_info($type = NULL, $reset = FALSE) {
706
  $language = \Drupal::languageManager()->getCurrentLanguage();
707
  $link_info = &drupal_static(__FUNCTION__);
708

709 710
  if ($reset) {
    $link_info = NULL;
711
    \Drupal::service('cache_tags.invalidator')->invalidateTags(['xmlsitemap']);
712 713
  }

714
  if (!isset($link_info)) {
715
    $cid = 'xmlsitemap:link_info:' . $language->getId();
716
    if ($cache = \Drupal::cache()->get($cid)) {
717
      $link_info = $cache->data;
718
    }
719
    else {
720
      $link_info = [];
721
      $entity_types = \Drupal::entityTypeManager()->getDefinitions();
722

723
      foreach ($entity_types as $key => $entity_type) {
724 725 726 727
        if (!xmlsitemap_is_entity_type_supported($entity_type)) {
          continue;
        }

728
        $link_info[$key] = [
729 730 731
          'label' => $entity_type->getLabel(),
          'type' => $entity_type->id(),
          'base table' => $entity_type->getBaseTable(),
732
          'bundles' => \Drupal::service('entity_type.bundle.info')->getBundleInfo($entity_type->id()),
733 734 735 736 737
          'bundle label' => $entity_type->getBundleLabel(),
          'entity keys' => [
            'id' => $entity_type->getKey('id'),
            'bundle' => $entity_type->getKey('bundle'),
          ],
738
        ];
739
      }
740

741
      $link_info = array_merge($link_info, \Drupal::moduleHandler()->invokeAll('xmlsitemap_link_info'));
742
      foreach ($link_info as $key => &$info) {
743
        $info += [
744 745
          'type' => $key,
          'base table' => FALSE,
746 747
          'bundles' => [],
        ];
748 749 750 751
        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'])) {
752 753 754
          $info['xmlsitemap']['rebuild callback'] = 'xmlsitemap_rebuild_batch_fetch';
        }
        foreach ($info['bundles'] as $bundle => &$bundle_info) {
755 756 757
          $bundle_info += [
            'xmlsitemap' => [],
          ];
758 759
          $bundle_info['xmlsitemap'] += xmlsitemap_link_bundle_load($key, $bundle, FALSE);
        }
760
      }
761
      \Drupal::moduleHandler()->alter('xmlsitemap_link_info', $link_info);
762 763 764 765 766 767 768 769 770 771 772 773 774

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

775
      // Cache by language since this info contains translated strings.
776 777 778 779 780 781 782 783 784 785 786 787
      // 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',
        ]
      );
788
    }
Darren Oh's avatar
Darren Oh committed
789
  }
790 791 792 793 794 795

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

  return $link_info;
Darren Oh's avatar
Darren Oh committed
796 797
}

798 799 800 801 802 803 804 805 806
/**
 * Returns enabled bundles of an entity type.
 *
 * @param string $entity_type
 *   Entity type id.
 *
 * @return array
 *   Array with entity bundles info.
 */
807
function xmlsitemap_get_link_type_enabled_bundles($entity_type) {
808
  $bundles = [];
809 810
  $info = xmlsitemap_get_link_info($entity_type);
  foreach ($info['bundles'] as $bundle => $bundle_info) {
811
    $settings = xmlsitemap_link_bundle_load($entity_type, $bundle);
812
    if (!empty($settings['status'])) {
813 814 815 816 817 818
      $bundles[] = $bundle;
    }
  }
  return $bundles;
}

819 820 821
/**
 * Returns statistics about specific entity links.
 *
822
 * @param string $entity_type_id
823 824
 *   Entity type id.
 * @param string $bundle
825
 *   Bundle id.
826 827 828 829
 *
 * @return array
 *   Array with statistics.
 */
830 831
function xmlsitemap_get_link_type_indexed_status($entity_type_id, $bundle = '') {
  $info = xmlsitemap_get_link_info($entity_type_id);
832
  $database = \Drupal::database();
833
  $entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id);