MarkupSitemap.module.php 22.2 KB
Newer Older
Mike Rockétt's avatar
Mike Rockétt committed
1 2
<?php

3 4 5 6 7
/**
 * Sitemap for ProcessWire
 *
 * Module class
 *
Mike Rockétt's avatar
Mike Rockétt committed
8
 * @author Mike Rockett <github@rockett.pw>
9
 * @copyright 2017-18
Mike Rockétt's avatar
Mike Rockétt committed
10
 * @license ISC
11 12
 */

Mike Rockétt's avatar
Mike Rockétt committed
13
// Require the classloader
Mike Rockétt's avatar
Mike Rockétt committed
14
require_once __DIR__ . '/ClassLoader.php';
Mike Rockétt's avatar
Mike Rockétt committed
15

16 17 18 19 20 21
use Rockett\Sitemap\Elements\Url;
use Rockett\Sitemap\Elements\Urlset;
use Rockett\Sitemap\Output;
use Rockett\Sitemap\SubElements\Image;
use Rockett\Sitemap\SubElements\Link;
use Rockett\Traits\FieldsTrait;
Mike Rockétt's avatar
Mike Rockétt committed
22 23 24

class MarkupSitemap extends WireData implements Module
{
25
  use FieldsTrait;
Mike Rockétt's avatar
Mike Rockétt committed
26 27 28 29 30 31

  /**
   * Image fields: each field is mapped to the relavent
   * function for the Image sub-element
   */
  const IMAGE_FIELDS = [
32 33 34
    'Caption' => 'description',
    'License' => 'license',
    'Title' => 'title',
Mike Rockétt's avatar
Mike Rockétt committed
35 36 37 38
    'GeoLocation' => 'geo|location|geolocation',
  ];

  /**
39 40 41 42 43 44 45 46
   * Default page config array, used for comparison at save-time
   */
  const DEFAULT_PAGE_OPTIONS = [
    'priority' => false,
    'excludes' => [
      'images' => false,
      'page' => false,
      'children' => false,
47
    ],
48 49 50
  ];

  /**
51
   * Sitemap URI
Mike Rockétt's avatar
Mike Rockétt committed
52 53 54 55 56 57 58 59 60 61
   */
  const SITEMAP_URI = '/sitemap.xml';

  /**
   * Current request URI
   * @var string
   */
  protected $requestUri = '';

  /**
62
   * Current UrlSet
Mike Rockétt's avatar
Mike Rockétt committed
63
   *
64
   * @var Urlset
Mike Rockétt's avatar
Mike Rockétt committed
65
   */
66
  protected $urlSet;
Mike Rockétt's avatar
Mike Rockétt committed
67 68 69 70 71 72 73 74 75 76 77 78

  /**
   * Module installer
   * Requires ProcessWire 2.8.16+/3.0.16+ (saveConfig; getConfig)
   * @throws WireException
   */
  public function ___install()
  {
    $processWireVersion = $this->config->version;
    $applicableMajorMinor = ProcessWire::versionMajor === 2 ? '2.8' : '3.0';
    if (version_compare($processWireVersion, "{$applicableMajorMinor}.16") < 0) {
      throw new WireException("Requires ProcessWire {$applicableMajorMinor}.16+ to run.");
Mike Rockétt's avatar
Mike Rockétt committed
79
    }
Mike Rockétt's avatar
Mike Rockétt committed
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
  }

  /**
   * Class constructor
   * Get and assign the current request URI
   */
  public function __construct()
  {
    // Set the request URI
    $this->requestUri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : null;
  }

  /**
   * Commit page sitemap options to module config
   * when page is saved. This is a centralised storage method
   * for all page sitemap options.
   * @param array $options
   */
  public function commitPageSitemapOptions($pageId, $options)
  {
    // Save the options for this page. Previous
    // config is completely discarded.
    return $this->modules->saveConfig($this, "o{$pageId}", [
      'priority' => $options['priority'],
      'excludes' => $options['excludes'],
    ]);
  }

  /**
   * When a page is deleted (note: not trashed), then its
   * sitemap options also need to be deleted. We don't do this
   * when its trashed, just in case the page is restored later.
   * @param  HookEvent $event
   * @return void
   */
  public function deletePageSitemapOptions(HookEvent $event)
  {
    // Get the ID of the page that was deleted
    $pageId = $event->arguments(0)->id;

    // By saving a null value by omission, we're effectively deleting
    // the sitemap options for the deleted page.

    return $this->modules->saveConfig($this, "o{$pageId}");
  }

  /**
   * Return a POSTed value or its default if not available
   * @var    string  $valueKey
   * @var    mixed   $default
   * @return mixed
   */
  public function getPostedValue($valueKey, $default = false)
  {
    return $this->input->post->$valueKey ?: $default;
  }

  /**
   * Initiate the module
   *
   * @return void
   */
  public function init()
  {
    // If the request is valid (/sitemap.xml)...
    if ($this->isValidRequest()) {
      // Add the relevant page hooks for multi-language support
      // as these are not bootstrapped at the 404 event (for some reason...)
      if ($this->siteUsesLanguageSupportPageNames()) {
        foreach (['localHttpUrl', 'localName'] as $pageHook) {
          $pageHookFunction = 'hookPage' . ucfirst($pageHook);
          $this->addHook("Page::{$pageHook}", null, function ($event) use ($pageHookFunction) {
            $this->modules->LanguageSupportPageNames->{$pageHookFunction}($event);
          });
Mike Rockétt's avatar
Mike Rockétt committed
154
        }
Mike Rockétt's avatar
Mike Rockétt committed
155 156 157
      }
      // Add the hook to process and render the sitemap.
      $this->addHookBefore('ProcessPageView::pageNotFound', $this, 'render');
Mike Rockétt's avatar
Mike Rockétt committed
158 159
    }

Mike Rockétt's avatar
Mike Rockétt committed
160 161 162 163
    // Add hook to render Sitemap fields on the Settings tab of each page
    if ($this->user->hasPermission('page-edit')) {
      $this->addHookAfter('ProcessPageEdit::buildFormSettings', $this, 'setupSettingsTab');
      $this->addHookAfter('ProcessPageEdit::processInput', $this, 'processSettingsTab');
Mike Rockétt's avatar
Mike Rockétt committed
164
    }
Mike Rockétt's avatar
Mike Rockétt committed
165 166 167 168
    // If the user can delete pages, then we need to hook into delete
    // events to remove sitemap options for deleted pages
    if ($this->user->hasPermission('page-delete')) {
      $this->addHookAfter('Pages::deleted', $this, 'deletePageSitemapOptions');
Mike Rockétt's avatar
Mike Rockétt committed
169
    }
Mike Rockétt's avatar
Mike Rockétt committed
170 171 172 173 174 175 176 177 178 179 180 181
  }

  /**
   * Process Sitemap fields from Settings tab
   * @param  HookEvent $event
   * @return void
   */
  public function processSettingsTab(HookEvent $event)
  {
    // Prevent recursion
    if (($level = $event->arguments(1)) > 0) {
      return;
Mike Rockétt's avatar
Mike Rockétt committed
182 183
    }

Mike Rockétt's avatar
Mike Rockétt committed
184 185 186 187 188
    // Get the current page and stop if we're working
    // with an admin or trashed page.
    $page = $event->object->getPage();
    if ($page->matches("has_parent={$this->config->adminRootPageID}|{$this->config->trashPageID}")) {
      return;
Mike Rockétt's avatar
Mike Rockétt committed
189 190
    }

Mike Rockétt's avatar
Mike Rockétt committed
191 192 193 194 195 196 197 198
    // Build the options instance for this page.
    // If saving the home page, excludes.page and excludes.children
    // are saved as false. The data is kept for the purposes
    // of code simplification, and has no effect on
    // how things work.
    $pageSitemapPageOptions = [
      'priority' => $this->getPostedValue('sitemap_priority'),
      'excludes' => [
199 200
        'images' => $this->getPostedValue('sitemap_exclude_images'),
        'page' => $this->getPostedValue('sitemap_exclude_page'),
Mike Rockétt's avatar
Mike Rockétt committed
201 202 203
        'children' => $this->getPostedValue('sitemap_exclude_children'),
      ],
    ];
Mike Rockétt's avatar
Mike Rockétt committed
204

205 206 207 208 209 210
    $existingOptions = $this->modules->getConfig($this, "o{$page->id}");

    if ($existingOptions === null && $pageSitemapPageOptions === self::DEFAULT_PAGE_OPTIONS) {
      return;
    }

Mike Rockétt's avatar
Mike Rockétt committed
211 212 213
    // Save options for this page
    if (!$this->commitPageSitemapOptions($page->id, $pageSitemapPageOptions)) {
      $this->error($this->_('Something went wrong, and the sitemap options for this page could not be saved.'));
Mike Rockétt's avatar
Mike Rockétt committed
214
    }
Mike Rockétt's avatar
Mike Rockétt committed
215 216 217 218 219 220 221
  }

  /**
   * Initiate the sitemap render by getting the root URI (giving
   * consideration to multi-site setups) and passing it to the
   * first/parent recursive render-method (addPages).
   *
222 223 224
   * Depending on config settings entire sitemap is cached using MarkupCache or
   * WireCache, and the cache is destroyed when settings are saved and, if set
   * up, a page is saved.
Mike Rockétt's avatar
Mike Rockétt committed
225 226 227 228 229 230 231 232 233 234 235 236 237 238
   *
   * @param HookEvent $event
   */
  public function render(HookEvent $event)
  {
    // Get the initial root URI.
    $rootPage = $this->getRootPageUri();

    // If multi-site is present and active, prepend the subdomain prefix.
    if ($this->modules->isInstalled('MultiSite')) {
      $multiSite = $this->modules->get('MultiSite');
      if ($multiSite->subdomain) {
        $rootPage = "/{$multiSite->subdomain}{$rootPage}";
      }
239 240
    }

Mike Rockétt's avatar
Mike Rockétt committed
241 242
    // Make sure that the root page exists.
    if (!$this->pages->get($rootPage) instanceof NullPage) {
243 244
      // Get cached sitemap
      $event->return = $this->getCached($rootPage);
Mike Rockétt's avatar
Mike Rockétt committed
245
      header('Content-Type: application/xml', true, 200);
246

Mike Rockétt's avatar
Mike Rockétt committed
247 248
      // Prevent further hooks. This stops
      // SystemNotifications from displaying a 404 event
249
      // when /sitemap.xml is requested. Additionally,
Mike Rockétt's avatar
Mike Rockétt committed
250 251 252 253 254 255
      // it prevents further modification to the sitemap.
      $event->replace = true;
      $event->cancelHooks = true;
    }
  }

256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294
  /**
   * Get cached sitemap markup
   *
   * @param string $rootPage
   * @return string
   */
  protected function getCached($rootPage)
  {
    // Bail out early if debug mode is enabled
    if ($this->config->debug) {
      header('X-Cached-Sitemap: no');
      return $this->buildNewSitemap($rootPage);
    }

    // Cache settings
    $cacheTTL = $this->cache_ttl ?: 3600;
    $cacheKey = 'MarkupSitemap';
    $cacheMethod = $this->cache_method ?: 'MarkupCache';

    // Attempt to fetch sitemap from cache
    $cache = $cacheMethod == 'WireCache' ? $this->cache : $this->modules->MarkupCache;
    $output = $cache->get($cacheKey, $cacheTTL);

    // If output is empty, generate and cache new sitemap
    if (empty($output)) {
      header('X-Cached-Sitemap: no');
      $output = $this->buildNewSitemap($rootPage);
      if ($cacheMethod == 'WireCache') {
        $cache->save($cacheKey, $output, $cacheTTL);
      } else {
        $cache->save($output);
      }
      return $output;
    }

    header('X-Cached-Sitemap: yes');
    return $output;
  }

Mike Rockétt's avatar
Mike Rockétt committed
295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319
  /**
   * Add sitemap fields to the Settings tab.
   * Responds to ProcessPageEdit::buildFormSettings hook
   * @param  HookEvent $event
   * @return void
   */
  public function setupSettingsTab(HookEvent $event)
  {
    // Get the current page
    $page = $event->object->getPage();

    // We only need to proceed with this process if the current page's
    // template has been assigned as configurable in the module's configuration.
    if ($this->sitemap_include_templates !== null
      && in_array($page->template->name, $this->sitemap_include_templates)
      && !in_array($page->template->name, $this->sitemap_exclude_templates)
    ) {
      // Get the settings tab inputfields
      $inputFields = $event->return;

      // Get the saved options for this page
      $pageOptions = $this->modules->getConfig($this, "o{$page->id}");

      // Sitemap fieldset
      $sitemapFieldset = $this->buildInputField('Fieldset', [
320 321
        'label' => 'Sitemap',
        'icon' => 'sitemap',
Mike Rockétt's avatar
Mike Rockétt committed
322 323 324 325 326
        'collapsed' => Inputfield::collapsedBlank,
      ]);

      // Add priority field
      $sitemapFieldset->append($this->buildInputField('Text', [
327 328
        'name' => 'sitemap_priority',
        'label' => $this->_('Page Priority'),
Mike Rockétt's avatar
Mike Rockétt committed
329
        'description' => $this->_('Set this page’s priority on a scale of 0.0 to 1.0.'),
330
        'notes' => $this->_('This field is optional, and the priority will only be included if it is set here.'),
Mike Rockétt's avatar
Mike Rockétt committed
331
        'columnWidth' => '50%',
332 333
        'pattern' => "(0(\.\d+)?|1(\.0+)?)",
        'value' => $pageOptions['priority'],
Mike Rockétt's avatar
Mike Rockétt committed
334 335 336 337
      ]));

      // Add exclude_images field
      $sitemapFieldset->append($this->buildInputField('Checkbox', [
338 339 340
        'name' => 'sitemap_exclude_images',
        'label' => $this->_('Exclude Images'),
        'label2' => $this->_('Do not add images to the sitemap for this page’s entry'),
Mike Rockétt's avatar
Mike Rockétt committed
341 342
        'description' => $this->_('By default, all image fields for this page will be included in the sitemap. If you don’t want this to happen, you can exclude such inclusion for this page by checking the box below.'),
        'columnWidth' => '50%',
343 344
        'autocheck' => true,
        'value' => $pageOptions['excludes']['images'],
Mike Rockétt's avatar
Mike Rockétt committed
345 346 347 348 349 350
      ]));

      // These fields may only be added to non-root pages.
      if ($page->id !== 1) {
        // Add exclude_page field
        $sitemapFieldset->append($this->buildInputField('Checkbox', [
351 352 353
          'name' => 'sitemap_exclude_page',
          'label' => $this->_('Exclude Page'),
          'label2' => $this->_('Do not include this page in the sitemap'),
Mike Rockétt's avatar
Mike Rockétt committed
354 355
          'description' => $this->_('If you’d like to skip the inclusion of this page (not considering its children, if any) from the sitemap, you can check the box below.'),
          'columnWidth' => '50%',
356 357
          'autocheck' => true,
          'value' => $pageOptions['excludes']['page'],
Mike Rockétt's avatar
Mike Rockétt committed
358 359 360 361
        ]));

        // Add exclude_children field
        $sitemapFieldset->append($this->buildInputField('Checkbox', [
362 363 364
          'name' => 'sitemap_exclude_children',
          'label' => $this->_('Exclude Children'),
          'label2' => $this->_('Do not include this page’s children (if any) in sitemap.xml'),
Mike Rockétt's avatar
Mike Rockétt committed
365 366
          'description' => $this->_('If you’d like to skip the inclusion of this page’s children (if any, and not considering the page itself) from the sitemap, you can check the box below.'),
          'columnWidth' => '50%',
367 368
          'autocheck' => true,
          'value' => $pageOptions['excludes']['children'],
Mike Rockétt's avatar
Mike Rockétt committed
369 370 371 372 373
        ]));
      }

      // Add the new fieldset to the Settings tab (fieldset)
      $inputFields->insertBefore($sitemapFieldset, $inputFields->find('name=status')->first());
374
    }
Mike Rockétt's avatar
Mike Rockétt committed
375 376 377 378 379 380 381 382 383 384 385 386 387 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 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433
  }

  /**
   * Correctly format the priority float to one decimal
   * @param  float
   * @return string
   */
  protected function formatPriorityFloat($priority)
  {
    return sprintf('%.1F', (float) $priority);
  }

  /**
   * Get the root page URI
   * @return string
   */
  protected function getRootPageUri()
  {
    return (string) str_ireplace(
      trim($this->config->urls->root, '/'),
      '',
      $this->sanitizer->path(dirname($this->requestUri))
    );
  }

  /**
   * Determine if the request is valud
   * @return boolean
   */
  protected function isValidRequest()
  {
    $valid = (bool) (
      $this->requestUri !== null &&
      strlen($this->requestUri) - strlen(self::SITEMAP_URI) === strrpos($this->requestUri, self::SITEMAP_URI)
    );

    return $valid;
  }

  /**
   * Check if the language is not default and that the
   * page is not available/statused in the default language.
   * @param  string $language
   * @param  Page   $page
   * @return bool
   */
  protected function pageLanguageInvalid($language, $page)
  {
    return (!$language->isDefault() && !$page->{"status{$language->id}"});
  }

  /**
   * Determine if the site uses the LanguageSupportPageNames module.
   * @return bool
   */
  protected function siteUsesLanguageSupportPageNames()
  {
    return $this->modules->isInstalled('LanguageSupportPageNames');
  }
434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 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 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748

  /**
   * Add alternative languges, including current.
   * @param Page $page
   * @param Url  $url
   */
  protected function addAltLanguages($page, $url)
  {
    foreach ($this->languages as $altLanguage) {
      if ($this->pageLanguageInvalid($altLanguage, $page)) {
        continue;
      }
      if ($altLanguage->isDefault()
        && $this->pages->get(1)->name === 'home'
        && !$this->modules->LanguageSupportPageNames->useHomeSegment
        && !empty($this->sitemap_default_iso)) {
        $languageIsoName = $this->sitemap_default_iso;
      } else {
        $languageIsoName = $this->pages->get(1)->localName($altLanguage);
      }
      $url->addSubElement(new Link($languageIsoName, $page->localHttpUrl($altLanguage)));
    }
  }

  /**
   * Generate an image tag for the current image in the loop
   * @param  Pageimage $image
   * @param  Language  $language
   * @return Image
   */
  protected function addImage($image, $language = null)
  {
    $locImage = new Image($image->httpUrl);
    foreach (self::IMAGE_FIELDS as $imageMetaMethod => $imageMetaValues) {
      foreach (explode('|', $imageMetaValues) as $imageMetaValue) {
        if ($language != null && !$language->isDefault() && $image->{"$imageMetaValue{$language->id}"}) {
          $imageMetaValue .= $language->id;
        }
        if ($image->$imageMetaValue) {
          if ($imageMetaMethod === 'License') {
            // Skip invalid licence URLs
            if (!filter_var($image->$imageMetaValue, FILTER_VALIDATE_URL)) {
              continue;
            }
          }
          $locImage->{"set{$imageMetaMethod}"}($image->$imageMetaValue);
        }
      }
    }

    return $locImage;
  }

  /**
   * Add images to the current Url
   * @param Url      $url
   * @param Language $language
   */
  protected function addImages($page, $url, $language = null)
  {
    // Loop through declared image fields and skip non image fields
    if ($this->sitemap_image_fields) {
      foreach ($this->sitemap_image_fields as $imageFieldName) {
        $page->of(false);
        $imageField = $page->$imageFieldName;
        if ($imageField) {
          foreach ($imageField as $image) {
            if ($image instanceof Pageimage || $image instanceof \ProcessWire\Pageimage) {
              $url->addSubElement($this->addImage($image, $language));
            }
          }
        }
      }
    }
  }

  /**
   * Determine if a page can be included in the sitemap
   * @param  $page
   * @param  $options
   * @return bool
   */
  public function pageIsIncludible($page, $options)
  {
    // If it's the home page, it's always includible.
    if ($page->id === 1) {
      return true;
    }

    // If the page's template is excluded from accessing Sitemap,
    // then it's not includible.
    if (in_array($page->template->name, $this->sitemap_exclude_templates)) {
      return false;
    }

    // Otherwise, check to see if the page itself has been excluded
    // via Sitemap options.
    return !$options['excludes']['page'];
  }

  /**
   * Recursively add pages in each language with
   * alternate language and image sub-elements.
   * @param  $page
   */
  protected function addPages($page)
  {
    // Get the saved options for this page
    $pageSitemapOptions = $this->modules->getConfig($this, "o{$page->id}");

    // If the template that this page belongs to is not using sitemap options
    // (per the module's current configuration), then we need to revert the keys
    // in $pageSitemapOptions to their defaults so as to prevent their
    // saved options from being used in this cycle.
    if ($this->sitemap_include_templates !== null
      && !in_array($page->template->name, $this->sitemap_include_templates)
      && is_array($pageSitemapOptions)) {
      array_walk_recursive($pageSitemapOptions, function (&$value) {
        $value = false;
      });
    }

    // If the page is viewable and not excluded or we’re working with the root page,
    // begin generating the sitemap by adding pages recursively. (Root is always added.)
    if ($page->viewable() && $this->pageIsIncludible($page, $pageSitemapOptions)) {
      // If language support is enabled, then we need to loop through each language
      // to generate <loc> for each language with all alternates, including the
      // current language. Then add image references with multi-language support.
      if ($this->siteUsesLanguageSupportPageNames()) {
        foreach ($this->languages as $language) {
          if ($this->pageLanguageInvalid($language, $page) || !$page->viewable($language)) {
            continue;
          }
          $url = new Url($page->localHttpUrl($language));
          $url->setLastMod(date('c', $page->modified));
          $this->addAltLanguages($page, $url);
          if ($pageSitemapOptions['priority']) {
            $url->setPriority($this->formatPriorityFloat($pageSitemapOptions['priority']));
          }
          if (!$pageSitemapOptions['excludes']['images']) {
            $this->addImages($page, $url, $language);
          }
          $this->urlSet->addUrl($url);
        }
      } else {
        // If multi-language support is not enabled, then we only need to
        // add the current URL to a new <loc>, along with images.
        $url = new Url($page->httpUrl);
        $url->setLastMod(date('c', $page->modified));
        if ($pageSitemapOptions['priority']) {
          $url->setPriority($this->formatPriorityFloat($pageSitemapOptions['priority']));
        }
        if (!$pageSitemapOptions['excludes']['images']) {
          $this->addImages($page, $url);
        }
        $this->urlSet->addUrl($url);
      }
    }

    // Check for children
    if (!$pageSitemapOptions['excludes']['children']) {

      // Build up the child selector.
      $selector = "id!={$this->config->http404PageID}";
      if ($this->sitemap_include_hidden) {
        $selector = implode(',', [
          'include=hidden',
          'template!=admin',
          $selector,
        ]);
      }

      // Check for children and include where possible.
      if ($page->hasChildren($selector)) {
        foreach ($page->children($selector) as $child) {
          $this->addPages($child);
        }
      }
    }
  }

  /**
   * Build a new sitemap (called when cache doesn't have one or we're debugging)
   * @return string
   */
  protected function buildNewSitemap($rootPage)
  {
    $this->urlSet = new Urlset();
    $this->addPages($this->pages->get($rootPage));
    $sitemapOutput = new Output();
    if ($this->sitemap_stylesheet) {
      $sitemapOutput->addProcessingInstruction(
        'xml-stylesheet',
        'type="text/xsl" href="' . $this->getStylesheetUrl() . '"'
      );
    }

    return $sitemapOutput->setIndented(true)->getOutput($this->urlSet);
  }

  /**
   * If using a stylesheet, return its absolute URL.
   * @return string
   */
  protected function getStylesheetUrl()
  {
    if ($this->sitemap_stylesheet_custom
      && filter_var($this->sitemap_stylesheet_custom, FILTER_VALIDATE_URL)) {
      return $this->sitemap_stylesheet_custom;
    }

    return $stylesheetPath = $this->urls->httpSiteModules . 'MarkupSitemap/assets/sitemap-stylesheet.xsl';
  }

  /**
   * Dump vars and die.
   *
   * @param  mixed  $mixed The vars to dump
   * @return void
   */
  protected function dd()
  {
    $this->dump(func_get_args()) && die;
  }

  /**
   * Dump vars.
   *
   * @param  mixed  $mixed The vars to dump
   * @return void
   */
  protected function dump()
  {
    $this->header();
    array_map(
      function ($mixed) {
        var_dump($mixed);
      },
      func_get_args()
    );

    return true;
  }

  /**
   * Prepare the content-type header
   *
   * @return void
   */
  protected function header()
  {
    $header = 'Content-Type: text/plain';
    if (!$this->headerPrepared($header)) {
      header($header);
      $this->timestamp = -microtime(true);
    }
  }

  /**
   * Determine if a header has been prepared.
   *
   * @param  $header
   * @return bool
   */
  protected function headerPrepared($header)
  {
    $header = trim($header, ': ');

    foreach (headers_list() as $listedHeader) {
      if (stripos($listedHeader, $header) !== false) {
        return true;
      }
    }

    return false;
  }

  /**
   * Print vars and die.
   *
   * @param  mixed  $mixed The vars to print
   * @return void
   */
  protected function pd()
  {
    $this->printVars(func_get_args()) && die;
  }

  /**
   * Print vars.
   *
   * @param  mixed  $mixed The vars to print
   * @return void
   */
  protected function printVars()
  {
    $this->header();
    array_map(
      function ($mixed) {
        print_r($mixed);
      },
      func_get_args()
    );

    return true;
  }

  /**
   * Log things to a dedicated log file.
   * @param $content
   */
  protected function logme($content)
  {
    if ($this->config->debug) $this->log->save('markup-sitemap', $content);
  }
Mike Rockétt's avatar
Mike Rockétt committed
749
}