Sitemap::generateSitemap()   F
last analyzed

Complexity

Conditions 82
Paths > 20000

Size

Total Lines 406
Code Lines 266

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6806

Importance

Changes 15
Bugs 4 Features 1
Metric Value
eloc 266
c 15
b 4
f 1
dl 0
loc 406
ccs 0
cts 346
cp 0
rs 0
cc 82
nc 541985158
nop 1
crap 6806

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace nystudio107\seomatic\helpers;
4
5
use benf\neo\elements\Block as NeoBlock;
6
use Craft;
7
use craft\base\Element;
8
use craft\console\Application as ConsoleApplication;
9
use craft\db\Paginator;
10
use craft\elements\Asset;
11
use craft\elements\db\ElementQueryInterface;
12
use craft\elements\MatrixBlock;
13
use craft\errors\SiteNotFoundException;
14
use craft\fields\Assets as AssetsField;
15
use DateTime;
16
use nystudio107\seomatic\base\SeoElementInterface;
17
use nystudio107\seomatic\events\IncludeSitemapEntryEvent;
18
use nystudio107\seomatic\events\ModifySitemapQueryEvent;
19
use nystudio107\seomatic\fields\SeoSettings;
20
use nystudio107\seomatic\helpers\EagerLoad as EagerLoadHelper;
21
use nystudio107\seomatic\helpers\Field as FieldHelper;
22
use nystudio107\seomatic\models\MetaBundle;
23
use nystudio107\seomatic\models\SitemapTemplate;
24
use nystudio107\seomatic\Seomatic;
25
use Throwable;
26
use verbb\supertable\elements\SuperTableBlockElement as SuperTableBlock;
27
use yii\base\Event;
28
use yii\base\Exception;
29
use yii\helpers\Html;
30
use function array_intersect_key;
31
use function count;
32
use function in_array;
33
34
/**
35
 * @author    nystudio107
36
 * @package   Seomatic
37
 * @since     3.4.18
38
 */
39
class Sitemap
40
{
41
    /**
42
     * @event IncludeSitemapEntryEvent The event that is triggered when an entry is
43
     * about to be included in a sitemap
44
     *
45
     * ---
46
     * ```php
47
     * use nystudio107\seomatic\events\IncludeSitemapEntryEvent;
48
     * use nystudio107\seomatic\helpers\Sitemap;
49
     * use yii\base\Event;
50
     * Event::on(Sitemap::class, Sitemap::EVENT_INCLUDE_SITEMAP_ENTRY, function(IncludeSitemapEntryEvent $e) {
51
     *     $e->include = false;
52
     * });
53
     * ```
54
     */
55
    const EVENT_INCLUDE_SITEMAP_ENTRY = 'includeSitemapEntry';
56
57
    /**
58
     * @event ModifySitemapQueryEvent Allows the modification of the element query used to generate a sitemap
59
     *
60
     * ---
61
     * ```php
62
     * use nystudio107\seomatic\events\ModifySitemapQueryEvent;
63
     * use nystudio107\seomatic\helpers\Sitemap;
64
     * use yii\base\Event;
65
     * Event::on(Sitemap::class, Sitemap::EVENT_MODIFY_SITEMAP_QUERY, function(ModifySitemapQueryEvent $e) {
66
     *     $e->query->limit(10);
67
     * });
68
     * ```
69
     */
70
    public const EVENT_MODIFY_SITEMAP_QUERY = 'modifySitemapQuery';
71
72
    /**
73
     * @const The number of assets to return in a single paginated query
74
     */
75
    const SITEMAP_QUERY_PAGE_SIZE = 100;
76
77
    /**
78
     * Generate a sitemap with the passed in $params
79
     *
80
     * @param array $params
81
     * @return string
82
     * @throws SiteNotFoundException
83
     */
84
    public static function generateSitemap(array $params): ?string
85
    {
86
        $groupId = $params['groupId'];
87
        $type = $params['type'];
88
        $handle = $params['handle'];
89
        $siteId = $params['siteId'];
90
        $page = $params['page'];
91
92
        // Get an array of site ids for this site group
93
        $groupSiteIds = [];
94
95
        if (Seomatic::$settings->siteGroupsSeparate) {
96
            if (empty($groupId)) {
97
                try {
98
                    $thisSite = Craft::$app->getSites()->getSiteById($siteId);
99
                    if ($thisSite !== null) {
100
                        $group = $thisSite->getGroup();
101
                        $groupId = $group->id;
102
                    }
103
                } catch (Throwable $e) {
104
                    Craft::error($e->getMessage(), __METHOD__);
105
                }
106
            }
107
            $siteGroup = Craft::$app->getSites()->getGroupById($groupId);
108
            if ($siteGroup !== null) {
109
                $groupSiteIds = $siteGroup->getSiteIds();
110
            }
111
        }
112
113
        if (empty($groupSiteIds)) {
114
            $groupSiteIds = Craft::$app->getSites()->allSiteIds;
115
        }
116
117
        $lines = [];
118
        // Sitemap index XML header and opening tag
119
        $lines[] = '<?xml version="1.0" encoding="UTF-8"?>';
120
        $lines[] = '<?xml-stylesheet type="text/xsl" href="sitemap.xsl"?>';
121
        // One sitemap entry for each element
122
        $metaBundle = Seomatic::$plugin->metaBundles->getMetaBundleBySourceHandle(
123
            $type,
124
            $handle,
125
            $siteId
126
        );
127
        // If it doesn't exist, exit
128
        if ($metaBundle === null) {
129
            return null;
130
        }
131
        $multiSite = count($metaBundle->sourceAltSiteSettings) > 1;
132
        $totalElements = null;
133
        $urlsetLine = '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"';
134
        if ($metaBundle->metaSitemapVars->sitemapAssets) {
135
            $urlsetLine .= ' xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"';
136
            $urlsetLine .= ' xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"';
137
        }
138
        if ($multiSite) {
139
            $urlsetLine .= ' xmlns:xhtml="http://www.w3.org/1999/xhtml"';
140
        }
141
        if ((bool)$metaBundle->metaSitemapVars->newsSitemap) {
142
            $urlsetLine .= ' xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"';
143
        }
144
        $urlsetLine .= '>';
145
        $lines[] = $urlsetLine;
146
147
        // Get all of the elements for this meta bundle type
148
        $seoElement = Seomatic::$plugin->seoElements->getSeoElementByMetaBundleType($metaBundle->sourceBundleType);
149
150
        if ($seoElement !== null) {
151
            // Ensure `null` so that the resulting element query is correct
152
            if (empty($metaBundle->metaSitemapVars->sitemapLimit)) {
153
                $metaBundle->metaSitemapVars->sitemapLimit = null;
154
            }
155
            $totalElements = self::getTotalElementsInSitemap($seoElement, $metaBundle);
156
        }
157
158
        // If no elements exist, just exit
159
        if (!$totalElements) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $totalElements of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
160
            return null;
161
        }
162
163
        // Stash the sitemap attributes so they can be modified on a per-entry basis
164
        $stashedSitemapAttrs = $metaBundle->metaSitemapVars->getAttributes();
0 ignored issues
show
Bug introduced by
The method getAttributes() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

164
        /** @scrutinizer ignore-call */ 
165
        $stashedSitemapAttrs = $metaBundle->metaSitemapVars->getAttributes();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
165
        $stashedGlobalVarsAttrs = $metaBundle->metaGlobalVars->getAttributes();
0 ignored issues
show
Bug introduced by
The method getAttributes() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

165
        /** @scrutinizer ignore-call */ 
166
        $stashedGlobalVarsAttrs = $metaBundle->metaGlobalVars->getAttributes();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
166
        // Use craft\db\Paginator to paginate the results so we don't exceed any memory limits
167
        // See batch() and each() discussion here: https://github.com/yiisoft/yii2/issues/8420
168
        // and here: https://github.com/craftcms/cms/issues/7338
169
170
        // Allow listeners to modify the query before we use it
171
        $elementQuery = $seoElement::sitemapElementsQuery($metaBundle);
172
        $event = new ModifySitemapQueryEvent([
173
            'query' => $elementQuery,
174
            'metaBundle' => $metaBundle,
175
        ]);
176
        Event::trigger(self::class, self::EVENT_MODIFY_SITEMAP_QUERY, $event);
177
178
        $sitemapPageSize = $metaBundle->metaSitemapVars->sitemapPageSize;
179
        $elementQuery->limit($metaBundle->metaSitemapVars->sitemapLimit ?? null);
180
181
        // Eager load assets & relations
182
        if ($metaBundle->metaSitemapVars->sitemapAssets || $metaBundle->metaSitemapVars->sitemapFiles) {
183
            $elementQuery->with(EagerLoadHelper::sitemapEagerLoadMap($metaBundle));
184
        }
185
186
        // If this is not a paged sitemap, go through full results
187
        if (empty($sitemapPageSize)) {
188
            $pagedSitemap = false;
189
            $paginator = new Paginator($elementQuery, [
190
                'pageSize' => self::SITEMAP_QUERY_PAGE_SIZE,
191
            ]);
192
            $elements = $paginator->getPageResults();
193
        } else {
194
            $sitemapPage = empty($page) ? 1 : $page;
195
            $pagedSitemap = true;
196
            $elementQuery->limit($sitemapPageSize);
197
            $elementQuery->offset(($sitemapPage - 1) * $sitemapPageSize);
198
            $elements = $elementQuery->all();
199
            $totalElements = $sitemapPageSize;
200
            $paginator = new Paginator($elementQuery, [
201
                'pageSize' => $sitemapPageSize,
202
            ]);
203
        }
204
205
        $currentElement = 1;
206
207
        do {
208
            if (Craft::$app instanceof ConsoleApplication) {
209
                if ($pagedSitemap) {
210
                    $message = sprintf('Query %d elements', $sitemapPageSize);
211
                } else {
212
                    $message = sprintf('Query %d / %d - elements: %d',
213
                        $paginator->getCurrentPage(),
214
                        $paginator->getTotalPages(),
215
                        $paginator->getTotalResults());
216
                }
217
                echo $message . PHP_EOL;
218
            }
219
            /** @var Element $element */
220
            foreach ($elements as $element) {
221
                // Output some info if this is a console app
222
                if (Craft::$app instanceof ConsoleApplication) {
223
                    echo "Processing element {$currentElement}/{$totalElements} - {$element->title}" . PHP_EOL;
224
                }
225
226
                $metaBundle->metaSitemapVars->setAttributes($stashedSitemapAttrs, false);
227
                $metaBundle->metaGlobalVars->setAttributes($stashedGlobalVarsAttrs, false);
228
                // Combine in any per-entry type settings
229
                self::combineEntryTypeSettings($seoElement, $element, $metaBundle);
230
                // Make sure this entry isn't disabled
231
                self::combineFieldSettings($element, $metaBundle);
232
                // Special case for the __home__ URI
233
                $path = ($element->uri === '__home__') ? '' : $element->uri;
234
                // Check to see if robots is `none` or `no index`
235
                $robotsEnabled = true;
236
                if (!empty($metaBundle->metaGlobalVars->robots)) {
237
                    $robotsEnabled = $metaBundle->metaGlobalVars->robots !== 'none' &&
238
                        $metaBundle->metaGlobalVars->robots !== 'noindex';
239
                }
240
                $enabled = true;
241
                if (Seomatic::$craft34) {
242
                    $enabled = $element->getEnabledForSite($metaBundle->sourceSiteId);
243
                }
244
                $enabled = $enabled && $path !== null && $metaBundle->metaSitemapVars->sitemapUrls && $robotsEnabled;
245
                $event = new IncludeSitemapEntryEvent([
246
                    'include' => $enabled,
247
                    'element' => $element,
248
                    'metaBundle' => $metaBundle,
249
                ]);
250
                Event::trigger(self::class, self::EVENT_INCLUDE_SITEMAP_ENTRY, $event);
251
                // Only add in a sitemap entry if it meets our criteria
252
                if ($event->include) {
253
                    // Get the url and canonicalUrl
254
                    try {
255
                        $url = UrlHelper::siteUrl($path, null, null, $metaBundle->sourceSiteId);
256
                    } catch (Exception $e) {
257
                        $url = '';
258
                    }
259
                    $url = UrlHelper::absoluteUrlWithProtocol($url);
260
                    if (Seomatic::$settings->excludeNonCanonicalUrls) {
261
                        Seomatic::$matchedElement = $element;
262
                        MetaValue::cache();
263
                        $path = $metaBundle->metaGlobalVars->parsedValue('canonicalUrl');
264
                        try {
265
                            $canonicalUrl = UrlHelper::siteUrl($path, null, null, $metaBundle->sourceSiteId);
266
                        } catch (Exception $e) {
267
                            $canonicalUrl = '';
268
                        }
269
                        $canonicalUrl = UrlHelper::absoluteUrlWithProtocol($canonicalUrl);
270
                        if ($url !== $canonicalUrl) {
271
                            Craft::info("Excluding URL: {$url} from the sitemap because it does not match the Canonical URL: {$canonicalUrl} - " . $metaBundle->metaGlobalVars->canonicalUrl . " - " . $element->uri);
0 ignored issues
show
Bug introduced by
Are you sure $metaBundle->metaGlobalVars->canonicalUrl of type object|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

271
                            Craft::info("Excluding URL: {$url} from the sitemap because it does not match the Canonical URL: {$canonicalUrl} - " . /** @scrutinizer ignore-type */ $metaBundle->metaGlobalVars->canonicalUrl . " - " . $element->uri);
Loading history...
272
                            continue;
273
                        }
274
                    }
275
                    $dateUpdated = $element->dateUpdated ?? $element->dateCreated ?? new DateTime();
276
                    $lines[] = '<url>';
277
                    // Standard sitemap key/values
278
                    $lines[] = '<loc>';
279
                    $lines[] = self::encodeSitemapEntity($url);
280
                    $lines[] = '</loc>';
281
                    $lines[] = '<lastmod>';
282
                    $lines[] = $dateUpdated->format(DateTime::W3C);
283
                    $lines[] = '</lastmod>';
284
                    $lines[] = '<changefreq>';
285
                    $lines[] = $metaBundle->metaSitemapVars->sitemapChangeFreq;
286
                    $lines[] = '</changefreq>';
287
                    $lines[] = '<priority>';
288
                    $lines[] = $metaBundle->metaSitemapVars->sitemapPriority;
289
                    $lines[] = '</priority>';
290
                    // Handle alternate URLs if this is multi-site
291
                    if ($multiSite && $metaBundle->metaSitemapVars->sitemapAltLinks) {
292
                        $primarySiteId = Craft::$app->getSites()->getPrimarySite()->id;
293
                        foreach ($metaBundle->sourceAltSiteSettings as $altSiteSettings) {
294
                            if (in_array($altSiteSettings['siteId'], $groupSiteIds, false) && SiteHelper::siteEnabledWithUrls($altSiteSettings['siteId'])) {
295
                                $altElement = null;
296
                                if ($seoElement !== null) {
297
                                    /** @var Element $altElement */
298
                                    $altElement = $seoElement::sitemapAltElement(
299
                                        $metaBundle,
300
                                        $element->id,
301
                                        $altSiteSettings['siteId']
302
                                    );
303
                                }
304
                                // Make sure to only include the `hreflang` if the element exists,
305
                                // and sitemaps are on for it
306
                                if (Seomatic::$settings->addHrefLang && $altElement && $altElement->url) {
307
                                    list($altSourceId, $altSourceBundleType, $altSourceHandle, $altSourceSiteId, $altTypeId)
308
                                        = Seomatic::$plugin->metaBundles->getMetaSourceFromElement($altElement);
309
                                    $altMetaBundle = Seomatic::$plugin->metaBundles->getMetaBundleBySourceId(
310
                                        $altSourceBundleType,
311
                                        $altSourceId,
312
                                        $altSourceSiteId
313
                                    );
314
                                    if ($altMetaBundle) {
315
                                        $altEnabled = true;
316
                                        if (Seomatic::$craft34) {
317
                                            $altEnabled = $altElement->getEnabledForSite($altMetaBundle->sourceSiteId);
318
                                        }
319
                                        // Make sure this entry isn't disabled
320
                                        self::combineFieldSettings($altElement, $altMetaBundle);
321
                                        if ($altEnabled && $altMetaBundle->metaSitemapVars->sitemapUrls) {
322
                                            try {
323
                                                $altUrl = UrlHelper::siteUrl($altElement->url, null, null, $altSourceId);
0 ignored issues
show
Bug introduced by
It seems like $altElement->url can also be of type craft\base\ElementInterface[]; however, parameter $path of nystudio107\seomatic\helpers\UrlHelper::siteUrl() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

323
                                                $altUrl = UrlHelper::siteUrl(/** @scrutinizer ignore-type */ $altElement->url, null, null, $altSourceId);
Loading history...
324
                                            } catch (Exception $e) {
325
                                                $altUrl = $altElement->url;
326
                                            }
327
                                            $altUrl = UrlHelper::absoluteUrlWithProtocol($altUrl);
0 ignored issues
show
Bug introduced by
It seems like $altUrl can also be of type craft\base\ElementInterface[]; however, parameter $url of nystudio107\seomatic\hel...soluteUrlWithProtocol() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

327
                                            $altUrl = UrlHelper::absoluteUrlWithProtocol(/** @scrutinizer ignore-type */ $altUrl);
Loading history...
328
                                            // If this is the primary site, add it as x-default, too
329
                                            if ($primarySiteId === $altSourceSiteId && Seomatic::$settings->addXDefaultHrefLang) {
330
                                                $lines[] = '<xhtml:link rel="alternate"'
331
                                                    . ' hreflang="x-default"'
332
                                                    . ' href="' . self::encodeSitemapEntity($altUrl) . '"'
333
                                                    . ' />';
334
                                            }
335
                                            $lines[] = '<xhtml:link rel="alternate"'
336
                                                . ' hreflang="' . $altSiteSettings['language'] . '"'
337
                                                . ' href="' . self::encodeSitemapEntity($altUrl) . '"'
338
                                                . ' />';
339
                                        }
340
                                    }
341
                                }
342
                            }
343
                        }
344
                    }
345
                    // Handle news sitemaps https://developers.google.com/search/docs/crawling-indexing/sitemaps/news-sitemap
346
                    if ((bool)$metaBundle->metaSitemapVars->newsSitemap) {
347
                        $now = new DateTime();
348
                        $interval = $now->diff($dateUpdated);
349
                        if ($interval->days <= 2) {
350
                            $language = strtolower($element->getLanguage());
351
                            if (!str_starts_with($language, 'zh')) {
352
                                $language = substr($language, 0, 2);
353
                            }
354
                            $lines[] = '<news:news>';
355
                            $lines[] = '<news:publication>';
356
                            $lines[] = '<news:name>' . self::encodeSitemapEntity($metaBundle->metaSitemapVars->newsPublicationName) . '</news:name>';
357
                            $lines[] = '<news:language>' . $language . '</news:language>';
358
                            $lines[] = '</news:publication>';
359
                            $lines[] = '<news:publication_date>' . $dateUpdated->format(DateTime::W3C) . '</news:publication_date>';
360
                            $lines[] = '<news:title>' . self::encodeSitemapEntity($element->title) . '</news:title>';
361
                            $lines[] = '</news:news>';
362
                        }
363
                    }
364
                    // Handle any Assets
365
                    if ($metaBundle->metaSitemapVars->sitemapAssets) {
366
                        // Regular Assets fields
367
                        $assetFields = FieldHelper::fieldsOfTypeFromElement(
368
                            $element,
369
                            FieldHelper::ASSET_FIELD_CLASS_KEY,
370
                            true
371
                        );
372
                        foreach ($assetFields as $assetField) {
373
                            $assets = $element[$assetField];
374
                            if ($assets instanceof ElementQueryInterface) {
375
                                $assets = $assets->all();
376
                            }
377
                            /** @var Asset[] $assets */
378
                            foreach ($assets as $asset) {
379
                                self::assetSitemapItem($asset, $metaBundle, $lines);
380
                            }
381
                        }
382
                        // Assets embeded in Block fields
383
                        $blockFields = FieldHelper::fieldsOfTypeFromElement(
384
                            $element,
385
                            FieldHelper::BLOCK_FIELD_CLASS_KEY,
386
                            true
387
                        );
388
                        foreach ($blockFields as $blockField) {
389
                            $blocks = $element[$blockField];
390
                            if ($blocks instanceof ElementQueryInterface) {
391
                                $blocks = $blocks->all();
392
                            }
393
                            /** @var MatrixBlock[]|NeoBlock[]|SuperTableBlock[]|object[] $blocks */
394
                            foreach ($blocks as $block) {
395
                                $assetFields = [];
396
                                if ($block instanceof MatrixBlock) {
397
                                    $assetFields = FieldHelper::matrixFieldsOfType($block, AssetsField::class);
398
                                }
399
                                if ($block instanceof NeoBlock) {
400
                                    $assetFields = FieldHelper::neoFieldsOfType($block, AssetsField::class);
401
                                }
402
                                if ($block instanceof SuperTableBlock) {
403
                                    $assetFields = FieldHelper::superTableFieldsOfType($block, AssetsField::class);
404
                                }
405
                                foreach ($assetFields as $assetField) {
406
                                    $assets = $block[$assetField];
407
                                    if ($assets instanceof ElementQueryInterface) {
408
                                        $assets = $assets->all();
409
                                    }
410
                                    foreach ($assets as $asset) {
411
                                        self::assetSitemapItem($asset, $metaBundle, $lines);
412
                                    }
413
                                }
414
                            }
415
                        }
416
                    }
417
                    $lines[] = '</url>';
418
                }
419
                // Include links to any known file types in the assets fields
420
                if ($metaBundle->metaSitemapVars->sitemapFiles) {
421
                    // Regular Assets fields
422
                    $assetFields = FieldHelper::fieldsOfTypeFromElement(
423
                        $element,
424
                        FieldHelper::ASSET_FIELD_CLASS_KEY,
425
                        true
426
                    );
427
                    foreach ($assetFields as $assetField) {
428
                        $assets = $element[$assetField];
429
                        if ($assets instanceof ElementQueryInterface) {
430
                            $assets = $assets->all();
431
                        }
432
                        foreach ($assets as $asset) {
433
                            self::assetFilesSitemapLink($asset, $metaBundle, $lines);
434
                        }
435
                    }
436
                    // Assets embeded in Block fields
437
                    $blockFields = FieldHelper::fieldsOfTypeFromElement(
438
                        $element,
439
                        FieldHelper::BLOCK_FIELD_CLASS_KEY,
440
                        true
441
                    );
442
                    foreach ($blockFields as $blockField) {
443
                        $blocks = $element[$blockField];
444
                        if ($blocks instanceof ElementQueryInterface) {
445
                            $blocks = $blocks->all();
446
                        }
447
                        /** @var MatrixBlock[]|NeoBlock[]|SuperTableBlock[]|object[] $blocks */
448
                        foreach ($blocks as $block) {
449
                            $assetFields = [];
450
                            if ($block instanceof MatrixBlock) {
451
                                $assetFields = FieldHelper::matrixFieldsOfType($block, AssetsField::class);
452
                            }
453
                            if ($block instanceof SuperTableBlock) {
454
                                $assetFields = FieldHelper::superTableFieldsOfType($block, AssetsField::class);
455
                            }
456
                            if ($block instanceof NeoBlock) {
457
                                $assetFields = FieldHelper::neoFieldsOfType($block, AssetsField::class);
458
                            }
459
                            foreach ($assetFields as $assetField) {
460
                                $assets = $block[$assetField];
461
                                if ($assets instanceof ElementQueryInterface) {
462
                                    $assets = $assets->all();
463
                                }
464
                                foreach ($assets as $asset) {
465
                                    self::assetFilesSitemapLink($asset, $metaBundle, $lines);
466
                                }
467
                            }
468
                        }
469
                    }
470
                }
471
                $currentElement++;
472
            }
473
474
            if ($pagedSitemap) {
475
                break;
476
            }
477
478
            if ($paginator->getCurrentPage() == $paginator->getTotalPages()) {
479
                break;
480
            }
481
482
            $paginator->currentPage++;
483
            $elements = $paginator->getPageResults();
484
        } while (!empty($elements));
485
486
        // Sitemap closing tag
487
        $lines[] = '</urlset>';
488
489
        return implode('', $lines);
490
    }
491
492
    /**
493
     * Encode sitemap entities to make sure they follow the RFC-3986 standard for URIs, the RFC-3987 standard for IRIs
494
     * and the XML standard.
495
     * ref: https://sitemaps.org/protocol.html#escaping
496
     *
497
     * @param string $text
498
     * @return string
499
     */
500
    public static function encodeSitemapEntity(string $text): string
501
    {
502
        return Html::encode(UrlHelper::encodeUrl($text));
503
    }
504
505
    /**
506
     * Return the total number of elements in a sitemap, respecting metabundle settings.
507
     *
508
     * @param class-string<SeoElementInterface> $seoElementClass
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<SeoElementInterface> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<SeoElementInterface>.
Loading history...
509
     * @param MetaBundle $metaBundle
510
     * @return int|null
511
     */
512
    public static function getTotalElementsInSitemap(string $seoElementClass, MetaBundle $metaBundle)
513
    {
514
        // Allow listeners to modify the query before we use it
515
        $query = $seoElementClass::sitemapElementsQuery($metaBundle);
516
        $event = new ModifySitemapQueryEvent([
517
            'query' => $query,
518
            'metaBundle' => $metaBundle,
519
        ]);
520
        Event::trigger(self::class, self::EVENT_MODIFY_SITEMAP_QUERY, $event);
521
        $totalElements = $query->count();
522
523
        if ($metaBundle->metaSitemapVars->sitemapLimit && ($totalElements > $metaBundle->metaSitemapVars->sitemapLimit)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $metaBundle->metaSitemapVars->sitemapLimit of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
524
            $totalElements = $metaBundle->metaSitemapVars->sitemapLimit;
525
        }
526
527
        return $totalElements;
528
    }
529
530
    /**
531
     * Combine any per-entry type field settings from $element with the passed in
532
     * $metaBundle
533
     *
534
     * @param SeoElementInterface|string $seoElement
535
     * @param Element $element
536
     * @param MetaBundle $metaBundle
537
     */
538
    protected static function combineEntryTypeSettings($seoElement, Element $element, MetaBundle $metaBundle)
539
    {
540
        if (!empty($seoElement::typeMenuFromHandle($metaBundle->sourceHandle))) {
541
            list($sourceId, $sourceBundleType, $sourceHandle, $sourceSiteId, $typeId)
542
                = Seomatic::$plugin->metaBundles->getMetaSourceFromElement($element);
543
            $entryTypeBundle = Seomatic::$plugin->metaBundles->getMetaBundleBySourceId(
544
                $sourceBundleType,
545
                $sourceId,
546
                $sourceSiteId,
547
                $typeId
548
            );
549
            // Combine in any settings for this entry type
550
            if ($entryTypeBundle) {
551
                // Combine the meta sitemap vars
552
                $attributes = $entryTypeBundle->metaSitemapVars->getAttributes();
0 ignored issues
show
Bug introduced by
The method getAttributes() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

552
                /** @scrutinizer ignore-call */ 
553
                $attributes = $entryTypeBundle->metaSitemapVars->getAttributes();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
553
                $attributes = array_filter(
554
                    $attributes,
555
                    [ArrayHelper::class, 'preserveBools']
556
                );
557
                $metaBundle->metaSitemapVars->setAttributes($attributes, false);
558
559
                // Combine the meta global vars
560
                $attributes = $entryTypeBundle->metaGlobalVars->getAttributes();
0 ignored issues
show
Bug introduced by
The method getAttributes() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

560
                /** @scrutinizer ignore-call */ 
561
                $attributes = $entryTypeBundle->metaGlobalVars->getAttributes();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
561
                $attributes = array_filter(
562
                    $attributes,
563
                    [ArrayHelper::class, 'preserveBools']
564
                );
565
                $metaBundle->metaGlobalVars->setAttributes($attributes, false);
566
            }
567
        }
568
    }
569
570
    /**
571
     * Combine any SEO Settings field settings from $element with the passed in
572
     * $metaBundle
573
     *
574
     * @param Element $element
575
     * @param MetaBundle $metaBundle
576
     */
577
    protected static function combineFieldSettings(Element $element, MetaBundle $metaBundle)
578
    {
579
        $fieldHandles = FieldHelper::fieldsOfTypeFromElement(
580
            $element,
581
            FieldHelper::SEO_SETTINGS_CLASS_KEY,
582
            true
583
        );
584
        foreach ($fieldHandles as $fieldHandle) {
585
            if (!empty($element->$fieldHandle)) {
586
                /** @var SeoSettings $seoSettingsField */
587
                $seoSettingsField = Craft::$app->getFields()->getFieldByHandle($fieldHandle);
588
                /** @var MetaBundle $fieldMetaBundle */
589
                $fieldMetaBundle = $element->$fieldHandle;
590
                if ($seoSettingsField !== null) {
591
                    if ($seoSettingsField->sitemapTabEnabled) {
592
                        Seomatic::$plugin->metaBundles->pruneFieldMetaBundleSettings($fieldMetaBundle, $fieldHandle);
593
                        // Combine the meta sitemap vars
594
                        $attributes = $fieldMetaBundle->metaSitemapVars->getAttributes();
0 ignored issues
show
Bug introduced by
The method getAttributes() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

594
                        /** @scrutinizer ignore-call */ 
595
                        $attributes = $fieldMetaBundle->metaSitemapVars->getAttributes();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
595
596
                        // Get the explicitly inherited attributes
597
                        $inherited = array_keys(ArrayHelper::remove($attributes, 'inherited', []));
0 ignored issues
show
Bug introduced by
It seems like nystudio107\seomatic\hel..., 'inherited', array()) can also be of type null; however, parameter $array of array_keys() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

597
                        $inherited = array_keys(/** @scrutinizer ignore-type */ ArrayHelper::remove($attributes, 'inherited', []));
Loading history...
598
599
                        $attributes = array_intersect_key(
600
                            $attributes,
601
                            array_flip($seoSettingsField->sitemapEnabledFields)
602
                        );
603
                        $attributes = array_filter(
604
                            $attributes,
605
                            [ArrayHelper::class, 'preserveBools']
606
                        );
607
608
                        foreach ($inherited as $inheritedAttribute) {
609
                            unset($attributes[$inheritedAttribute]);
610
                        }
611
612
                        $metaBundle->metaSitemapVars->setAttributes($attributes, false);
613
                    }
614
                    // Combine the meta global vars
615
                    $attributes = $fieldMetaBundle->metaGlobalVars->getAttributes();
0 ignored issues
show
Bug introduced by
The method getAttributes() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

615
                    /** @scrutinizer ignore-call */ 
616
                    $attributes = $fieldMetaBundle->metaGlobalVars->getAttributes();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
616
                    $attributes = array_filter(
617
                        $attributes,
618
                        [ArrayHelper::class, 'preserveBools']
619
                    );
620
                    $metaBundle->metaGlobalVars->setAttributes($attributes, false);
621
                }
622
            }
623
        }
624
    }
625
626
    /**
627
     * @param Asset $asset
628
     * @param MetaBundle $metaBundle
629
     * @param array $lines
630
     */
631
    protected static function assetSitemapItem(Asset $asset, MetaBundle $metaBundle, array &$lines)
632
    {
633
        if ((bool)$asset->enabledForSite && $asset->getUrl() !== null) {
634
            switch ($asset->kind) {
635
                case 'image':
636
                    $transform = Craft::$app->getAssetTransforms()->getTransformByHandle($metaBundle->metaSitemapVars->sitemapAssetTransform ?? '');
637
                    $lines[] = '<image:image>';
638
                    $lines[] = '<image:loc>';
639
                    $lines[] = self::encodeSitemapEntity(UrlHelper::absoluteUrlWithProtocol($asset->getUrl($transform, true)));
640
                    $lines[] = '</image:loc>';
641
                    // Handle the dynamic field => property mappings
642
                    foreach ($metaBundle->metaSitemapVars->sitemapImageFieldMap as $row) {
643
                        $fieldName = $row['field'] ?? '';
644
                        $propName = $row['property'] ?? '';
645
                        if (!empty($fieldName) && !empty($asset[$fieldName]) && !empty($propName)) {
646
                            $lines[] = '<image:' . $propName . '>';
647
                            $lines[] = self::encodeSitemapEntity($asset[$fieldName]);
648
                            $lines[] = '</image:' . $propName . '>';
649
                        }
650
                    }
651
                    $lines[] = '</image:image>';
652
                    break;
653
654
                case 'video':
655
                    $lines[] = '<video:video>';
656
                    $lines[] = '<video:content_loc>';
657
                    $lines[] = self::encodeSitemapEntity(UrlHelper::absoluteUrlWithProtocol($asset->getUrl()));
658
                    $lines[] = '</video:content_loc>';
659
                    // Handle the dynamic field => property mappings
660
                    foreach ($metaBundle->metaSitemapVars->sitemapVideoFieldMap as $row) {
661
                        $fieldName = $row['field'] ?? '';
662
                        $propName = $row['property'] ?? '';
663
                        if (!empty($fieldName) && !empty($asset[$fieldName]) && !empty($propName)) {
664
                            $lines[] = '<video:' . $propName . '>';
665
                            $lines[] = self::encodeSitemapEntity($asset[$fieldName]);
666
                            $lines[] = '</video:' . $propName . '>';
667
                        }
668
                    }
669
                    $lines[] = '</video:video>';
670
                    break;
671
            }
672
        }
673
    }
674
675
    /**
676
     * @param Asset $asset
677
     * @param MetaBundle $metaBundle
678
     * @param array $lines
679
     */
680
    protected static function assetFilesSitemapLink(Asset $asset, MetaBundle $metaBundle, array &$lines)
681
    {
682
        if ((bool)$asset->enabledForSite && $asset->getUrl() !== null) {
683
            if (in_array($asset->kind, SitemapTemplate::FILE_TYPES, false)) {
684
                $dateUpdated = $asset->dateUpdated ?? $asset->dateCreated ?? new DateTime();
685
                $lines[] = '<url>';
686
                $lines[] = '<loc>';
687
                $lines[] = self::encodeSitemapEntity(UrlHelper::absoluteUrlWithProtocol($asset->getUrl()));
688
                $lines[] = '</loc>';
689
                $lines[] = '<lastmod>';
690
                $lines[] = $dateUpdated->format(DateTime::W3C);
691
                $lines[] = '</lastmod>';
692
                $lines[] = '<changefreq>';
693
                $lines[] = $metaBundle->metaSitemapVars->sitemapChangeFreq;
694
                $lines[] = '</changefreq>';
695
                $lines[] = '<priority>';
696
                $lines[] = $metaBundle->metaSitemapVars->sitemapPriority;
697
                $lines[] = '</priority>';
698
                $lines[] = '</url>';
699
            }
700
        }
701
    }
702
}
703