SitemapIndexTemplate::rules()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 7
ccs 0
cts 5
cp 0
crap 2
rs 10
c 0
b 0
f 0
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\models;
13
14
use Craft;
15
use DateTime;
16
use Exception;
17
use nystudio107\seomatic\base\FrontendTemplate;
18
use nystudio107\seomatic\base\SitemapInterface;
19
use nystudio107\seomatic\events\RegisterSitemapsEvent;
20
use nystudio107\seomatic\events\RegisterSitemapUrlsEvent;
21
use nystudio107\seomatic\helpers\MetaValue as MetaValueHelper;
22
use nystudio107\seomatic\helpers\Sitemap;
23
use nystudio107\seomatic\Seomatic;
24
use yii\base\Event;
25
use yii\caching\TagDependency;
26
use yii\helpers\Html;
27
use yii\web\NotFoundHttpException;
28
use function in_array;
29
30
/**
31
 * @author    nystudio107
32
 * @package   Seomatic
33
 * @since     3.0.0
34
 */
35
class SitemapIndexTemplate extends FrontendTemplate implements SitemapInterface
36
{
37
    // Constants
38
    // =========================================================================
39
40
    /**
41
     * @event RegisterSitemapsEvent The event that is triggered when registering
42
     * additional sitemaps for the sitemap index.
43
     *
44
     * ---
45
     * ```php
46
     * use nystudio107\seomatic\events\RegisterSitemapsEvent;
47
     * use nystudio107\seomatic\models\SitemapIndexTemplate;
48
     * use yii\base\Event;
49
     * Event::on(SitemapIndexTemplate::class, SitemapIndexTemplate::EVENT_REGISTER_SITEMAPS, function(RegisterSitemapsEvent $e) {
50
     *     $e->sitemaps[] = [
51
     *         'loc' => $url,
52
     *         'lastmod' => $lastMod,
53
     *     ];
54
     * });
55
     * ```
56
     */
57
    const EVENT_REGISTER_SITEMAPS = 'registerSitemaps';
58
59
    const TEMPLATE_TYPE = 'SitemapIndexTemplate';
60
61
    const CACHE_KEY = 'seomatic_sitemap_index';
62
63
    const SITEMAP_INDEX_CACHE_TAG = 'seomatic_sitemap_index';
64
65
    // Static Methods
66
    // =========================================================================
67
68
    /**
69
     * @param array $config
70
     *
71
     * @return null|SitemapIndexTemplate
72
     */
73
    public static function create(array $config = [])
74
    {
75
        $defaults = [
76
            'path' => 'sitemaps-<groupId:\d+>-sitemap.xml',
77
            'template' => '',
78
            'controller' => 'sitemap',
79
            'action' => 'sitemap-index',
80
        ];
81
        $config = array_merge($config, $defaults);
82
83
        return new SitemapIndexTemplate($config);
84
    }
85
86
    // Public Properties
87
    // =========================================================================
88
89
    // Public Methods
90
    // =========================================================================
91
92
    /**
93
     * @inheritdoc
94
     */
95
    public function rules(): array
96
    {
97
        $rules = parent::rules();
98
        $rules = array_merge($rules, [
99
        ]);
100
101
        return $rules;
102
    }
103
104
    /**
105
     * @inheritdoc
106
     */
107
    public function fields(): array
108
    {
109
        return parent::fields();
110
    }
111
112
    /**
113
     * Get the filename of the sitemap index.
114
     *
115
     * @param int $groupId
116
     * @return string
117
     */
118
    public function getFilename(int $groupId): string
119
    {
120
        return 'sitemaps-' . $groupId . '-sitemap.xml';
121
    }
122
123
    /**
124
     * @inheritdoc
125
     *
126
     * @throws NotFoundHttpException if the sitemap.xml doesn't exist
127
     */
128
    public function render(array $params = []): string
129
    {
130
        $cache = Craft::$app->getCache();
131
        $groupId = $params['groupId'];
132
        $siteId = $params['siteId'];
133
        if (Seomatic::$settings->siteGroupsSeparate) {
134
            $siteGroup = Craft::$app->getSites()->getGroupById($groupId);
135
            if ($siteGroup === null) {
136
                throw new NotFoundHttpException(Craft::t('seomatic', 'Sitemap.xml not found for groupId {groupId}', [
137
                    'groupId' => $groupId,
138
                ]));
139
            }
140
            $groupSiteIds = $siteGroup->getSiteIds();
141
        } else {
142
            $groupSiteIds = Craft::$app->getSites()->allSiteIds;
143
        }
144
145
        $dependency = new TagDependency([
146
            'tags' => [
147
                self::GLOBAL_SITEMAP_CACHE_TAG,
148
                self::SITEMAP_INDEX_CACHE_TAG,
149
            ],
150
        ]);
151
152
        $cacheDuration = Seomatic::$plugin->helper::isPreview() ? 1 : Seomatic::$cacheDuration;
153
154
        return $cache->getOrSet(self::CACHE_KEY . $groupId . '.' . $siteId, function() use ($groupSiteIds, $siteId) {
155
            Craft::info(
156
                'Sitemap index cache miss',
157
                __METHOD__
158
            );
159
            $lines = [];
160
            // Sitemap index XML header and opening tag
161
            $lines[] = '<?xml version="1.0" encoding="UTF-8"?>';
162
            $lines[] = '<?xml-stylesheet type="text/xsl" href="sitemap.xsl"?>';
163
            $lines[] = '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
164
            // One sitemap entry for each MeteBundle
165
            $metaBundles = Seomatic::$plugin->metaBundles->getContentMetaBundlesForSiteId($siteId);
166
            Seomatic::$plugin->metaBundles->pruneVestigialMetaBundles($metaBundles);
167
            /** @var MetaBundle $metaBundle */
168
            foreach ($metaBundles as $metaBundle) {
169
                $sitemapUrls = $metaBundle->metaSitemapVars->sitemapUrls;
170
                // Check to see if robots is `none` or `no index`
171
                $robotsEnabled = true;
172
                if (!empty($metaBundle->metaGlobalVars->robots)) {
173
                    $robotsEnabled = $metaBundle->metaGlobalVars->robots !== 'none' &&
174
                        $metaBundle->metaGlobalVars->robots !== 'noindex';
175
                }
176
                if (Seomatic::$plugin->sitemaps->anyEntryTypeHasSitemapUrls($metaBundle)) {
177
                    $robotsEnabled = true;
178
                    $sitemapUrls = true;
179
                }
180
                // Only add in a sitemap entry if it meets our criteria
181
                if (in_array($metaBundle->sourceSiteId, $groupSiteIds, false)
182
                    && $sitemapUrls
183
                    && $robotsEnabled) {
184
                    // Get all of the elements for this meta bundle type
185
                    $seoElement = Seomatic::$plugin->seoElements->getSeoElementByMetaBundleType($metaBundle->sourceBundleType);
186
                    $totalElements = 0;
187
                    $pageCount = 0;
188
189
                    if ($seoElement !== null) {
190
                        // Ensure `null` so that the resulting element query is correct
191
                        if (empty($metaBundle->metaSitemapVars->sitemapLimit)) {
192
                            $metaBundle->metaSitemapVars->sitemapLimit = null;
193
                        }
194
195
                        $totalElements = Sitemap::getTotalElementsInSitemap($seoElement, $metaBundle) ?? 0;
196
197
                        $pageSize = $metaBundle->metaSitemapVars->sitemapPageSize;
198
                        $pageCount = (!empty($pageSize) && $pageSize > 0) ? ceil($totalElements / $pageSize) : 1;
199
                    }
200
201
                    // Only add a sitemap to the sitemap index if there's at least 1 element in the resulting sitemap
202
                    if ($totalElements > 0 && $pageCount > 0) {
203
                        for ($page = 1; $page <= $pageCount; $page++) {
204
                            $sitemapUrl = Seomatic::$plugin->sitemaps->sitemapUrlForBundle(
205
                                $metaBundle->sourceBundleType,
206
                                $metaBundle->sourceHandle,
207
                                $metaBundle->sourceSiteId,
208
                                $pageCount > 1 ? $page : 0 // No paging, if only one page
209
                            );
210
211
                            $lines[] = '<sitemap>';
212
                            $lines[] = '<loc>';
213
                            $lines[] = Html::encode($sitemapUrl);
214
                            $lines[] = '</loc>';
215
216
                            if ($metaBundle->sourceDateUpdated !== null) {
217
                                $lines[] = '<lastmod>';
218
                                $lines[] = $metaBundle->sourceDateUpdated->format(DateTime::W3C);
219
                                $lines[] = '</lastmod>';
220
                            }
221
222
                            $lines[] = '</sitemap>';
223
                        }
224
                    }
225
                }
226
            }
227
            // Custom sitemap entries
228
            $metaBundle = Seomatic::$plugin->metaBundles->getGlobalMetaBundle($siteId, false);
229
            if ($metaBundle !== null) {
230
                $this->addAdditionalSitemapUrls($metaBundle, $siteId, $lines);
231
                $this->addAdditionalSitemaps($metaBundle, $siteId, $lines);
232
            }
233
            // Sitemap index closing tag
234
            $lines[] = '</sitemapindex>';
235
236
            return implode('', $lines);
237
        }, $cacheDuration, $dependency);
238
    }
239
240
    /**
241
     * Invalidate the sitemap index cache
242
     */
243
    public function invalidateCache()
244
    {
245
        $cache = Craft::$app->getCache();
246
        TagDependency::invalidate($cache, self::SITEMAP_INDEX_CACHE_TAG);
247
        Craft::info(
248
            'Sitemap index cache cleared',
249
            __METHOD__
250
        );
251
    }
252
253
    // Protected Methods
254
    // =========================================================================
255
256
    /**
257
     * Add an additional sitemap to the sitemap index, coming from the global
258
     * meta bundle metaSiteVars->additionalSitemaps
259
     *
260
     * @param MetaBundle $metaBundle
261
     * @param int $groupSiteId
262
     * @param array $lines
263
     *
264
     * @throws Exception
265
     */
266
    protected function addAdditionalSitemaps(MetaBundle $metaBundle, int $groupSiteId, array &$lines)
267
    {
268
        $additionalSitemaps = $metaBundle->metaSiteVars->additionalSitemaps;
269
        $additionalSitemaps = empty($additionalSitemaps) ? [] : $additionalSitemaps;
270
        // Allow plugins/modules to add custom URLs
271
        $event = new RegisterSitemapsEvent([
272
            'sitemaps' => $additionalSitemaps,
273
            'siteId' => $groupSiteId,
274
        ]);
275
        Event::trigger(SitemapIndexTemplate::class, SitemapIndexTemplate::EVENT_REGISTER_SITEMAPS, $event);
276
        $additionalSitemaps = array_filter($event->sitemaps);
277
        // Output the sitemap index
278
        if (!empty($additionalSitemaps)) {
279
            foreach ($additionalSitemaps as $additionalSitemap) {
280
                if (!empty($additionalSitemap['loc'])) {
281
                    $loc = MetaValueHelper::parseString($additionalSitemap['loc']);
282
                    $lines[] = '<sitemap>';
283
                    $lines[] = '<loc>';
284
                    $lines[] = Html::encode($loc);
285
                    $lines[] = '</loc>';
286
                    // Find the most recent date
287
                    $dateUpdated = !empty($additionalSitemap['lastmod'])
288
                        ? $additionalSitemap['lastmod']
289
                        : new DateTime();
290
                    $lines[] = '<lastmod>';
291
                    $lines[] = $dateUpdated->format(DateTime::W3C);
292
                    $lines[] = '</lastmod>';
293
                    $lines[] = '</sitemap>';
294
                }
295
            }
296
        }
297
    }
298
299
    /**
300
     * Add an additional "custom" sitemap to the sitemap index, with URLs coming from
301
     * the global meta bundle metaSiteVars->additionalSitemapUrls
302
     *
303
     * @param MetaBundle $metaBundle
304
     * @param int $groupSiteId
305
     * @param array $lines
306
     *
307
     * @throws Exception
308
     */
309
    protected function addAdditionalSitemapUrls(MetaBundle $metaBundle, int $groupSiteId, array &$lines)
310
    {
311
        $additionalSitemapUrls = $metaBundle->metaSiteVars->additionalSitemapUrls;
312
        $additionalSitemapUrls = empty($additionalSitemapUrls) ? [] : $additionalSitemapUrls;
313
        // Allow plugins/modules to add custom URLs
314
        $event = new RegisterSitemapUrlsEvent([
315
            'sitemaps' => $additionalSitemapUrls,
316
            'siteId' => $groupSiteId,
317
        ]);
318
        Event::trigger(SitemapCustomTemplate::class, SitemapCustomTemplate::EVENT_REGISTER_SITEMAP_URLS, $event);
319
        $additionalSitemapUrls = array_filter($event->sitemaps);
320
        // Output the sitemap index
321
        if (!empty($additionalSitemapUrls)) {
322
            $sitemapUrl = Seomatic::$plugin->sitemaps->sitemapCustomUrlForSiteId(
323
                $groupSiteId
324
            );
325
            $lines[] = '<sitemap>';
326
            $lines[] = '<loc>';
327
            $lines[] = Html::encode($sitemapUrl);
328
            $lines[] = '</loc>';
329
            // Find the most recent date
330
            $dateUpdated = $metaBundle->metaSiteVars->additionalSitemapUrlsDateUpdated
331
                ?? new DateTime();
332
            foreach ($additionalSitemapUrls as $additionalSitemapUrl) {
333
                if (!empty($additionalSitemapUrl['lastmod'])) {
334
                    if ($additionalSitemapUrl['lastmod'] > $dateUpdated) {
335
                        $dateUpdated = $additionalSitemapUrl['lastmod'];
336
                    }
337
                }
338
            }
339
            if ($dateUpdated !== null) {
340
                $lines[] = '<lastmod>';
341
                $lines[] = $dateUpdated->format(DateTime::W3C);
342
                $lines[] = '</lastmod>';
343
            }
344
            $lines[] = '</sitemap>';
345
        }
346
    }
347
}
348