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