Passed
Push — develop ( a0f72e...5f9c0a )
by Andrew
15:15
created

Sitemaps::sitemapUrlForBundle()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 35
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

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