DynamicMeta::addCspHeaders()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 6
ccs 0
cts 2
cp 0
rs 10
cc 2
nc 2
nop 0
crap 6
1
<?php
2
/**
3
 * SEOmatic plugin for Craft CMS
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\helpers;
13
14
use Craft;
15
use craft\base\Element;
16
use craft\errors\SiteNotFoundException;
17
use craft\helpers\DateTimeHelper;
18
use craft\web\twig\variables\Paginate;
19
use DateTime;
20
use nystudio107\seomatic\events\AddDynamicMetaEvent;
21
use nystudio107\seomatic\fields\SeoSettings;
22
use nystudio107\seomatic\helpers\Field as FieldHelper;
23
use nystudio107\seomatic\helpers\Localization as LocalizationHelper;
24
use nystudio107\seomatic\helpers\Text as TextHelper;
25
use nystudio107\seomatic\models\Entity;
26
use nystudio107\seomatic\models\jsonld\BreadcrumbList;
27
use nystudio107\seomatic\models\jsonld\ContactPoint;
28
use nystudio107\seomatic\models\jsonld\LocalBusiness;
29
use nystudio107\seomatic\models\jsonld\OpeningHoursSpecification;
30
use nystudio107\seomatic\models\jsonld\Organization;
31
use nystudio107\seomatic\models\jsonld\Thing;
32
use nystudio107\seomatic\models\jsonld\WebPage;
33
use nystudio107\seomatic\models\jsonld\WebSite;
34
use nystudio107\seomatic\models\MetaBundle;
35
use nystudio107\seomatic\models\MetaJsonLd;
36
use nystudio107\seomatic\models\MetaLink;
37
use nystudio107\seomatic\Seomatic;
38
use nystudio107\seomatic\services\Helper as SeomaticHelper;
39
use RecursiveArrayIterator;
40
use RecursiveIteratorIterator;
41
use Throwable;
42
use yii\base\Event;
43
use yii\base\Exception;
44
use yii\base\InvalidConfigException;
45
use function count;
46
use function in_array;
47
use function is_array;
48
use function is_string;
49
50
/**
51
 * @author    nystudio107
52
 * @package   Seomatic
53
 * @since     3.0.0
54
 */
55
class DynamicMeta
56
{
57
    // Constants
58
    // =========================================================================
59
60
    /**
61
     * @event AddDynamicMetaEvent The event that is triggered when SEOmatic has
62
     *        included the standard meta containers, and gives your plugin/module
63
     *        the chance to add whatever custom dynamic meta items you like
64
     *
65
     * ---
66
     * ```php
67
     * use nystudio107\seomatic\events\AddDynamicMetaEvent;
68
     * use nystudio107\seomatic\helpers\DynamicMeta;
69
     * use yii\base\Event;
70
     * Event::on(DynamicMeta::class, DynamicMeta::EVENT_ADD_DYNAMIC_META, function(AddDynamicMetaEvent $e) {
71
     *     // Add whatever dynamic meta items to the containers as you like
72
     * });
73
     * ```
74
     */
75
    public const EVENT_ADD_DYNAMIC_META = 'addDynamicMeta';
76
77
    // Static Methods
78
    // =========================================================================
79
80
    /**
81
     * Paginate based on the passed in Paginate variable as returned from the
82
     * Twig {% paginate %} tag:
83
     * https://docs.craftcms.com/v3/templating/tags/paginate.html#the-pageInfo-variable
84
     *
85
     * @param ?Paginate $pageInfo
86
     */
87
    public static function paginate(?Paginate $pageInfo)
88
    {
89
        if ($pageInfo !== null && $pageInfo->currentPage !== null) {
90
            // Let the meta containers know that this page is paginated
91
            Seomatic::$plugin->metaContainers->paginationPage = (string)$pageInfo->currentPage;
92
            // See if we should strip the query params
93
            $stripQueryParams = true;
94
            $pageTrigger = Craft::$app->getConfig()->getGeneral()->pageTrigger;
95
            // Is this query string-based pagination?
96
            if ($pageTrigger[0] === '?') {
97
                $stripQueryParams = false;
98
            }
99
            // Set the canonical URL to be the paginated URL
100
            // see: https://github.com/nystudio107/craft-seomatic/issues/375#issuecomment-488369209
101
            $url = $pageInfo->getPageUrl($pageInfo->currentPage);
102
            if ($stripQueryParams) {
103
                $url = preg_replace('/\?.*/', '', $url);
104
            }
105
            if (!empty($url)) {
106
                $url = UrlHelper::absoluteUrlWithProtocol($url);
107
                Seomatic::$seomaticVariable->meta->canonicalUrl = $url;
108
                $canonical = Seomatic::$seomaticVariable->link->get('canonical');
109
                if ($canonical !== null) {
110
                    $canonical->href = $url;
111
                }
112
            }
113
            // Set the previous URL
114
            $url = $pageInfo->getPrevUrl();
115
            if ($stripQueryParams) {
116
                $url = preg_replace('/\?.*/', '', $url);
117
            }
118
            if (!empty($url)) {
119
                $url = UrlHelper::absoluteUrlWithProtocol($url);
120
                $metaTag = Seomatic::$plugin->link->create([
0 ignored issues
show
Unused Code introduced by
The assignment to $metaTag is dead and can be removed.
Loading history...
121
                    'rel' => 'prev',
122
                    'href' => $url,
123
                ]);
124
            }
125
            // Set the next URL
126
            $url = $pageInfo->getNextUrl();
127
            if ($stripQueryParams) {
128
                $url = preg_replace('/\?.*/', '', $url);
129
            }
130
            if (!empty($url)) {
131
                $url = UrlHelper::absoluteUrlWithProtocol($url);
132
                $metaTag = Seomatic::$plugin->link->create([
133
                    'rel' => 'next',
134
                    'href' => $url,
135
                ]);
136
            }
137
            // If this page is paginated, we need to factor that into the cache key
138
            // We also need to re-add the hreflangs
139
            if (Seomatic::$plugin->metaContainers->paginationPage !== '1') {
140
                if (Seomatic::$settings->addHrefLang && Seomatic::$settings->addPaginatedHreflang) {
141
                    self::addMetaLinkHrefLang();
142
                }
143
            }
144
        }
145
    }
146
147
    /**
148
     * Include any headers for this request
149
     */
150
    public static function includeHttpHeaders()
151
    {
152
        self::addCspHeaders();
153
        // Don't include headers for any response code >= 400
154
        $request = Craft::$app->getRequest();
155
        if (!$request->isConsoleRequest) {
156
            $response = Craft::$app->getResponse();
157
            if ($response->statusCode >= 400 || SeomaticHelper::isPreview()) {
158
                return;
159
            }
160
        }
161
        // Assuming they have headersEnabled, add the response code to the headers
162
        if (Seomatic::$settings->headersEnabled) {
163
            $response = Craft::$app->getResponse();
164
            // X-Robots-Tag header
165
            $robots = Seomatic::$seomaticVariable->tag->get('robots');
166
            if ($robots !== null && $robots->include) {
167
                $robotsArray = $robots->renderAttributes();
168
                $content = $robotsArray['content'] ?? '';
169
                if (!empty($content)) {
170
                    // The content property can be a string or an array
171
                    if (is_array($content)) {
172
                        $headerValue = '';
173
                        foreach ($content as $contentVal) {
174
                            $headerValue .= ($contentVal . ',');
175
                        }
176
                        $headerValue = rtrim($headerValue, ',');
177
                    } else {
178
                        $headerValue = $content;
179
                    }
180
                    $response->headers->set('X-Robots-Tag', $headerValue);
181
                }
182
            }
183
            // Link canonical header
184
            $canonical = Seomatic::$seomaticVariable->link->get('canonical');
185
            if ($canonical !== null && $canonical->include) {
186
                $canonicalArray = $canonical->renderAttributes();
187
                $href = $canonicalArray['href'] ?? '';
188
                if (!empty($href)) {
189
                    // The href property can be a string or an array
190
                    if (is_array($href)) {
191
                        $headerValue = '';
192
                        foreach ($href as $hrefVal) {
193
                            $hrefVal = UrlHelper::encodeUrl($hrefVal);
194
                            $headerValue .= ('<' . $hrefVal . '>' . ',');
195
                        }
196
                        $headerValue = rtrim($headerValue, ',');
197
                    } else {
198
                        $href = UrlHelper::encodeUrl($href);
199
                        $headerValue = '<' . $href . '>';
200
                    }
201
                    $headerValue .= "; rel='canonical'";
202
                    $response->headers->add('Link', $headerValue);
203
                }
204
            }
205
            // Referrer-Policy header
206
            $referrer = Seomatic::$seomaticVariable->tag->get('referrer');
207
            if ($referrer !== null && $referrer->include) {
208
                $referrerArray = $referrer->renderAttributes();
209
                $content = $referrerArray['content'] ?? '';
210
                if (!empty($content)) {
211
                    // The content property can be a string or an array
212
                    if (is_array($content)) {
213
                        $headerValue = '';
214
                        foreach ($content as $contentVal) {
215
                            $headerValue .= ($contentVal . ',');
216
                        }
217
                        $headerValue = rtrim($headerValue, ',');
218
                    } else {
219
                        $headerValue = $content;
220
                    }
221
                    $response->headers->add('Referrer-Policy', $headerValue);
222
                }
223
            }
224
            // The X-Powered-By tag
225
            if (Seomatic::$settings->generatorEnabled) {
226
                $response->headers->add('X-Powered-By', 'SEOmatic');
227
            }
228
        }
229
    }
230
231
    /**
232
     * Add the Content-Security-Policy script-src headers
233
     */
234
    public static function addCspHeaders()
235
    {
236
        $cspNonces = self::getCspNonces();
237
        $container = Seomatic::$plugin->script->container();
238
        if ($container !== null) {
239
            $container->addNonceHeaders($cspNonces);
240
        }
241
    }
242
243
    /**
244
     * Get all of the CSP Nonces from containers that can have them
245
     *
246
     * @return array
247
     */
248
    public static function getCspNonces(): array
249
    {
250
        $cspNonces = [];
251
        // Add in any fixed policies from Settings
252
        if (!empty(Seomatic::$settings->cspScriptSrcPolicies)) {
253
            $fixedCsps = Seomatic::$settings->cspScriptSrcPolicies;
254
            $iterator = new RecursiveIteratorIterator(new RecursiveArrayIterator($fixedCsps));
255
            foreach ($iterator as $value) {
256
                $cspNonces[] = $value;
257
            }
258
        }
259
        // Add in any CSP nonce headers
260
        $container = Seomatic::$plugin->jsonLd->container();
261
        if ($container !== null) {
262
            $cspNonces = array_merge($cspNonces, $container->getCspNonces());
263
        }
264
        $container = Seomatic::$plugin->script->container();
265
        if ($container !== null) {
266
            $cspNonces = array_merge($cspNonces, $container->getCspNonces());
267
        }
268
269
        return $cspNonces;
270
    }
271
272
    /**
273
     * Add the Content-Security-Policy script-src tags
274
     */
275
    public static function addCspTags()
276
    {
277
        $cspNonces = self::getCspNonces();
278
        $container = Seomatic::$plugin->script->container();
279
        if ($container !== null) {
280
            $container->addNonceTags($cspNonces);
281
        }
282
    }
283
284
    /**
285
     * Add any custom/dynamic meta to the containers
286
     *
287
     * @param string|null $uri The URI of the route to add dynamic metadata for
288
     * @param int|null $siteId The siteId of the current site
289
     */
290
    public static function addDynamicMetaToContainers(string $uri = null, int $siteId = null)
291
    {
292
        Craft::beginProfile('DynamicMeta::addDynamicMetaToContainers', __METHOD__);
293
        $request = Craft::$app->getRequest();
294
        // Don't add dynamic meta to console requests, they have no concept of a URI or segments
295
        if (!$request->getIsConsoleRequest()) {
296
            $response = Craft::$app->getResponse();
297
            if ($response->statusCode < 400) {
298
                self::handleHomepage();
299
                self::addMetaJsonLdBreadCrumbs($siteId);
300
                if (Seomatic::$settings->addHrefLang) {
301
                    self::addMetaLinkHrefLang($uri, $siteId);
302
                }
303
                self::addSameAsMeta();
304
                $metaSiteVars = Seomatic::$plugin->metaContainers->metaSiteVars;
305
                $jsonLd = Seomatic::$plugin->jsonLd->get('identity');
306
                if ($jsonLd !== null) {
307
                    self::addOpeningHours($jsonLd, $metaSiteVars->identity);
0 ignored issues
show
Bug introduced by
It seems like $metaSiteVars->identity can also be of type array; however, parameter $entity of nystudio107\seomatic\hel...Meta::addOpeningHours() does only seem to accept null|nystudio107\seomatic\models\Entity, 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

307
                    self::addOpeningHours($jsonLd, /** @scrutinizer ignore-type */ $metaSiteVars->identity);
Loading history...
308
                    self::addContactPoints($jsonLd, $metaSiteVars->identity);
0 ignored issues
show
Bug introduced by
It seems like $metaSiteVars->identity can also be of type array; however, parameter $entity of nystudio107\seomatic\hel...eta::addContactPoints() does only seem to accept null|nystudio107\seomatic\models\Entity, 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

308
                    self::addContactPoints($jsonLd, /** @scrutinizer ignore-type */ $metaSiteVars->identity);
Loading history...
309
                }
310
                $jsonLd = Seomatic::$plugin->jsonLd->get('creator');
311
                if ($jsonLd !== null) {
312
                    self::addOpeningHours($jsonLd, $metaSiteVars->creator);
313
                    self::addContactPoints($jsonLd, $metaSiteVars->creator);
314
                }
315
                // Allow modules/plugins a chance to add dynamic meta
316
                $event = new AddDynamicMetaEvent([
317
                    'uri' => $uri,
318
                    'siteId' => $siteId,
319
                ]);
320
                Event::trigger(static::class, self::EVENT_ADD_DYNAMIC_META, $event);
321
            }
322
        }
323
        Craft::endProfile('DynamicMeta::addDynamicMetaToContainers', __METHOD__);
324
    }
325
326
    /**
327
     * If this is the homepage, and the MainEntityOfPage is WebPage or a WebSite, set the name
328
     * and alternateName so it shows up in SERP as per:
329
     * https://developers.google.com/search/docs/appearance/site-names
330
     *
331
     * @return void
332
     */
333
    public static function handleHomepage(): void
334
    {
335
        if (Seomatic::$matchedElement && Seomatic::$matchedElement->uri === '__home__') {
336
            $mainEntity = Seomatic::$plugin->jsonLd->get('mainEntityOfPage');
337
            if ($mainEntity instanceof WebPage || $mainEntity instanceof WebSite) {
338
                /** WebPage $mainEntity */
339
                $mainEntity->name = "{{ seomatic.site.siteName }}";
340
                $mainEntity->alternateName = "{{ seomatic.site.siteAlternateName }}";
341
            }
342
        }
343
    }
344
345
    /**
346
     * Add breadcrumbs to the MetaJsonLdContainer
347
     *
348
     * @param int|null $siteId
349
     */
350
    public static function addMetaJsonLdBreadCrumbs(int $siteId = null)
351
    {
352
        Craft::beginProfile('DynamicMeta::addMetaJsonLdBreadCrumbs', __METHOD__);
353
        $position = 0;
354
        if ($siteId === null) {
355
            $siteId = Craft::$app->getSites()->currentSite->id
356
                ?? Craft::$app->getSites()->primarySite->id
357
                ?? 1;
358
        }
359
        $site = Craft::$app->getSites()->getSiteById($siteId);
360
        if ($site === null) {
361
            return;
362
        }
363
        $siteUrl = '/';
0 ignored issues
show
Unused Code introduced by
The assignment to $siteUrl is dead and can be removed.
Loading history...
364
        try {
365
            $siteUrl = SiteHelper::siteEnabledWithUrls($siteId) ? $site->baseUrl : Craft::$app->getSites()->getPrimarySite()->baseUrl;
366
        } catch (SiteNotFoundException $e) {
367
            Craft::error($e->getMessage(), __METHOD__);
368
        }
369
        if (!empty(Seomatic::$settings->siteUrlOverride)) {
370
            try {
371
                $siteUrl = UrlHelper::getSiteUrlOverrideSetting($siteId);
372
            } catch (Throwable $e) {
373
                // That's okay
374
            }
375
        }
376
        /** @var BreadcrumbList $crumbs */
377
        $crumbs = Seomatic::$plugin->jsonLd->create([
378
            'type' => 'BreadcrumbList',
379
            'name' => 'Breadcrumbs',
380
            'description' => 'Breadcrumbs list',
381
        ], false);
382
        // Include the Homepage in the breadcrumbs, if includeHomepageInBreadcrumbs is true
383
        $element = null;
384
        if (Seomatic::$settings->includeHomepageInBreadcrumbs) {
385
            /** @var Element $element */
386
            $position++;
387
            $element = Craft::$app->getElements()->getElementByUri('__home__', $siteId, true);
388
            if ($element) {
389
                $uri = $element->uri === '__home__' ? '' : ($element->uri ?? '');
390
                try {
391
                    $id = UrlHelper::siteUrl($uri, null, null, $siteId);
392
                } catch (Exception $e) {
393
                    $id = $siteUrl;
394
                    Craft::error($e->getMessage(), __METHOD__);
395
                }
396
                $item = UrlHelper::stripQueryString($id);
397
                $item = UrlHelper::absoluteUrlWithProtocol($item);
398
                $listItem = MetaJsonLd::create('ListItem', [
399
                    'position' => $position,
400
                    'name' => $element->title,
401
                    'item' => $item,
402
                    '@id' => $id,
403
                ]);
404
                $crumbs->itemListElement[] = $listItem;
405
            } else {
406
                $item = UrlHelper::stripQueryString($siteUrl ?? '/');
407
                $item = UrlHelper::absoluteUrlWithProtocol($item);
408
                $crumbs->itemListElement[] = MetaJsonLd::create('ListItem', [
409
                    'position' => $position,
410
                    'name' => 'Homepage',
411
                    'item' => $item,
412
                    '@id' => $siteUrl,
413
                ]);
414
            }
415
        }
416
        // Build up the segments, and look for elements that match
417
        $uri = '';
418
        $segments = Craft::$app->getRequest()->getSegments();
419
        /** @var Element|null $lastElement */
420
        $lastElement = Seomatic::$matchedElement;
421
        if ($lastElement && $element) {
422
            if ($lastElement->uri !== '__home__' && $element->uri) {
423
                $path = $lastElement->uri;
424
                $segments = array_values(array_filter(explode('/', $path), function($segment) {
425
                    return $segment !== '';
426
                }));
427
            }
428
        }
429
        // Parse through the segments looking for elements that match
430
        foreach ($segments as $segment) {
431
            $uri .= $segment;
432
            /** @var Element|null $element */
433
            $element = Craft::$app->getElements()->getElementByUri($uri, $siteId, true);
434
            if ($element && $element->uri) {
435
                $position++;
436
                $uri = $element->uri === '__home__' ? '' : $element->uri;
437
                try {
438
                    $id = UrlHelper::siteUrl($uri, null, null, $siteId);
439
                } catch (Exception $e) {
440
                    $id = $siteUrl;
441
                    Craft::error($e->getMessage(), __METHOD__);
442
                }
443
                $item = UrlHelper::stripQueryString($id);
444
                $item = UrlHelper::absoluteUrlWithProtocol($item);
445
                $crumbs->itemListElement[] = MetaJsonLd::create('ListItem', [
446
                    'position' => $position,
447
                    'name' => $element->title,
448
                    'item' => $item,
449
                    '@id' => $id,
450
                ]);
451
            }
452
            $uri .= '/';
453
        }
454
        if (!empty($crumbs->itemListElement)) {
455
            Seomatic::$plugin->jsonLd->add($crumbs);
456
        }
457
        Craft::endProfile('DynamicMeta::addMetaJsonLdBreadCrumbs', __METHOD__);
458
    }
459
460
    /**
461
     * Add meta hreflang tags if there is more than one site
462
     *
463
     * @param string|null $uri
464
     * @param int|null $siteId
465
     */
466
    public static function addMetaLinkHrefLang(string $uri = null, int $siteId = null)
467
    {
468
        Craft::beginProfile('DynamicMeta::addMetaLinkHrefLang', __METHOD__);
469
        $siteLocalizedUrls = self::getLocalizedUrls($uri, $siteId);
470
        $currentPaginationUrl = null;
471
        if (Seomatic::$plugin->metaContainers->paginationPage !== '1') {
472
            $currentPaginationUrl = Seomatic::$seomaticVariable->meta->canonicalUrl ?? null;
473
        }
474
        if (!empty($siteLocalizedUrls)) {
475
            // Add the rel=alternate tag
476
            /** @var MetaLink $metaTag */
477
            $metaTag = Seomatic::$plugin->link->create([
478
                'rel' => 'alternate',
479
                'hreflang' => [],
480
                'href' => [],
481
            ]);
482
            // Add the alternate language link rel's
483
            if (count($siteLocalizedUrls) > 1) {
484
                foreach ($siteLocalizedUrls as $siteLocalizedUrl) {
485
                    $url = $siteLocalizedUrl['url'];
486
                    if ($siteLocalizedUrl['current']) {
487
                        $url = $currentPaginationUrl ?? $siteLocalizedUrl['url'];
488
                    }
489
                    $metaTag->hreflang[] = $siteLocalizedUrl['hreflangLanguage'];
490
                    $metaTag->href[] = $url;
491
                    // Add the x-default hreflang
492
                    if ($siteLocalizedUrl['primary'] && Seomatic::$settings->addXDefaultHrefLang) {
493
                        $metaTag->hreflang[] = 'x-default';
494
                        $metaTag->href[] = $siteLocalizedUrl['url'];
495
                    }
496
                }
497
                Seomatic::$plugin->link->add($metaTag);
498
            }
499
            // Add in the og:locale:alternate tags
500
            $ogLocaleAlternate = Seomatic::$plugin->tag->get('og:locale:alternate');
501
            if (count($siteLocalizedUrls) > 1 && $ogLocaleAlternate) {
502
                $ogContentArray = [];
503
                foreach ($siteLocalizedUrls as $siteLocalizedUrl) {
504
                    if (!in_array($siteLocalizedUrl['ogLanguage'], $ogContentArray, true) &&
505
                        Craft::$app->language !== $siteLocalizedUrl['language']) {
506
                        $ogContentArray[] = $siteLocalizedUrl['ogLanguage'];
507
                    }
508
                }
509
                $ogLocaleAlternate->content = $ogContentArray;
510
            }
511
        }
512
        Craft::endProfile('DynamicMeta::addMetaLinkHrefLang', __METHOD__);
513
    }
514
515
    /**
516
     * Return a list of localized URLs that are in the current site's group
517
     * The current URI is used if $uri is null. Similarly, the current site is
518
     * used if $siteId is null.
519
     * The resulting array of arrays has `id`, `language`, `ogLanguage`,
520
     * `hreflangLanguage`, and `url` as keys.
521
     *
522
     * @param string|null $uri
523
     * @param int|null $siteId
524
     *
525
     * @return array
526
     */
527
    public static function getLocalizedUrls(string $uri = null, int $siteId = null): array
528
    {
529
        Craft::beginProfile('DynamicMeta::getLocalizedUrls', __METHOD__);
530
        $localizedUrls = [];
531
        // No pagination params for URLs
532
        $urlParams = null;
533
        // Get the request URI
534
        if ($uri === null) {
535
            $requestUri = Craft::$app->getRequest()->pathInfo;
536
        } else {
537
            $requestUri = $uri;
538
        }
539
        // Get the site to use
540
        if ($siteId === null) {
541
            try {
542
                $thisSite = Craft::$app->getSites()->getCurrentSite();
543
            } catch (SiteNotFoundException $e) {
544
                $thisSite = null;
545
                Craft::error($e->getMessage(), __METHOD__);
546
            }
547
        } else {
548
            $thisSite = Craft::$app->getSites()->getSiteById($siteId);
549
        }
550
        // Bail if we can't get a site
551
        if ($thisSite === null) {
552
            return $localizedUrls;
553
        }
554
        if (Seomatic::$settings->siteGroupsSeparate) {
555
            // Get only the sites that are in the current site's group
556
            try {
557
                $siteGroup = $thisSite->getGroup();
558
            } catch (InvalidConfigException $e) {
559
                $siteGroup = null;
560
                Craft::error($e->getMessage(), __METHOD__);
561
            }
562
            // Bail if we can't get a site group
563
            if ($siteGroup === null) {
564
                return $localizedUrls;
565
            }
566
            $sites = $siteGroup->getSites();
567
        } else {
568
            $sites = Craft::$app->getSites()->getAllSites();
569
        }
570
        $elements = Craft::$app->getElements();
571
        foreach ($sites as $site) {
572
            $includeUrl = true;
573
            $matchedElement = $elements->getElementByUri($requestUri, $thisSite->id, true);
574
            if ($matchedElement) {
575
                $url = $elements->getElementUriForSite($matchedElement->getId(), $site->id);
576
                // See if they have disabled sitemaps or robots for this entry,
577
                // and if so, don't include it in the hreflang
578
                $element = null;
579
                if ($url) {
580
                    /** @var Element $element */
581
                    $element = $elements->getElementByUri($url, $site->id, false);
582
                }
583
                if ($element !== null) {
584
                    /** @var MetaBundle $metaBundle */
585
                    list($sourceId, $sourceBundleType, $sourceHandle, $sourceSiteId, $typeId)
586
                        = Seomatic::$plugin->metaBundles->getMetaSourceFromElement($element);
587
                    $metaBundle = Seomatic::$plugin->metaBundles->getMetaBundleBySourceId(
588
                        $sourceBundleType,
589
                        $sourceId,
590
                        $sourceSiteId
591
                    );
592
                    if ($metaBundle !== null) {
593
                        // If robots contains 'none' or 'noindex' don't include the URL
594
                        $robotsArray = explode(',', $metaBundle->metaGlobalVars->robots);
595
                        if (in_array('noindex', $robotsArray, true) || in_array('none', $robotsArray, true)) {
596
                            $includeUrl = false;
597
                        }
598
                    }
599
                    $fieldHandles = FieldHelper::fieldsOfTypeFromElement(
600
                        $element,
601
                        FieldHelper::SEO_SETTINGS_CLASS_KEY,
602
                        true
603
                    );
604
                    foreach ($fieldHandles as $fieldHandle) {
605
                        if (!empty($element->$fieldHandle)) {
606
                            /** @var MetaBundle $fieldMetaBundle */
607
                            $fieldMetaBundle = $element->$fieldHandle;
608
                            /** @var SeoSettings $seoSettingsField */
609
                            $seoSettingsField = Craft::$app->getFields()->getFieldByHandle($fieldHandle);
610
                            if ($seoSettingsField !== null) {
611
                                // If robots is set to 'none' don't include the URL
612
                                if ($seoSettingsField->generalTabEnabled
613
                                    && in_array('robots', $seoSettingsField->generalEnabledFields, false)
614
                                    && !Seomatic::$plugin->helper->isInherited($fieldMetaBundle->metaGlobalVars, 'robots')
0 ignored issues
show
Bug introduced by
It seems like $fieldMetaBundle->metaGlobalVars can also be of type array and null; however, parameter $settingCollection of nystudio107\seomatic\ser...s\Helper::isInherited() does only seem to accept nystudio107\seomatic\base\InheritableSettingsModel, 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

614
                                    && !Seomatic::$plugin->helper->isInherited(/** @scrutinizer ignore-type */ $fieldMetaBundle->metaGlobalVars, 'robots')
Loading history...
615
                                ) {
616
                                    // If robots contains 'none' or 'noindex' don't include the URL
617
                                    $robotsArray = explode(',', $fieldMetaBundle->metaGlobalVars->robots);
618
                                    if (in_array('noindex', $robotsArray, true) || in_array('none', $robotsArray, true)) {
619
                                        $includeUrl = false;
620
                                    } else {
621
                                        // Otherwise, include the URL
622
                                        $includeUrl = true;
623
                                    }
624
                                }
625
                            }
626
                        }
627
                    }
628
                    // Never include the URL if the element isn't enabled for the site
629
                    if (isset($element->enabledForSite) && !(bool)$element->enabledForSite) {
630
                        $includeUrl = false;
631
                    }
632
                } else {
633
                    $includeUrl = false;
634
                }
635
                $url = ($url === '__home__') ? '' : $url;
636
            } else {
637
                try {
638
                    $url = SiteHelper::siteEnabledWithUrls($site->id) ? UrlHelper::siteUrl($requestUri, $urlParams, null, $site->id)
639
                        : Craft::$app->getSites()->getPrimarySite()->baseUrl;
640
                } catch (SiteNotFoundException $e) {
641
                    $url = '';
642
                    Craft::error($e->getMessage(), __METHOD__);
643
                } catch (Exception $e) {
644
                    $url = '';
645
                    Craft::error($e->getMessage(), __METHOD__);
646
                }
647
            }
648
            $url = $url ?? '';
649
            if (!UrlHelper::isAbsoluteUrl($url)) {
650
                try {
651
                    $url = UrlHelper::siteUrl($url, $urlParams, null, $site->id);
652
                } catch (Exception $e) {
653
                    $url = '';
654
                    Craft::error($e->getMessage(), __METHOD__);
655
                }
656
            }
657
            // Strip any query string params, and make sure we have an absolute URL with protocol
658
            if ($urlParams === null) {
659
                $url = UrlHelper::stripQueryString($url);
660
            }
661
            $url = UrlHelper::absoluteUrlWithProtocol($url);
662
663
            $url = self::sanitizeUrl($url);
664
            $language = $site->language;
665
            $ogLanguage = LocalizationHelper::normalizeOgLocaleLanguage($language);
666
            $hreflangLanguage = $language;
667
            $hreflangLanguage = strtolower($hreflangLanguage);
668
            $hreflangLanguage = str_replace('_', '-', $hreflangLanguage);
669
            $primary = Seomatic::$settings->xDefaultSite == 0 ? $site->primary : Seomatic::$settings->xDefaultSite == $site->id;
670
            if ($includeUrl) {
671 2
                $localizedUrls[] = [
672
                    'id' => $site->id,
673
                    'language' => $language,
674 2
                    'ogLanguage' => $ogLanguage,
675 2
                    'hreflangLanguage' => $hreflangLanguage,
676
                    'url' => $url,
677 2
                    'primary' => $primary,
678
                    'current' => $thisSite->id === $site->id,
679
                ];
680 2
            }
681
        }
682
        Craft::endProfile('DynamicMeta::getLocalizedUrls', __METHOD__);
683
684 2
        return $localizedUrls;
685
    }
686
687
    /**
688
     * Return a sanitized URL with the query string stripped
689
     *
690
     * @param string $url
691
     * @param bool $checkStatus
692
     *
693
     * @return string
694
     */
695
    public static function sanitizeUrl(string $url, bool $checkStatus = true, bool $stripQueryString = true): string
696
    {
697
        // Remove the query string
698
        if ($stripQueryString) {
699
            $url = UrlHelper::stripQueryString($url);
700
        }
701
        $url = UrlHelper::encodeUrlQueryParams(TextHelper::sanitizeUserInput($url));
702
703
        // If this is a >= 400 status code, set the canonical URL to nothing
704
        if ($checkStatus && !Craft::$app->getRequest()->getIsConsoleRequest() && Craft::$app->getResponse()->statusCode >= 400) {
705
            $url = '';
706
        }
707
708
        return $url;
709
    }
710
711
    /**
712
     * Add the Same As meta tags and JSON-LD
713
     */
714
    public static function addSameAsMeta()
715
    {
716
        Craft::beginProfile('DynamicMeta::addSameAsMeta', __METHOD__);
717
        $metaContainers = Seomatic::$plugin->metaContainers;
718
        $sameAsUrls = [];
719
        if (!empty($metaContainers->metaSiteVars->sameAsLinks)) {
720
            $sameAsUrls = ArrayHelper::getColumn($metaContainers->metaSiteVars->sameAsLinks, 'url', false);
721
            $sameAsUrls = array_values(array_filter($sameAsUrls));
722
        }
723
        // Facebook OpenGraph
724
        $ogSeeAlso = Seomatic::$plugin->tag->get('og:see_also');
725
        if ($ogSeeAlso) {
0 ignored issues
show
introduced by
$ogSeeAlso is of type nystudio107\seomatic\models\MetaTag, thus it always evaluated to true.
Loading history...
726
            $ogSeeAlso->content = $sameAsUrls;
727
        }
728
        // Site Identity JSON-LD
729
        $identity = Seomatic::$plugin->jsonLd->get('identity');
730
        /** @var Thing|null $identity */
731
        if ($identity !== null && property_exists($identity, 'sameAs')) {
732
            $identity->sameAs = $sameAsUrls;
733
        }
734
        Craft::endProfile('DynamicMeta::addSameAsMeta', __METHOD__);
735
    }
736
737
    /**
738
     * Add the OpeningHoursSpecific to the $jsonLd based on the Entity settings
739
     *
740
     * @param MetaJsonLd $jsonLd
741
     * @param ?Entity $entity
742
     */
743
    public static function addOpeningHours(MetaJsonLd $jsonLd, ?Entity $entity)
744
    {
745
        Craft::beginProfile('DynamicMeta::addOpeningHours', __METHOD__);
746
        if ($jsonLd instanceof LocalBusiness && $entity !== null) {
747
            /** @var LocalBusiness $jsonLd */
748
            $openingHours = [];
749
            $days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
750
            $times = $entity->localBusinessOpeningHours;
751
            $index = 0;
752
            foreach ($times as $hours) {
753
                $openTime = '';
754
                $closeTime = '';
755
                if (!empty($hours['open'])) {
756
                    /** @var DateTime $dateTime */
757
                    try {
758
                        $dateTime = DateTimeHelper::toDateTime($hours['open']['date'], false, false);
759
                    } catch (\Exception $e) {
760
                        $dateTime = false;
761
                    }
762
                    if ($dateTime !== false) {
763
                        $openTime = $dateTime->format('H:i:s');
764
                    }
765
                }
766
                if (!empty($hours['close'])) {
767
                    /** @var DateTime $dateTime */
768
                    try {
769
                        $dateTime = DateTimeHelper::toDateTime($hours['close']['date'], false, false);
770
                    } catch (\Exception $e) {
771
                        $dateTime = false;
772
                    }
773
                    if ($dateTime !== false) {
774
                        $closeTime = $dateTime->format('H:i:s');
775
                    }
776
                }
777
                if ($openTime && $closeTime) {
778
                    /** @var OpeningHoursSpecification $hours */
779
                    $hours = Seomatic::$plugin->jsonLd->create([
780
                        'type' => 'OpeningHoursSpecification',
781
                        'opens' => $openTime,
782
                        'closes' => $closeTime,
783
                        'dayOfWeek' => [$days[$index]],
784
                    ], false);
785
                    $openingHours[] = $hours;
786
                }
787
                $index++;
788
            }
789
            $jsonLd->openingHoursSpecification = $openingHours;
790
        }
791
        Craft::endProfile('DynamicMeta::addOpeningHours', __METHOD__);
792
    }
793
794
    /**
795
     * Add the ContactPoint to the $jsonLd based on the Entity settings
796
     *
797
     * @param MetaJsonLd $jsonLd
798
     * @param Entity|null $entity
799
     */
800
    public static function addContactPoints(MetaJsonLd $jsonLd, ?Entity $entity)
801
    {
802
        Craft::beginProfile('DynamicMeta::addContactPoints', __METHOD__);
803
        if ($jsonLd instanceof Organization && $entity !== null) {
804
            /** @var Organization $jsonLd */
805
            $contactPoints = [];
806
            if (is_array($entity->organizationContactPoints)) {
807
                foreach ($entity->organizationContactPoints as $contacts) {
808
                    /** @var ContactPoint $contact */
809
                    $contact = Seomatic::$plugin->jsonLd->create([
810
                        'type' => 'ContactPoint',
811
                        'telephone' => $contacts['telephone'],
812
                        'contactType' => $contacts['contactType'],
813
                    ], false);
814
                    $contactPoints[] = $contact;
815
                }
816
            }
817
            $jsonLd->contactPoint = $contactPoints;
818
        }
819
        Craft::endProfile('DynamicMeta::addContactPoints', __METHOD__);
820
    }
821
822
    /**
823
     * Normalize the array of opening hours passed in
824
     *
825
     * @param $value
826
     */
827
    public static function normalizeTimes(&$value)
828
    {
829
        if (is_string($value)) {
830
            $value = Json::decode($value);
831
        }
832
        $normalized = [];
833
        $times = ['open', 'close'];
834
        for ($day = 0; $day <= 6; $day++) {
835
            foreach ($times as $time) {
836
                if (isset($value[$day][$time])
837
                    && ($date = DateTimeHelper::toDateTime($value[$day][$time])) !== false
838
                ) {
839
                    $normalized[$day][$time] = (array)($date);
840
                } else {
841
                    $normalized[$day][$time] = null;
842
                }
843
            }
844
        }
845
846
        $value = $normalized;
847
    }
848
}
849