Test Failed
Push — v5 ( 55df5c...cddd2a )
by Andrew
31:11 queued 16:26
created

DynamicMeta::handleHomepage()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 5
c 2
b 0
f 0
dl 0
loc 8
rs 9.6111
cc 5
nc 3
nop 0
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
                            $headerValue .= ('<' . $hrefVal . '>' . ',');
194
                        }
195
                        $headerValue = rtrim($headerValue, ',');
196
                    } else {
197
                        $headerValue = '<' . $href . '>';
198
                    }
199
                    $headerValue .= "; rel='canonical'";
200
                    $response->headers->add('Link', $headerValue);
201
                }
202
            }
203
            // Referrer-Policy header
204
            $referrer = Seomatic::$seomaticVariable->tag->get('referrer');
205
            if ($referrer !== null && $referrer->include) {
206
                $referrerArray = $referrer->renderAttributes();
207
                $content = $referrerArray['content'] ?? '';
208
                if (!empty($content)) {
209
                    // The content property can be a string or an array
210
                    if (is_array($content)) {
211
                        $headerValue = '';
212
                        foreach ($content as $contentVal) {
213
                            $headerValue .= ($contentVal . ',');
214
                        }
215
                        $headerValue = rtrim($headerValue, ',');
216
                    } else {
217
                        $headerValue = $content;
218
                    }
219
                    $response->headers->add('Referrer-Policy', $headerValue);
220
                }
221
            }
222
            // The X-Powered-By tag
223
            if (Seomatic::$settings->generatorEnabled) {
224
                $response->headers->add('X-Powered-By', 'SEOmatic');
225
            }
226
        }
227
    }
228
229
    /**
230
     * Add the Content-Security-Policy script-src headers
231
     */
232
    public static function addCspHeaders()
233
    {
234
        $cspNonces = self::getCspNonces();
235
        $container = Seomatic::$plugin->script->container();
236
        if ($container !== null) {
237
            $container->addNonceHeaders($cspNonces);
238
        }
239
    }
240
241
    /**
242
     * Get all of the CSP Nonces from containers that can have them
243
     *
244
     * @return array
245
     */
246
    public static function getCspNonces(): array
247
    {
248
        $cspNonces = [];
249
        // Add in any fixed policies from Settings
250
        if (!empty(Seomatic::$settings->cspScriptSrcPolicies)) {
251
            $fixedCsps = Seomatic::$settings->cspScriptSrcPolicies;
252
            $iterator = new RecursiveIteratorIterator(new RecursiveArrayIterator($fixedCsps));
253
            foreach ($iterator as $value) {
254
                $cspNonces[] = $value;
255
            }
256
        }
257
        // Add in any CSP nonce headers
258
        $container = Seomatic::$plugin->jsonLd->container();
259
        if ($container !== null) {
260
            $cspNonces = array_merge($cspNonces, $container->getCspNonces());
261
        }
262
        $container = Seomatic::$plugin->script->container();
263
        if ($container !== null) {
264
            $cspNonces = array_merge($cspNonces, $container->getCspNonces());
265
        }
266
267
        return $cspNonces;
268
    }
269
270
    /**
271
     * Add the Content-Security-Policy script-src tags
272
     */
273
    public static function addCspTags()
274
    {
275
        $cspNonces = self::getCspNonces();
276
        $container = Seomatic::$plugin->script->container();
277
        if ($container !== null) {
278
            $container->addNonceTags($cspNonces);
279
        }
280
    }
281
282
    /**
283
     * Add any custom/dynamic meta to the containers
284
     *
285
     * @param string|null $uri The URI of the route to add dynamic metadata for
286
     * @param int|null $siteId The siteId of the current site
287
     */
288
    public static function addDynamicMetaToContainers(string $uri = null, int $siteId = null)
289
    {
290
        Craft::beginProfile('DynamicMeta::addDynamicMetaToContainers', __METHOD__);
291
        $request = Craft::$app->getRequest();
292
        // Don't add dynamic meta to console requests, they have no concept of a URI or segments
293
        if (!$request->getIsConsoleRequest()) {
294
            $response = Craft::$app->getResponse();
295
            if ($response->statusCode < 400) {
296
                self::handleHomepage();
297
                self::addMetaJsonLdBreadCrumbs($siteId);
298
                if (Seomatic::$settings->addHrefLang) {
299
                    self::addMetaLinkHrefLang($uri, $siteId);
300
                }
301
                self::addSameAsMeta();
302
                $metaSiteVars = Seomatic::$plugin->metaContainers->metaSiteVars;
303
                $jsonLd = Seomatic::$plugin->jsonLd->get('identity');
304
                if ($jsonLd !== null) {
305
                    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

305
                    self::addOpeningHours($jsonLd, /** @scrutinizer ignore-type */ $metaSiteVars->identity);
Loading history...
306
                    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

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

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