Passed
Push — v3 ( 01a338...22e12d )
by Andrew
53:06 queued 24:15
created

src/services/Sitemaps.php (1 issue)

Severity
1
<?php
2
/**
3
 * SEOmatic plugin for Craft CMS 3.x
4
 *
5
 * A turnkey SEO implementation for Craft CMS that is comprehensive, powerful,
6
 * and flexible
7
 *
8
 * @link      https://nystudio107.com
9
 * @copyright Copyright (c) 2017 nystudio107
10
 */
11
12
namespace nystudio107\seomatic\services;
13
14
use nystudio107\seomatic\jobs\GenerateSitemap;
15
use nystudio107\seomatic\models\MetaBundle;
16
use nystudio107\seomatic\Seomatic;
17
use nystudio107\seomatic\base\FrontendTemplate;
18
use nystudio107\seomatic\base\SitemapInterface;
19
use nystudio107\seomatic\helpers\UrlHelper;
20
use nystudio107\seomatic\models\FrontendTemplateContainer;
21
use nystudio107\seomatic\models\SitemapIndexTemplate;
22
use nystudio107\seomatic\models\SitemapTemplate;
23
use nystudio107\seomatic\models\SitemapCustomTemplate;
24
25
use Craft;
26
use craft\base\Component;
27
use craft\base\Element;
28
use craft\base\ElementInterface;
29
use craft\errors\SiteNotFoundException;
30
use craft\events\RegisterUrlRulesEvent;
31
use craft\web\UrlManager;
32
33
use yii\base\Event;
34
use yii\base\Exception;
35
use yii\base\InvalidConfigException;
36
use yii\caching\TagDependency;
37
38
/**
39
 * @author    nystudio107
40
 * @package   Seomatic
41
 * @since     3.0.0
42
 */
43
class Sitemaps extends Component implements SitemapInterface
44
{
45
    // Constants
46
    // =========================================================================
47
48
    const SEOMATIC_SITEMAPINDEX_CONTAINER = Seomatic::SEOMATIC_HANDLE.SitemapIndexTemplate::TEMPLATE_TYPE;
49
50
    const SEOMATIC_SITEMAP_CONTAINER = Seomatic::SEOMATIC_HANDLE.SitemapTemplate::TEMPLATE_TYPE;
51
52
    const SEOMATIC_SITEMAPCUSTOM_CONTAINER = Seomatic::SEOMATIC_HANDLE.SitemapCustomTemplate::TEMPLATE_TYPE;
53
54
    const SEARCH_ENGINE_SUBMISSION_URLS = [
55
        'google' => 'https://www.google.com/ping?sitemap=',
56
        'bing' => 'https://www.bing.com/ping?sitemap=',
57
    ];
58
59
    // Protected Properties
60
    // =========================================================================
61
62
    /**
63
     * @var FrontendTemplateContainer
64
     */
65
    protected $sitemapTemplateContainer;
66
67
    // Public Methods
68
    // =========================================================================
69
70
    /**
71
     *
72
     */
73
    public function init()
74
    {
75
        parent::init();
76
    }
77
78
    /**
79
     * Load in the sitemap frontend template containers
80
     */
81
    public function loadSitemapContainers()
82
    {
83
        if (Seomatic::$settings->sitemapsEnabled) {
84
            $this->sitemapTemplateContainer = FrontendTemplateContainer::create();
85
            // The Sitemap Index
86
            $sitemapIndexTemplate = SitemapIndexTemplate::create();
87
            $this->sitemapTemplateContainer->addData($sitemapIndexTemplate, self::SEOMATIC_SITEMAPINDEX_CONTAINER);
88
            // A custom sitemap
89
            $sitemapCustomTemplate = SitemapCustomTemplate::create();
90
            $this->sitemapTemplateContainer->addData($sitemapCustomTemplate, self::SEOMATIC_SITEMAPCUSTOM_CONTAINER);
91
            // A generic sitemap
92
            $sitemapTemplate = SitemapTemplate::create();
93
            $this->sitemapTemplateContainer->addData($sitemapTemplate, self::SEOMATIC_SITEMAP_CONTAINER);
94
            // Handler: UrlManager::EVENT_REGISTER_SITE_URL_RULES
95
            Event::on(
96
                UrlManager::class,
97
                UrlManager::EVENT_REGISTER_SITE_URL_RULES,
98
                function (RegisterUrlRulesEvent $event) {
99
                    Craft::debug(
100
                        'UrlManager::EVENT_REGISTER_SITE_URL_RULES',
101
                        __METHOD__
102
                    );
103
                    // Register our sitemap routes
104
                    $event->rules = array_merge(
105
                        $event->rules,
106
                        $this->sitemapRouteRules()
107
                    );
108
                }
109
            );
110
        }
111
    }
112
113
    /**
114
     * @return array
115
     */
116
    public function sitemapRouteRules(): array
117
    {
118
        $rules = [];
119
        $groups = Craft::$app->getSites()->getAllGroups();
120
        $groupId = $groups[0]->id;
121
        $currentSite = null;
122
        try {
123
            $currentSite = Craft::$app->getSites()->getCurrentSite();
124
        } catch (SiteNotFoundException $e) {
125
            Craft::error($e->getMessage(), __METHOD__);
126
        }
127
        if ($currentSite) {
128
            try {
129
                $groupId = $currentSite->getGroup()->id;
130
            } catch (InvalidConfigException $e) {
131
                Craft::error($e->getMessage(), __METHOD__);
132
            }
133
        }
134
        // Add the route to redirect sitemap.xml to the actual sitemap
135
        $route =
136
            Seomatic::$plugin->handle
137
            .'/'
138
            .'sitemap'
139
            .'/'
140
            .'sitemap-index-redirect';
141
        $rules['sitemap.xml'] = [
142
            'route' => $route,
143
        ];
144
        // Add the route for the sitemap.xsl styles
145
        $route =
146
            Seomatic::$plugin->handle
147
            .'/'
148
            .'sitemap'
149
            .'/'
150
            .'sitemap-styles';
151
        $rules['sitemap.xsl'] = [
152
            'route' => $route,
153
        ];
154
        // Add the route for the sitemap-empty.xsl styles
155
        $route =
156
            Seomatic::$plugin->handle
157
            .'/'
158
            .'sitemap'
159
            .'/'
160
            .'sitemap-empty-styles';
161
        $rules['sitemap-empty.xsl'] = [
162
            'route' => $route,
163
        ];
164
        // Add all of the frontend container routes
165
        foreach ($this->sitemapTemplateContainer->data as $sitemapTemplate) {
166
            /** @var $sitemapTemplate FrontendTemplate */
167
            $rules = array_merge(
168
                $rules,
169
                $sitemapTemplate->routeRules()
170
            );
171
        }
172
173
        return $rules;
174
    }
175
176
    /**
177
     * See if any of the entry types have robots enable and sitemap urls enabled
178
     *
179
     * @param MetaBundle $metaBundle
180
     * @return bool
181
     */
182
    public function anyEntryTypeHasSitemapUrls(MetaBundle $metaBundle): bool
183
    {
184
        $result = false;
185
        $seoElement = Seomatic::$plugin->seoElements->getSeoElementByMetaBundleType($metaBundle->sourceBundleType);
186
        if ($seoElement) {
187
            if (!empty($seoElement::typeMenuFromHandle($metaBundle->sourceHandle))) {
188
                $section = $seoElement::sourceModelFromHandle($metaBundle->sourceHandle);
189
                if ($section !== null) {
190
                    $entryTypes = $section->getEntryTypes();
191
                    // Fetch each meta bundle for each entry type to see if _any_ of them have sitemap URLs
192
                    foreach ($entryTypes as $entryType) {
193
                        $entryTypeBundle = Seomatic::$plugin->metaBundles->getMetaBundleBySourceId(
194
                            $metaBundle->sourceBundleType,
195
                            $metaBundle->sourceId,
196
                            $metaBundle->sourceSiteId,
197
                            $entryType->id
198
                        );
199
                        if ($entryTypeBundle) {
200
                            $robotsEnabled = true;
201
                            if (!empty($entryTypeBundle->metaGlobalVars->robots)) {
202
                                $robotsEnabled = $entryTypeBundle->metaGlobalVars->robots !== 'none' &&
203
                                    $entryTypeBundle->metaGlobalVars->robots !== 'noindex';
204
                            }
205
                            if ($entryTypeBundle->metaSitemapVars->sitemapUrls && $robotsEnabled) {
206
                                $result = true;
207
                            }
208
                        }
209
                    }
210
                }
211
            }
212
        }
213
214
        return $result;
215
    }
216
217
    /**
218
     * @param string $template
219
     * @param array  $params
220
     *
221
     * @return string
222
     */
223
    public function renderTemplate(string $template, array $params = []): string
224
    {
225
        $html = '';
226
        if (!empty($this->sitemapTemplateContainer->data[$template])) {
227
            /** @var FrontendTemplate $sitemapTemplate */
228
            $sitemapTemplate = $this->sitemapTemplateContainer->data[$template];
229
            $html = $sitemapTemplate->render($params);
230
        }
231
232
        return $html;
233
    }
234
235
    /**
236
     * Submit the sitemap index to the search engine services
237
     */
238
    public function submitSitemapIndex()
239
    {
240
        if (Seomatic::$settings->sitemapsEnabled && Seomatic::$environment === 'live' && Seomatic::$settings->submitSitemaps) {
241
            // Submit the sitemap to each search engine
242
            $searchEngineUrls = self::SEARCH_ENGINE_SUBMISSION_URLS;
243
            foreach ($searchEngineUrls as &$url) {
244
                $groups = Craft::$app->getSites()->getAllGroups();
245
                foreach ($groups as $group) {
246
                    $groupSiteIds = $group->getSiteIds();
247
                    if (!empty($groupSiteIds)) {
248
                        $siteId = $groupSiteIds[0];
249
                        $sitemapIndexUrl = $this->sitemapIndexUrlForSiteId($siteId);
250
                        if (!empty($sitemapIndexUrl)) {
251
                            $submissionUrl = $url.urlencode($sitemapIndexUrl);
252
                            // create new guzzle client
253
                            $guzzleClient = Craft::createGuzzleClient(['timeout' => 120, 'connect_timeout' => 120]);
254
                            // Submit the sitemap index to each search engine
255
                            try {
256
                                $guzzleClient->post($submissionUrl);
257
                                Craft::info(
258
                                    'Sitemap index submitted to: '.$submissionUrl,
259
                                    __METHOD__
260
                                );
261
                            } catch (\Exception $e) {
262
                                Craft::error(
263
                                    'Error submitting sitemap index to: '.$submissionUrl.' - '.$e->getMessage(),
264
                                    __METHOD__
265
                                );
266
                            }
267
                        }
268
                    }
269
                }
270
            }
271
        }
272
    }
273
274
    /**
275
     * Submit the bundle sitemap to the search engine services
276
     *
277
     * @param ElementInterface $element
278
     */
279
    public function submitSitemapForElement(ElementInterface $element)
280
    {
281
        if (Seomatic::$settings->sitemapsEnabled && Seomatic::$environment === 'live' && Seomatic::$settings->submitSitemaps) {
282
            /** @var Element $element */
283
            list($sourceId, $sourceBundleType, $sourceHandle, $sourceSiteId, $typeId)
284
                = Seomatic::$plugin->metaBundles->getMetaSourceFromElement($element);
285
            // Submit the sitemap to each search engine
286
            $searchEngineUrls = self::SEARCH_ENGINE_SUBMISSION_URLS;
287
            foreach ($searchEngineUrls as &$url) {
288
                $sitemapUrl = $this->sitemapUrlForBundle($sourceBundleType, $sourceHandle, $sourceSiteId);
289
                if (!empty($sitemapUrl)) {
290
                    $submissionUrl = $url.urlencode($sitemapUrl);
291
                    // create new guzzle client
292
                    $guzzleClient = Craft::createGuzzleClient(['timeout' => 120, 'connect_timeout' => 120]);
293
                    // Submit the sitemap index to each search engine
294
                    try {
295
                        $guzzleClient->post($submissionUrl);
296
                        Craft::info(
297
                            'Sitemap index submitted to: '.$submissionUrl,
298
                            __METHOD__
299
                        );
300
                    } catch (\Exception $e) {
301
                        Craft::error(
302
                            'Error submitting sitemap index to: '.$submissionUrl.' - '.$e->getMessage(),
303
                            __METHOD__
304
                        );
305
                    }
306
                }
307
            }
308
        }
309
    }
310
311
    /**
312
     * Submit the bundle sitemap to the search engine services
313
     *
314
     * @param int $siteId
315
     */
316
    public function submitCustomSitemap(int $siteId)
317
    {
318
        if (Seomatic::$settings->sitemapsEnabled && Seomatic::$environment === 'live' && Seomatic::$settings->submitSitemaps) {
319
            // Submit the sitemap to each search engine
320
            $searchEngineUrls = self::SEARCH_ENGINE_SUBMISSION_URLS;
321
            foreach ($searchEngineUrls as &$url) {
322
                $sitemapUrl = $this->sitemapCustomUrlForSiteId($siteId);
323
                if (!empty($sitemapUrl)) {
324
                    $submissionUrl = $url.urlencode($sitemapUrl);
325
                    // create new guzzle client
326
                    $guzzleClient = Craft::createGuzzleClient(['timeout' => 120, 'connect_timeout' => 120]);
327
                    // Submit the sitemap index to each search engine
328
                    try {
329
                        $guzzleClient->post($submissionUrl);
330
                        Craft::info(
331
                            'Sitemap Custom submitted to: '.$submissionUrl,
332
                            __METHOD__
333
                        );
334
                    } catch (\Exception $e) {
335
                        Craft::error(
336
                            'Error submitting sitemap index to: '.$submissionUrl.' - '.$e->getMessage(),
337
                            __METHOD__
338
                        );
339
                    }
340
                }
341
            }
342
        }
343
    }
344
345
    /**
346
     * Get the URL to the $siteId's sitemap index
347
     *
348
     * @param int|null $siteId
349
     *
350
     * @return string
351
     */
352
    public function sitemapIndexUrlForSiteId(int $siteId = null): string
353
    {
354
        $url = '';
355
        $sites = Craft::$app->getSites();
356
        if ($siteId === null) {
357
            $siteId = $sites->currentSite->id ?? 1;
358
        }
359
        $site = $sites->getSiteById($siteId);
360
        if ($site !== null) {
361
            try {
362
                $url = UrlHelper::siteUrl(
363
                    '/sitemaps-'
364
                    .$site->groupId
365
                    .'-sitemap.xml',
366
                    null,
367
                    null,
368
                    $siteId
369
                );
370
            } catch (Exception $e) {
371
                Craft::error($e->getMessage(), __METHOD__);
372
            }
373
        }
374
375
        return $url;
376
    }
377
378
    /**
379
     * Return all of the sitemap indexes the current group of sites
380
     *
381
     * @return string
382
     */
383
    public function sitemapIndex(): string
384
    {
385
        $result = '';
386
        $sites = [];
387
        // If sitemaps aren't enabled globally, return nothing for the sitemap index
388
        if (!Seomatic::$settings->sitemapsEnabled) {
389
            return '';
390
        }
391
        if (Seomatic::$settings->siteGroupsSeparate) {
392
            // Get only the sites that are in the current site's group
393
            try {
394
                $siteGroup = Craft::$app->getSites()->getCurrentSite()->getGroup();
395
            } catch (InvalidConfigException $e) {
396
                $siteGroup = null;
397
                Craft::error($e->getMessage(), __METHOD__);
398
            }
399
            // If we can't get a group, just use the current site
400
            if ($siteGroup === null) {
401
                $sites = [Craft::$app->getSites()->getCurrentSite()];
402
            } else  {
403
                $sites = $siteGroup->getSites();
404
            }
405
        } else {
406
            $sites = Craft::$app->getSites()->getAllSites();
407
        }
408
409
        foreach($sites as $site) {
410
            $result .= 'sitemap: ' . $this->sitemapIndexUrlForSiteId($site->id) . PHP_EOL;
411
        }
412
413
        return rtrim($result, PHP_EOL);
414
    }
415
416
    /**
417
     * @param int|null $siteId
418
     *
419
     * @return string
420
     */
421
    public function sitemapCustomUrlForSiteId(int $siteId = null)
422
    {
423
        $url = '';
424
        $sites = Craft::$app->getSites();
425
        if ($siteId === null) {
426
            $siteId = $sites->currentSite->id ?? 1;
427
        }
428
        $site = $sites->getSiteById($siteId);
429
        if ($site) {
430
            try {
431
                $url = UrlHelper::siteUrl(
432
                    '/sitemaps-'
433
                    .$site->groupId
434
                    .'-'
435
                    .SitemapCustomTemplate::CUSTOM_SCOPE
436
                    .'-'
437
                    .SitemapCustomTemplate::CUSTOM_HANDLE
438
                    .'-'
439
                    .$siteId
440
                    .'-sitemap.xml',
441
                    null,
442
                    null,
443
                    $siteId
444
                );
445
            } catch (Exception $e) {
446
                Craft::error($e->getMessage(), __METHOD__);
447
            }
448
        }
449
450
        return $url;
451
    }
452
453
    /**
454
     * @param string   $sourceBundleType
455
     * @param string   $sourceHandle
456
     * @param int|null $siteId
457
     *
458
     * @return string
459
     */
460
    public function sitemapUrlForBundle(string $sourceBundleType, string $sourceHandle, int $siteId = null): string
461
    {
462
        $url = '';
463
        $sites = Craft::$app->getSites();
464
        if ($siteId === null) {
465
            $siteId = $sites->currentSite->id ?? 1;
466
        }
467
        $site = $sites->getSiteById($siteId);
468
        $metaBundle = Seomatic::$plugin->metaBundles->getMetaBundleBySourceHandle(
469
            $sourceBundleType,
470
            $sourceHandle,
471
            $siteId
472
        );
473
        if ($site && $metaBundle) {
474
            try {
475
                $url = UrlHelper::siteUrl(
476
                    '/sitemaps-'
477
                    .$site->groupId
478
                    .'-'
479
                    .$metaBundle->sourceBundleType
480
                    .'-'
481
                    .$metaBundle->sourceHandle
482
                    .'-'
483
                    .$metaBundle->sourceSiteId
484
                    .'-sitemap.xml',
485
                    null,
486
                    null,
487
                    $siteId
488
                );
489
            } catch (Exception $e) {
490
                Craft::error($e->getMessage(), __METHOD__);
491
            }
492
        }
493
494
        return $url;
495
    }
496
497
    /**
498
     * Invalidate all of the sitemap caches
499
     */
500
    public function invalidateCaches()
501
    {
502
        $cache = Craft::$app->getCache();
503
        TagDependency::invalidate($cache, self::GLOBAL_SITEMAP_CACHE_TAG);
504
        Craft::info(
505
            'All sitemap caches cleared',
506
            __METHOD__
507
        );
508
    }
509
510
    /**
511
     * Invalidate the sitemap cache passed in $handle
512
     *
513
     * @param string $handle
514
     * @param int    $siteId
515
     * @param string $type
516
     */
517
    public function invalidateSitemapCache(string $handle, int $siteId, string $type)
518
    {
519
        $cache = Craft::$app->getCache();
520
        // If the queue should be run automatically, do it now
521
        TagDependency::invalidate($cache, SitemapTemplate::SITEMAP_CACHE_TAG.$handle.$siteId);
522
        Craft::info(
523
            'Sitemap cache cleared: '.$handle,
524
            __METHOD__
525
        );
526
        $sites = Craft::$app->getSites();
527
        if ($siteId === null) {
528
            $siteId = $sites->currentSite->id ?? 1;
529
        }
530
        $site = $sites->getSiteById($siteId);
531
        $groupId = $site->groupId;
532
        $sitemapTemplate = SitemapTemplate::create();
533
        $xml = $sitemapTemplate->render(
0 ignored issues
show
The assignment to $xml is dead and can be removed.
Loading history...
534
            [
535
                'groupId' => $groupId,
536
                'type' => $type,
537
                'handle' => $handle,
538
                'siteId' => $siteId,
539
            ]
540
        );
541
    }
542
543
    /**
544
     * Invalidate the sitemap index cache
545
     */
546
    public function invalidateSitemapIndexCache()
547
    {
548
        $cache = Craft::$app->getCache();
549
        TagDependency::invalidate($cache, SitemapIndexTemplate::SITEMAP_INDEX_CACHE_TAG);
550
        Craft::info(
551
            'Sitemap index cache cleared',
552
            __METHOD__
553
        );
554
    }
555
}
556