Sitemap::generateSitemap()   F
last analyzed

Complexity

Conditions 74
Paths > 20000

Size

Total Lines 380
Code Lines 248

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 5550

Importance

Changes 11
Bugs 2 Features 1
Metric Value
eloc 248
c 11
b 2
f 1
dl 0
loc 380
ccs 0
cts 241
cp 0
rs 0
cc 74
nc 271007110
nop 1
crap 5550

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

163
        /** @scrutinizer ignore-call */ 
164
        $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...
164
        $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

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

267
                            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...
268
                            continue;
269
                        }
270
                    }
271
                    $dateUpdated = $element->dateUpdated ?? $element->dateCreated ?? new DateTime();
272
                    $lines[] = '<url>';
273
                    // Standard sitemap key/values
274
                    $lines[] = '<loc>';
275
                    $lines[] = self::encodeSitemapEntity($url);
276
                    $lines[] = '</loc>';
277
                    $lines[] = '<lastmod>';
278
                    $lines[] = $dateUpdated->format(DateTime::W3C);
279
                    $lines[] = '</lastmod>';
280
                    $lines[] = '<changefreq>';
281
                    $lines[] = $metaBundle->metaSitemapVars->sitemapChangeFreq;
282
                    $lines[] = '</changefreq>';
283
                    $lines[] = '<priority>';
284
                    $lines[] = $metaBundle->metaSitemapVars->sitemapPriority;
285
                    $lines[] = '</priority>';
286
                    // Handle alternate URLs if this is multi-site
287
                    if ($multiSite && $metaBundle->metaSitemapVars->sitemapAltLinks) {
288
                        $primarySiteId = Craft::$app->getSites()->getPrimarySite()->id;
289
                        foreach ($metaBundle->sourceAltSiteSettings as $altSiteSettings) {
290
                            if (in_array($altSiteSettings['siteId'], $groupSiteIds, false) && SiteHelper::siteEnabledWithUrls($altSiteSettings['siteId'])) {
291
                                $altElement = null;
292
                                if ($seoElement !== null) {
293
                                    /** @var Element $altElement */
294
                                    $altElement = $seoElement::sitemapAltElement(
295
                                        $metaBundle,
296
                                        $element->id,
297
                                        $altSiteSettings['siteId']
298
                                    );
299
                                }
300
                                // Make sure to only include the `hreflang` if the element exists,
301
                                // and sitemaps are on for it
302
                                if (Seomatic::$settings->addHrefLang && $altElement && $altElement->url) {
303
                                    list($altSourceId, $altSourceBundleType, $altSourceHandle, $altSourceSiteId, $altTypeId)
304
                                        = Seomatic::$plugin->metaBundles->getMetaSourceFromElement($altElement);
305
                                    $altMetaBundle = Seomatic::$plugin->metaBundles->getMetaBundleBySourceId(
306
                                        $altSourceBundleType,
307
                                        $altSourceId,
308
                                        $altSourceSiteId
309
                                    );
310
                                    if ($altMetaBundle) {
311
                                        $altEnabled = $altElement->getEnabledForSite($altMetaBundle->sourceSiteId);
312
                                        // Make sure this entry isn't disabled
313
                                        self::combineFieldSettings($altElement, $altMetaBundle);
314
                                        if ($altEnabled && $altMetaBundle->metaSitemapVars->sitemapUrls) {
315
                                            try {
316
                                                $altUrl = UrlHelper::siteUrl($altElement->url, null, null, $altSourceId);
317
                                            } catch (Exception $e) {
318
                                                $altUrl = $altElement->url;
319
                                            }
320
                                            $altUrl = UrlHelper::absoluteUrlWithProtocol($altUrl);
321
                                            // If this is the primary site, add it as x-default, too
322
                                            if ($primarySiteId === $altSourceSiteId && Seomatic::$settings->addXDefaultHrefLang) {
323
                                                $lines[] = '<xhtml:link rel="alternate"'
324
                                                    . ' hreflang="x-default"'
325
                                                    . ' href="' . self::encodeSitemapEntity($altUrl) . '"'
326
                                                    . ' />';
327
                                            }
328
                                            $lines[] = '<xhtml:link rel="alternate"'
329
                                                . ' hreflang="' . $altSiteSettings['language'] . '"'
330
                                                . ' href="' . self::encodeSitemapEntity($altUrl) . '"'
331
                                                . ' />';
332
                                        }
333
                                    }
334
                                }
335
                            }
336
                        }
337
                    }
338
                    // Handle news sitemaps https://developers.google.com/search/docs/crawling-indexing/sitemaps/news-sitemap
339
                    if ((bool)$metaBundle->metaSitemapVars->newsSitemap) {
340
                        $now = new DateTime();
341
                        $interval = $now->diff($dateUpdated);
342
                        if ($interval->days <= 2) {
343
                            $language = strtolower($element->getLanguage());
344
                            if (!str_starts_with($language, 'zh')) {
345
                                $language = substr($language, 0, 2);
346
                            }
347
                            $lines[] = '<news:news>';
348
                            $lines[] = '<news:publication>';
349
                            $lines[] = '<news:name>' . self::encodeSitemapEntity($metaBundle->metaSitemapVars->newsPublicationName) . '</news:name>';
350
                            $lines[] = '<news:language>' . $language . '</news:language>';
351
                            $lines[] = '</news:publication>';
352
                            $lines[] = '<news:publication_date>' . $dateUpdated->format(DateTime::W3C) . '</news:publication_date>';
353
                            $lines[] = '<news:title>' . self::encodeSitemapEntity($element->title) . '</news:title>';
354
                            $lines[] = '</news:news>';
355
                        }
356
                    }
357
                    // Handle any Assets
358
                    if ($metaBundle->metaSitemapVars->sitemapAssets) {
359
                        // Regular Assets fields
360
                        $assetFields = FieldHelper::fieldsOfTypeFromElement(
361
                            $element,
362
                            FieldHelper::ASSET_FIELD_CLASS_KEY,
363
                            true
364
                        );
365
                        foreach ($assetFields as $assetField) {
366
                            $assets = $element[$assetField]->all();
367
                            /** @var Asset[] $assets */
368
                            foreach ($assets as $asset) {
369
                                self::assetSitemapItem($asset, $metaBundle, $lines);
370
                            }
371
                        }
372
                        // Assets embeded in Block fields
373
                        $blockFields = FieldHelper::fieldsOfTypeFromElement(
374
                            $element,
375
                            FieldHelper::BLOCK_FIELD_CLASS_KEY,
376
                            true
377
                        );
378
                        foreach ($blockFields as $blockField) {
379
                            $blocks = $element[$blockField]->all();
380
                            /** @var MatrixBlock[]|NeoBlock[]|SuperTableBlock[]|object[] $blocks */
381
                            foreach ($blocks as $block) {
382
                                $assetFields = [];
383
                                if ($block instanceof MatrixBlock) {
384
                                    $assetFields = FieldHelper::matrixFieldsOfType($block, AssetsField::class);
385
                                }
386
                                if ($block instanceof NeoBlock) {
387
                                    $assetFields = FieldHelper::neoFieldsOfType($block, AssetsField::class);
388
                                }
389
                                if ($block instanceof SuperTableBlock) {
390
                                    $assetFields = FieldHelper::superTableFieldsOfType($block, AssetsField::class);
391
                                }
392
                                foreach ($assetFields as $assetField) {
393
                                    foreach ($block[$assetField]->all() as $asset) {
394
                                        self::assetSitemapItem($asset, $metaBundle, $lines);
395
                                    }
396
                                }
397
                            }
398
                        }
399
                    }
400
                    $lines[] = '</url>';
401
                }
402
                // Include links to any known file types in the assets fields
403
                if ($metaBundle->metaSitemapVars->sitemapFiles) {
404
                    // Regular Assets fields
405
                    $assetFields = FieldHelper::fieldsOfTypeFromElement(
406
                        $element,
407
                        FieldHelper::ASSET_FIELD_CLASS_KEY,
408
                        true
409
                    );
410
                    foreach ($assetFields as $assetField) {
411
                        $assets = $element[$assetField]->all();
412
                        foreach ($assets as $asset) {
413
                            self::assetFilesSitemapLink($asset, $metaBundle, $lines);
414
                        }
415
                    }
416
                    // Assets embeded in Block fields
417
                    $blockFields = FieldHelper::fieldsOfTypeFromElement(
418
                        $element,
419
                        FieldHelper::BLOCK_FIELD_CLASS_KEY,
420
                        true
421
                    );
422
                    foreach ($blockFields as $blockField) {
423
                        $blocks = $element[$blockField]->all();
424
                        /** @var MatrixBlock[]|NeoBlock[]|SuperTableBlock[]|object[] $blocks */
425
                        foreach ($blocks as $block) {
426
                            $assetFields = [];
427
                            if ($block instanceof MatrixBlock) {
428
                                $assetFields = FieldHelper::matrixFieldsOfType($block, AssetsField::class);
429
                            }
430
                            if ($block instanceof SuperTableBlock) {
431
                                $assetFields = FieldHelper::superTableFieldsOfType($block, AssetsField::class);
432
                            }
433
                            if ($block instanceof NeoBlock) {
434
                                $assetFields = FieldHelper::neoFieldsOfType($block, AssetsField::class);
435
                            }
436
                            foreach ($assetFields as $assetField) {
437
                                foreach ($block[$assetField]->all() as $asset) {
438
                                    self::assetFilesSitemapLink($asset, $metaBundle, $lines);
439
                                }
440
                            }
441
                        }
442
                    }
443
                }
444
                $currentElement++;
445
            }
446
447
            if ($pagedSitemap) {
448
                break;
449
            }
450
451
            if ($paginator->getCurrentPage() == $paginator->getTotalPages()) {
452
                break;
453
            }
454
455
            $paginator->currentPage++;
456
            $elements = $paginator->getPageResults();
457
        } while (!empty($elements));
458
459
        // Sitemap closing tag
460
        $lines[] = '</urlset>';
461
462
        return implode('', $lines);
463
    }
464
465
    /**
466
     * Encode sitemap entities to make sure they follow the RFC-3986 standard for URIs, the RFC-3987 standard for IRIs
467
     * and the XML standard.
468
     * ref: https://sitemaps.org/protocol.html#escaping
469
     *
470
     * @param string $text
471
     * @return string
472
     */
473
    public static function encodeSitemapEntity(string $text): string
474
    {
475
        return Html::encode(UrlHelper::encodeUrl($text));
476
    }
477
478
    /**
479
     * Return the total number of elements in a sitemap, respecting metabundle settings.
480
     *
481
     * @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...
482
     * @param MetaBundle $metaBundle
483
     * @return int|null
484
     */
485
    public static function getTotalElementsInSitemap(string $seoElementClass, MetaBundle $metaBundle): ?int
486
    {
487
        // Allow listeners to modify the query before we use it
488
        $query = $seoElementClass::sitemapElementsQuery($metaBundle);
489
        $event = new ModifySitemapQueryEvent([
490
            'query' => $query,
491
            'metaBundle' => $metaBundle,
492
        ]);
493
        Event::trigger(self::class, self::EVENT_MODIFY_SITEMAP_QUERY, $event);
494
        $totalElements = $query->count();
495
496
        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...
497
            $totalElements = $metaBundle->metaSitemapVars->sitemapLimit;
498
        }
499
500
        return $totalElements;
501
    }
502
503
    /**
504
     * Combine any per-entry type field settings from $element with the passed in
505
     * $metaBundle
506
     *
507
     * @param SeoElementInterface|string $seoElement
508
     * @param Element $element
509
     * @param MetaBundle $metaBundle
510
     */
511
    protected static function combineEntryTypeSettings($seoElement, Element $element, MetaBundle $metaBundle)
512
    {
513
        if (!empty($seoElement::typeMenuFromHandle($metaBundle->sourceHandle))) {
514
            list($sourceId, $sourceBundleType, $sourceHandle, $sourceSiteId, $typeId)
515
                = Seomatic::$plugin->metaBundles->getMetaSourceFromElement($element);
516
            $entryTypeBundle = Seomatic::$plugin->metaBundles->getMetaBundleBySourceId(
517
                $sourceBundleType,
518
                $sourceId,
519
                $sourceSiteId,
520
                $typeId
521
            );
522
            // Combine in any settings for this entry type
523
            if ($entryTypeBundle) {
524
                // Combine the meta sitemap vars
525
                $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

525
                /** @scrutinizer ignore-call */ 
526
                $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...
526
                $attributes = array_filter(
527
                    $attributes,
528
                    [ArrayHelper::class, 'preserveBools']
529
                );
530
                $metaBundle->metaSitemapVars->setAttributes($attributes, false);
531
532
                // Combine the meta global vars
533
                $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

533
                /** @scrutinizer ignore-call */ 
534
                $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...
534
                $attributes = array_filter(
535
                    $attributes,
536
                    [ArrayHelper::class, 'preserveBools']
537
                );
538
                $metaBundle->metaGlobalVars->setAttributes($attributes, false);
539
            }
540
        }
541
    }
542
543
    /**
544
     * Combine any SEO Settings field settings from $element with the passed in
545
     * $metaBundle
546
     *
547
     * @param Element $element
548
     * @param MetaBundle $metaBundle
549
     */
550
    protected static function combineFieldSettings(Element $element, MetaBundle $metaBundle)
551
    {
552
        $fieldHandles = FieldHelper::fieldsOfTypeFromElement(
553
            $element,
554
            FieldHelper::SEO_SETTINGS_CLASS_KEY,
555
            true
556
        );
557
        foreach ($fieldHandles as $fieldHandle) {
558
            if (!empty($element->$fieldHandle)) {
559
                /** @var SeoSettings $seoSettingsField */
560
                $seoSettingsField = Craft::$app->getFields()->getFieldByHandle($fieldHandle);
561
                /** @var MetaBundle $metaBundle */
562
                $fieldMetaBundle = $element->$fieldHandle;
563
                if ($seoSettingsField !== null) {
564
                    if ($seoSettingsField->sitemapTabEnabled) {
565
                        Seomatic::$plugin->metaBundles->pruneFieldMetaBundleSettings($fieldMetaBundle, $fieldHandle);
566
                        // Combine the meta sitemap vars
567
                        $attributes = $fieldMetaBundle->metaSitemapVars->getAttributes();
568
569
                        // Get the explicitly inherited attributes
570
                        $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

570
                        $inherited = array_keys(/** @scrutinizer ignore-type */ ArrayHelper::remove($attributes, 'inherited', []));
Loading history...
571
572
                        $attributes = array_intersect_key(
573
                            $attributes,
574
                            array_flip((array)$seoSettingsField->sitemapEnabledFields)
575
                        );
576
                        $attributes = array_filter(
577
                            $attributes,
578
                            [ArrayHelper::class, 'preserveBools']
579
                        );
580
581
                        foreach ($inherited as $inheritedAttribute) {
582
                            unset($attributes[$inheritedAttribute]);
583
                        }
584
585
                        $metaBundle->metaSitemapVars->setAttributes($attributes, false);
586
                    }
587
                    // Combine the meta global vars
588
                    $attributes = $fieldMetaBundle->metaGlobalVars->getAttributes();
589
                    $attributes = array_filter(
590
                        $attributes,
591
                        [ArrayHelper::class, 'preserveBools']
592
                    );
593
                    $metaBundle->metaGlobalVars->setAttributes($attributes, false);
594
                }
595
            }
596
        }
597
    }
598
599
    /**
600
     * @param Asset $asset
601
     * @param MetaBundle $metaBundle
602
     * @param array $lines
603
     */
604
    protected static function assetSitemapItem(Asset $asset, MetaBundle $metaBundle, array &$lines)
605
    {
606
        if ((bool)$asset->enabledForSite && $asset->getUrl() !== null) {
607
            switch ($asset->kind) {
608
                case 'image':
609
                    $transform = Craft::$app->getImageTransforms()->getTransformByHandle($metaBundle->metaSitemapVars->sitemapAssetTransform ?? '');
610
                    $lines[] = '<image:image>';
611
                    $lines[] = '<image:loc>';
612
                    $lines[] = self::encodeSitemapEntity(UrlHelper::absoluteUrlWithProtocol($asset->getUrl($transform, true)));
613
                    $lines[] = '</image:loc>';
614
                    // Handle the dynamic field => property mappings
615
                    foreach ($metaBundle->metaSitemapVars->sitemapImageFieldMap as $row) {
616
                        $fieldName = $row['field'] ?? '';
617
                        $propName = $row['property'] ?? '';
618
                        if (!empty($fieldName) && !empty($asset[$fieldName]) && !empty($propName)) {
619
                            $lines[] = '<image:' . $propName . '>';
620
                            $lines[] = self::encodeSitemapEntity($asset[$fieldName]);
621
                            $lines[] = '</image:' . $propName . '>';
622
                        }
623
                    }
624
                    $lines[] = '</image:image>';
625
                    break;
626
627
                case 'video':
628
                    $lines[] = '<video:video>';
629
                    $lines[] = '<video:content_loc>';
630
                    $lines[] = self::encodeSitemapEntity(UrlHelper::absoluteUrlWithProtocol($asset->getUrl()));
631
                    $lines[] = '</video:content_loc>';
632
                    // Handle the dynamic field => property mappings
633
                    foreach ($metaBundle->metaSitemapVars->sitemapVideoFieldMap as $row) {
634
                        $fieldName = $row['field'] ?? '';
635
                        $propName = $row['property'] ?? '';
636
                        if (!empty($fieldName) && !empty($asset[$fieldName]) && !empty($propName)) {
637
                            $lines[] = '<video:' . $propName . '>';
638
                            $lines[] = self::encodeSitemapEntity($asset[$fieldName]);
639
                            $lines[] = '</video:' . $propName . '>';
640
                        }
641
                    }
642
                    $lines[] = '</video:video>';
643
                    break;
644
            }
645
        }
646
    }
647
648
    /**
649
     * @param Asset $asset
650
     * @param MetaBundle $metaBundle
651
     * @param array $lines
652
     */
653
    protected static function assetFilesSitemapLink(Asset $asset, MetaBundle $metaBundle, array &$lines)
654
    {
655
        if ((bool)$asset->enabledForSite && $asset->getUrl() !== null) {
656
            if (in_array($asset->kind, SitemapTemplate::FILE_TYPES, false)) {
657
                $dateUpdated = $asset->dateUpdated ?? $asset->dateCreated ?? new DateTime();
658
                $lines[] = '<url>';
659
                $lines[] = '<loc>';
660
                $lines[] = self::encodeSitemapEntity(UrlHelper::absoluteUrlWithProtocol($asset->getUrl()));
661
                $lines[] = '</loc>';
662
                $lines[] = '<lastmod>';
663
                $lines[] = $dateUpdated->format(DateTime::W3C);
664
                $lines[] = '</lastmod>';
665
                $lines[] = '<changefreq>';
666
                $lines[] = $metaBundle->metaSitemapVars->sitemapChangeFreq;
667
                $lines[] = '</changefreq>';
668
                $lines[] = '<priority>';
669
                $lines[] = $metaBundle->metaSitemapVars->sitemapPriority;
670
                $lines[] = '</priority>';
671
                $lines[] = '</url>';
672
            }
673
        }
674
    }
675
}
676