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

Mike Rockétt's avatar
Mike Rockétt committed
3 4 5 6
/**
 * Sitemap for ProcessWire
 * Module class
 *
7
 * @author Mike Rockett <[email protected]>
Mike Rockétt's avatar
Mike Rockétt committed
8
 * @copyright 2017-20
Mike Rockétt's avatar
Mike Rockétt committed
9
 * @license ISC
Mike Rockétt's avatar
Mike Rockétt committed
10 11
 */

12 13 14
// Require the classloaders
wire('classLoader')->addNamespace('Thepixeldeveloper\Sitemap', __DIR__ . '/src/Sitemap');
wire('classLoader')->addNamespace('Rockett\Concerns', __DIR__ . '/src/Concerns');
Mike Rockétt's avatar
Mike Rockétt committed
15
wire('classLoader')->addNamespace('Rockett\Support', __DIR__ . '/src/Support');
16

17 18 19
use ProcessWire\Language;
use ProcessWire\Page;
use ProcessWire\WireException;
20 21
use Rockett\Concerns;
use Rockett\Support\ParseFloat;
Mike Rockétt's avatar
Mike Rockétt committed
22
use Rockett\Support\ParseTimestamp;
23 24 25 26
use Thepixeldeveloper\Sitemap\Drivers\XmlWriterDriver;
use Thepixeldeveloper\Sitemap\Extensions\Link;
use Thepixeldeveloper\Sitemap\Url;
use Thepixeldeveloper\Sitemap\Urlset;
Mike Rockétt's avatar
Mike Rockétt committed
27

Mike Rockétt's avatar
init  
Mike Rockétt committed
28 29
class MarkupSitemap extends WireData implements Module
{
30 31 32 33 34 35
  use Concerns\DebugsThings;
  use Concerns\BuildsInputFields;
  use Concerns\ConfiguresTabs;
  use Concerns\ProcessesTabs;
  use Concerns\HandlesEvents;
  use Concerns\SupportsImages;
Mike Rockétt's avatar
Mike Rockétt committed
36

37 38 39 40 41 42 43 44 45 46 47 48
  /**
   * Default page config array, used for comparison at save-time
   */
  private static $defaultPageOptions = [
    'priority' => false,
    'excludes' => [
      'images' => false,
      'page' => false,
      'children' => false,
    ],
  ];

Mike Rockétt's avatar
Mike Rockétt committed
49 50 51 52
  /**
   * Image fields: each field is mapped to the relavent
   * function for the Image sub-element
   */
53
  private static $imageFields = [
54 55 56
    'Caption' => 'description',
    'License' => 'license',
    'Title' => 'title',
Mike Rockétt's avatar
Mike Rockétt committed
57 58 59
    'GeoLocation' => 'geo|location|geolocation',
  ];

60
  /**
61
   * Sitemap URI
Mike Rockétt's avatar
Mike Rockétt committed
62
   */
63
  const sitemapUri = '/sitemap.xml';
Mike Rockétt's avatar
Mike Rockétt committed
64

65 66 67 68 69 70 71 72 73 74 75 76
  /**
   * The name of the additional pages hook
   */
  const getAdditionalPages = 'MarkupSitemap::getAdditionalPages';

  /**
   * Determine whether language support hooks have been added.
   *
   * @var bool
   */
  private static $languageSupportHooksApplied;

Mike Rockétt's avatar
Mike Rockétt committed
77 78
  /**
   * Current request URI
79
   *
Mike Rockétt's avatar
Mike Rockétt committed
80 81 82 83 84
   * @var string
   */
  protected $requestUri = '';

  /**
85
   * Current UrlSet
Mike Rockétt's avatar
Mike Rockétt committed
86
   *
87
   * @var Urlset
Mike Rockétt's avatar
Mike Rockétt committed
88
   */
89
  protected $urlSet;
Mike Rockétt's avatar
Mike Rockétt committed
90 91 92

  /**
   * Module installer
Mike Rockétt's avatar
Mike Rockétt committed
93 94
   * Requires ProcessWire 3.0.16+
   *
Mike Rockétt's avatar
Mike Rockétt committed
95 96 97 98
   * @throws WireException
   */
  public function ___install()
  {
Mike Rockétt's avatar
Mike Rockétt committed
99
    if (version_compare($this->config->version, '3.0.16') < 0) {
Mike Rockétt's avatar
Mike Rockétt committed
100
      throw new WireException('Requires ProcessWire 3.0.16+ to run.');
Mike Rockétt's avatar
init  
Mike Rockétt committed
101
    }
Mike Rockétt's avatar
Mike Rockétt committed
102 103 104 105 106 107 108 109 110 111 112 113 114
  }

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

  /**
   * Return a POSTed value or its default if not available
115 116 117
   *
   * @var string $valueKey
   * @var mixed $default
Mike Rockétt's avatar
Mike Rockétt committed
118 119 120 121 122 123 124 125
   * @return mixed
   */
  public function getPostedValue($valueKey, $default = false)
  {
    return $this->input->post->$valueKey ?: $default;
  }

  /**
126
   * Initialize the module
Mike Rockétt's avatar
Mike Rockétt committed
127 128 129
   *
   * @return void
   */
130
  public function init(): void
Mike Rockétt's avatar
Mike Rockétt committed
131 132 133 134 135 136
  {
    // 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()) {
137
        static::applyLanguageSupportHooks();
Mike Rockétt's avatar
Mike Rockétt committed
138
      }
139

Mike Rockétt's avatar
Mike Rockétt committed
140
      // Add the hook to process and render the sitemap.
141
      $this->addHookAfter('ProcessPageView::pageNotFound', $this, 'render');
Mike Rockétt's avatar
init  
Mike Rockétt committed
142 143
    }

Mike Rockétt's avatar
Mike Rockétt committed
144 145 146 147
    // 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
init  
Mike Rockétt committed
148
    }
149

Mike Rockétt's avatar
Mike Rockétt committed
150 151 152 153
    // 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
init  
Mike Rockétt committed
154
    }
Mike Rockétt's avatar
Mike Rockétt committed
155 156
  }

157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
  /**
   * Add the relevant page hooks for multi-language support
   *
   * @return void
   */
  public static function applyLanguageSupportHooks(): void
  {
    if (!static::$languageSupportHooksApplied) {
      foreach (['localUrl', 'localHttpUrl', 'localName'] as $pageHook) {
        $pageHookFunction = 'hookPage' . ucfirst($pageHook);
        wire()->addHook("Page::{$pageHook}", null, function ($event) use ($pageHookFunction) {
          wire('modules')->LanguageSupportPageNames->{$pageHookFunction}($event);
        });
      }
      static::$languageSupportHooksApplied = true;
    }
  }

Mike Rockétt's avatar
Mike Rockétt committed
175
  /**
176
   * Initialize the sitemap render by getting the root URI (giving
Mike Rockétt's avatar
Mike Rockétt committed
177 178 179
   * consideration to multi-site setups) and passing it to the
   * first/parent recursive render-method (addPages).
   *
180 181 182
   * 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
183 184
   *
   * @param HookEvent $event
185
   * @return void
Mike Rockétt's avatar
Mike Rockétt committed
186
   */
187
  public function render(HookEvent $event): void
Mike Rockétt's avatar
Mike Rockétt committed
188 189 190 191 192 193 194 195 196 197
  {
    // 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}";
      }
198 199
    }

Mike Rockétt's avatar
Mike Rockétt committed
200 201
    // Make sure that the root page exists.
    if (!$this->pages->get($rootPage) instanceof NullPage) {
Mike Rockétt's avatar
Mike Rockétt committed
202
      $event->return = $this->getSitemap($rootPage);
Mike Rockétt's avatar
Mike Rockétt committed
203
      header('Content-Type: application/xml', true, 200);
204

Mike Rockétt's avatar
Mike Rockétt committed
205 206
      // Prevent further hooks. This stops
      // SystemNotifications from displaying a 404 event
207
      // when /sitemap.xml is requested. Additionally,
Mike Rockétt's avatar
Mike Rockétt committed
208 209 210 211 212 213
      // it prevents further modification to the sitemap.
      $event->replace = true;
      $event->cancelHooks = true;
    }
  }

214 215 216 217 218 219
  /**
   * Get cached sitemap markup
   *
   * @param string $rootPage
   * @return string
   */
Mike Rockétt's avatar
Mike Rockétt committed
220
  protected function getSitemap(string $rootPage): string
221
  {
Mike Rockétt's avatar
Mike Rockétt committed
222 223 224 225 226 227
    $sitemap = $this->buildNewSitemap($rootPage);

    // Bail out early if debug mode is enabled, or if the
    // cache rules require a fresh Sitemap for this request.
    if ($this->requiresFreshSitemap()) {
      return $sitemap;
228 229 230
    }

    // Cache settings
231
    $cacheTtl = $this->cache_ttl ?: 3600;
232 233 234 235
    $cacheKey = 'MarkupSitemap';
    $cacheMethod = $this->cache_method ?: 'MarkupCache';

    // Attempt to fetch sitemap from cache
Mike Rockétt's avatar
Mike Rockétt committed
236 237 238 239
    $cache = $cacheMethod == 'WireCache'
      ? $this->cache
      : $this->modules->MarkupCache;

240
    $output = $cache->get($cacheKey, $cacheTtl);
241 242 243

    // If output is empty, generate and cache new sitemap
    if (empty($output)) {
Mike Rockétt's avatar
Mike Rockétt committed
244 245 246 247
      header('X-Cached-Sitemap: no, next-request');

      $output = $sitemap;

248
      if ($cacheMethod == 'WireCache') {
249
        $cache->save($cacheKey, $output, $cacheTtl);
250 251 252
      } else {
        $cache->save($output);
      }
Mike Rockétt's avatar
Mike Rockétt committed
253

254 255 256 257
      return $output;
    }

    header('X-Cached-Sitemap: yes');
Mike Rockétt's avatar
Mike Rockétt committed
258

259 260 261
    return $output;
  }

Mike Rockétt's avatar
Mike Rockétt committed
262 263
  /**
   * Get the root page URI
264
   *
Mike Rockétt's avatar
Mike Rockétt committed
265 266
   * @return string
   */
267
  protected function getRootPageUri(): string
Mike Rockétt's avatar
Mike Rockétt committed
268 269 270 271 272 273 274 275 276 277
  {
    return (string) str_ireplace(
      trim($this->config->urls->root, '/'),
      '',
      $this->sanitizer->path(dirname($this->requestUri))
    );
  }

  /**
   * Determine if the request is valud
278
   *
Mike Rockétt's avatar
Mike Rockétt committed
279 280
   * @return boolean
   */
281
  protected function isValidRequest(): bool
Mike Rockétt's avatar
Mike Rockétt committed
282 283 284
  {
    $valid = (bool) (
      $this->requestUri !== null &&
285
      strlen($this->requestUri) - strlen(self::sitemapUri) === strrpos($this->requestUri, self::sitemapUri)
Mike Rockétt's avatar
Mike Rockétt committed
286 287 288 289 290
    );

    return $valid;
  }

Mike Rockétt's avatar
Mike Rockétt committed
291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317
  /**
   * Determines whether or not a fresh sitemap is required
   * for the current request. A few factors are considered,
   * such as debug mode, the cache method, and the update policy.
   *
   * @return boolean
   */
  protected function requiresFreshSitemap(): bool
  {
    if ($this->config->debug) {
      header('X-Cached-Sitemap: no, debug');
      return true;
    }

    if ($this->cache_method === 'None') {
      header('X-Cached-Sitemap: no, disabled');
      return true;
    }

    if ($this->cache_policy === 'guest' && !$this->user->isGuest()) {
      header('X-Cached-Sitemap: no, guest-policy');
      return true;
    }

    return false;
  }

Mike Rockétt's avatar
Mike Rockétt committed
318 319 320
  /**
   * Check if the language is not default and that the
   * page is not available/statused in the default language.
321
   *
Mike Rockétt's avatar
Mike Rockétt committed
322 323
   * @param Language $language
   * @param Page $page
Mike Rockétt's avatar
Mike Rockétt committed
324 325
   * @return bool
   */
Mike Rockétt's avatar
Mike Rockétt committed
326
  protected function pageLanguageInvalid(Language $language, Page $page): bool
Mike Rockétt's avatar
Mike Rockétt committed
327 328 329 330 331 332
  {
    return (!$language->isDefault() && !$page->{"status{$language->id}"});
  }

  /**
   * Determine if the site uses the LanguageSupportPageNames module.
333
   *
Mike Rockétt's avatar
Mike Rockétt committed
334 335
   * @return bool
   */
336
  protected function siteUsesLanguageSupportPageNames(): bool
Mike Rockétt's avatar
Mike Rockétt committed
337 338 339
  {
    return $this->modules->isInstalled('LanguageSupportPageNames');
  }
340 341

  /**
Mike Rockétt's avatar
Mike Rockétt committed
342
   * Add languages to the location entry.
Mike Rockétt's avatar
Mike Rockétt committed
343
   *
344
   * @param Page $page
Mike Rockétt's avatar
Mike Rockétt committed
345
   * @param Url $url
346
   * @return void
347
   */
Mike Rockétt's avatar
Mike Rockétt committed
348
  protected function addLanguages(Page $page, Url $url): void
349 350
  {
    foreach ($this->languages as $altLanguage) {
351 352 353 354
      if ($this->pageLanguageInvalid($altLanguage, $page)) {
        continue;
      }

Mike Rockétt's avatar
Mike Rockétt committed
355
      $languageIsoName = $this->getLanguageIsoName($altLanguage);
356
      $url->addExtension(new Link($languageIsoName, $page->localHttpUrl($altLanguage)));
357 358 359
    }
  }

Mike Rockétt's avatar
Mike Rockétt committed
360 361 362 363 364 365 366 367 368
  /**
   * Get a language's ISO name
   *
   * @param Language $laguage
   * @return string
   */
  protected function getLanguageIsoName(Language $language): string
  {
    $usesDefaultIso = $language->isDefault()
369 370 371
    && $this->pages->get(1)->name === 'home'
    && !$this->modules->LanguageSupportPageNames->useHomeSegment
    && !empty($this->sitemap_default_iso);
Mike Rockétt's avatar
Mike Rockétt committed
372 373

    return $usesDefaultIso
Mike Rockétt's avatar
Mike Rockétt committed
374 375
      ? $this->sitemap_default_iso
      : $this->pages->get(1)->localName($language);
Mike Rockétt's avatar
Mike Rockétt committed
376 377
  }

378 379
  /**
   * Determine if a page can be included in the sitemap
380
   *
Mike Rockétt's avatar
Mike Rockétt committed
381 382
   * @param Page $page
   * @param array $options
383 384
   * @return bool
   */
Mike Rockétt's avatar
Mike Rockétt committed
385
  public function canBeIncluded(Page $page, ?array $options): bool
386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405
  {
    // 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.
406
   *
Mike Rockétt's avatar
Mike Rockétt committed
407
   * @param Page $page
408
   * @return void
409
   */
Mike Rockétt's avatar
Mike Rockétt committed
410
  protected function addPagesFromRoot(Page $page): void
411 412
  {
    // Get the saved options for this page
413
    $pageSitemapOptions = $this->modules->getConfig($this, "o$page->id") ?: static::$defaultPageOptions;
414 415 416 417 418 419 420 421 422 423 424 425 426 427 428

    // 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.)
429
    if ($page->viewable() && $this->canBeIncluded($page, $pageSitemapOptions)) {
430 431 432 433 434 435 436 437
      // 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;
          }
438

439
          $url = new Url($page->localHttpUrl($language));
Mike Rockétt's avatar
Mike Rockétt committed
440 441
          $url->setLastMod(ParseTimestamp::fromInt($page->modified));
          $this->addLanguages($page, $url);
442

443
          if ($pageSitemapOptions['priority']) {
444
            $url->setPriority(ParseFloat::asString($pageSitemapOptions['priority']));
445
          }
446

447 448 449
          if (!$pageSitemapOptions['excludes']['images']) {
            $this->addImages($page, $url, $language);
          }
450 451

          $this->urlSet->add($url);
452
          $this->addAdditionalPages($page, $language);
453 454 455 456 457
        }
      } 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);
Mike Rockétt's avatar
Mike Rockétt committed
458
        $url->setLastMod(ParseTimestamp::fromInt($page->modified));
459

460
        if ($pageSitemapOptions['priority']) {
461
          $url->setPriority(ParseFloat::asString($pageSitemapOptions['priority']));
462
        }
463

464 465 466
        if (!$pageSitemapOptions['excludes']['images']) {
          $this->addImages($page, $url);
        }
467 468

        $this->urlSet->add($url);
469
        $this->addAdditionalPages($page);
470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488
      }
    }

    // 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) {
Mike Rockétt's avatar
Mike Rockétt committed
489
          $this->addPagesFromRoot($child);
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
  /**
   * Add additional pages supplied via the getAdditionalPages() hook
   *
   * @param Page $page
   * @param Language $language
   * @return void
   */
  protected function addAdditionalPages(Page $page, Language $language = null): void
  {
    $additionalPages = $this->getAdditionalPages($page, $language);

    // Process each page from the data provided in the hook
    foreach ($additionalPages as $key => $item) {
      if (!$item['url']) {
        continue;
      }

      $url = new Url($item['url']);
      $modified = isset($item['modified']) ? $item['modified'] : $page->modified;

      $url->setLastMod(ParseTimestamp::fromInt($modified));

      if (isset($item['priority'])) {
        $url->setPriority(ParseFloat::asString($item['priority']));
      }

      // If language support is enabled, then we need to loop through each language
      // and add the alternate URLs of each additional page
      if ($this->siteUsesLanguageSupportPageNames()) {
        foreach ($this->languages as $language) {
          // Generate the additional URLs in the alternate language
          // and check if the same item is found within the alternate language URLs
          $urlsInLanguage = $this->getAdditionalPages($page, $language);

          if (isset($urlsInLanguage[$key])) {
            $languageItem = $urlsInLanguage[$key];
            if (!$languageItem['url']) {
              continue;
            }

            // Add the alternate language URL
            $languageIsoName = $this->getLanguageIsoName($language);
            $url->addExtension(new Link($languageIsoName, $languageItem['url']));
          }
        }
      }

      $this->urlSet->add($url);
    }
  }

546 547
  /**
   * Build a new sitemap (called when cache doesn't have one or we're debugging)
548
   *
Mike Rockétt's avatar
Mike Rockétt committed
549
   * @param string $rootPage
550 551
   * @return string
   */
Mike Rockétt's avatar
Mike Rockétt committed
552
  protected function buildNewSitemap(string $rootPage): string
553 554
  {
    $this->urlSet = new Urlset();
Mike Rockétt's avatar
Mike Rockétt committed
555
    $this->addPagesFromRoot($this->pages->get($rootPage));
556 557 558 559 560
    $writer = new XmlWriterDriver();

    $timestamp = date('c');
    $writer->addComment("Last generated: $timestamp");

561
    if ($this->sitemap_stylesheet) {
562
      $writer->addProcessingInstructions(
563 564 565 566 567
        'xml-stylesheet',
        'type="text/xsl" href="' . $this->getStylesheetUrl() . '"'
      );
    }

568 569 570
    $this->urlSet->accept($writer);

    return $writer->output();
571 572 573 574
  }

  /**
   * If using a stylesheet, return its absolute URL.
575
   *
576 577
   * @return string
   */
578
  protected function getStylesheetUrl(): string
579 580 581 582 583 584
  {
    if ($this->sitemap_stylesheet_custom
      && filter_var($this->sitemap_stylesheet_custom, FILTER_VALIDATE_URL)) {
      return $this->sitemap_stylesheet_custom;
    }

585
    return $this->urls->httpSiteModules . 'MarkupSitemap/assets/sitemap-stylesheet.xsl';
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

  /**
   * This hook adds support for pages that do not exist in the Page Tree,
   * such as those build behind a URL segment.
   *
   * It receives the actual parent Page as well as the Language, in the case
   * of a multi-language setup. The return value must b an array of
   * additional URL objects, containing the following three keys:
   *
   * `url` string, required
   * `modified` int, optional
   * `priority` float|string, optional
   *
   * To associate additional pages with their alternate-language variants, make sure
   * to add unique keys to the result array. Ex: an index or a language-independent ID.
   *
   * @param Page $page
   * @param Language $language
   * @return array
   */
  protected function ___getAdditionalPages(Page $page, Language $language = null): array
  {
    $return = [];

    if ($this->siteUsesLanguageSupportPageNames()) {
      static::applyLanguageSupportHooks();
    }

    return $return;
  }

Mike Rockétt's avatar
init  
Mike Rockétt committed
618
}