Passed
Push — v3 ( 2d07e9...8d152e )
by Andrew
16:09 queued 08:23
created

DynamicMeta::sanitizeUrl()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 1
dl 0
loc 14
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * SEOmatic plugin for Craft CMS 3.x
4
 *
5
 * A turnkey SEO implementation for Craft CMS that is comprehensive, powerful,
6
 * and flexible
7
 *
8
 * @link      https://nystudio107.com
9
 * @copyright Copyright (c) 2017 nystudio107
10
 */
11
12
namespace nystudio107\seomatic\helpers;
13
14
use nystudio107\seomatic\Seomatic;
15
use nystudio107\seomatic\events\AddDynamicMetaEvent;
16
use nystudio107\seomatic\fields\SeoSettings;
17
use nystudio107\seomatic\helpers\Field as FieldHelper;
18
use nystudio107\seomatic\models\Entity;
19
use nystudio107\seomatic\models\jsonld\ContactPoint;
20
use nystudio107\seomatic\models\jsonld\LocalBusiness;
21
use nystudio107\seomatic\models\jsonld\Organization;
22
use nystudio107\seomatic\models\jsonld\BreadcrumbList;
23
use nystudio107\seomatic\models\jsonld\Thing;
24
use nystudio107\seomatic\models\MetaBundle;
25
use nystudio107\seomatic\models\MetaJsonLd;
26
27
28
use Craft;
29
use craft\base\Element;
30
use craft\errors\SiteNotFoundException;
31
use craft\helpers\DateTimeHelper;
32
use craft\web\twig\variables\Paginate;
33
34
use yii\base\Event;
35
use yii\base\Exception;
36
use yii\base\InvalidConfigException;
37
38
/**
39
 * @author    nystudio107
0 ignored issues
show
Coding Style introduced by
The tag in position 1 should be the @package tag
Loading history...
Coding Style introduced by
Content of the @author tag must be in the form "Display Name <[email protected]>"
Loading history...
40
 * @package   Seomatic
41
 * @since     3.0.0
0 ignored issues
show
Coding Style introduced by
The tag in position 3 should be the @author tag
Loading history...
42
 */
43
class DynamicMeta
44
{
45
    // Constants
46
    // =========================================================================
47
48
    /**
49
     * @event AddDynamicMetaEvent The event that is triggered when SEOmatic has
50
     *        included the standard meta containers, and gives your plugin/module
51
     *        the chance to add whatever custom dynamic meta items you like
52
     *
53
     * ---
54
     * ```php
55
     * use nystudio107\seomatic\events\AddDynamicMetaEvent;
56
     * use nystudio107\seomatic\helpers\DynamicMeta;
57
     * use yii\base\Event;
58
     * Event::on(DynamicMeta::class, DynamicMeta::EVENT_INCLUDE_CONTAINER, function(AddDynamicMetaEvent $e) {
59
     *     // Add whatever dynamic meta items to the containers as you like
60
     * });
61
     * ```
62
     */
63
    const EVENT_ADD_DYNAMIC_META = 'addDynamicMeta';
64
65
    // Static Methods
66
    // =========================================================================
67
68
    /**
69
     * Return a sanitized URL with the query string stripped
70
     * @param string $url
0 ignored issues
show
Coding Style introduced by
There must be exactly one blank line before the tags in a doc comment
Loading history...
Coding Style introduced by
Missing parameter comment
Loading history...
71
     *
72
     * @return string
73
     */
74
    public static function sanitizeUrl(string $url): string
75
    {
76
        // Remove the query string
77
        $url = UrlHelper::stripQueryString($url);
78
        // HTML decode the entities, then strip out any tags
79
        $url = html_entity_decode($url, ENT_NOQUOTES, 'UTF-8');
80
        $url = strip_tags($url);
81
82
        // If this is a >= 400 status code, set the canonical URL to nothing
83
        if (Craft::$app->getResponse()->statusCode >= 400) {
84
            $url = '';
85
        }
86
87
        return UrlHelper::absoluteUrlWithProtocol($url);
88
    }
89
90
    /**
91
     * Paginate based on the passed in Paginate variable as returned from the
92
     * Twig {% paginate %} tag:
93
     * https://docs.craftcms.com/v3/templating/tags/paginate.html#the-pageInfo-variable
94
     *
95
     * @param Paginate $pageInfo
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
96
     */
97
    public static function paginate(Paginate $pageInfo)
98
    {
99
        if ($pageInfo !== null && $pageInfo->currentPage !== null) {
100
            // Let the meta containers know that this page is paginated
101
            Seomatic::$plugin->metaContainers->paginationPage = (string)$pageInfo->currentPage;
102
            // Set the current page
103
            $url = $pageInfo->getPageUrl($pageInfo->currentPage);
104
            $url = self::sanitizeUrl($url);
0 ignored issues
show
Bug introduced by
It seems like $url can also be of type null; however, parameter $url of nystudio107\seomatic\hel...amicMeta::sanitizeUrl() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

104
            $url = self::sanitizeUrl(/** @scrutinizer ignore-type */ $url);
Loading history...
105
            if (!empty($url)) {
106
                Seomatic::$seomaticVariable->meta->canonicalUrl = $url;
107
            }
108
            // Set the previous URL
109
            $url = $pageInfo->getPrevUrl();
110
            if (!empty($url)) {
111
                $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...
112
                    'rel' => 'prev',
113
                    'href' => $url,
114
                ]);
115
            }
116
            // Set the next URL
117
            $url = $pageInfo->getNextUrl();
118
            if (!empty($url)) {
119
                $metaTag = Seomatic::$plugin->link->create([
120
                    'rel' => 'next',
121
                    'href' => $url,
122
                ]);
123
            }
124
        }
125
    }
126
127
    /**
128
     * Include any headers for this request
129
     */
130
    public static function includeHttpHeaders()
131
    {
132
        if (Seomatic::$settings->headersEnabled) {
133
            $response = Craft::$app->getResponse();
134
            // X-Robots-Tag header
135
            $robots = Seomatic::$seomaticVariable->tag->get('robots');
136
            if ($robots !== null && $robots->include) {
137
                $robotsArray = $robots->renderAttributes();
138
                $content = $robotsArray['content'] ?? '';
139
                if (!empty($content)) {
140
                    // The content property can be a string or an array
141
                    if (\is_array($content)) {
142
                        $headerValue = '';
143
                        foreach ($content as $contentVal) {
144
                            $headerValue .= ($contentVal.',');
145
                        }
146
                        $headerValue = rtrim($headerValue, ',');
147
                    } else {
148
                        $headerValue = $content;
149
                    }
150
                    $response->headers->add('X-Robots-Tag', $headerValue);
151
                }
152
            }
153
            // Link canonical header
154
            $canonical = Seomatic::$seomaticVariable->link->get('canonical');
155
            if ($canonical !== null && $canonical->include) {
156
                $canonicalArray = $canonical->renderAttributes();
157
                $href = $canonicalArray['href'] ?? '';
158
                if (!empty($href)) {
159
                    // The href property can be a string or an array
160
                    if (\is_array($href)) {
161
                        $headerValue = '';
162
                        foreach ($href as $hrefVal) {
163
                            $headerValue .= ('<'.$hrefVal.'>'.',');
164
                        }
165
                        $headerValue = rtrim($headerValue, ',');
166
                    } else {
167
                        $headerValue = '<'.$href.'>';
168
                    }
169
                    $headerValue .= "; rel='canonical'";
170
                    $response->headers->add('Link', $headerValue);
171
                }
172
            }
173
            // Referrer-Policy header
174
            $referrer = Seomatic::$seomaticVariable->tag->get('referrer');
175
            if ($referrer !== null && $referrer->include) {
176
                $referrerArray = $referrer->renderAttributes();
177
                $content = $referrerArray['content'] ?? '';
178
                if (!empty($content)) {
179
                    // The content property can be a string or an array
180
                    if (\is_array($content)) {
181
                        $headerValue = '';
182
                        foreach ($content as $contentVal) {
183
                            $headerValue .= ($contentVal.',');
184
                        }
185
                        $headerValue = rtrim($headerValue, ',');
186
                    } else {
187
                        $headerValue = $content;
188
                    }
189
                    $response->headers->add('Referrer-Policy', $headerValue);
190
                }
191
            }
192
            // The X-Powered-By tag
193
            if (Seomatic::$settings->generatorEnabled) {
194
                $response->headers->add('X-Powered-By', 'SEOmatic');
195
            }
196
        }
197
    }
198
199
    /**
200
     * Add any custom/dynamic meta to the containers
201
     *
202
     * @param string|null $uri     The URI of the route to add dynamic metadata for
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces after parameter name; 5 found
Loading history...
203
     * @param int|null    $siteId  The siteId of the current site
0 ignored issues
show
Coding Style introduced by
Expected 1 spaces after parameter name; 2 found
Loading history...
204
     */
205
    public static function addDynamicMetaToContainers(string $uri = null, int $siteId = null)
206
    {
207
        Craft::beginProfile('DynamicMeta::addDynamicMetaToContainers', __METHOD__);
208
        $request = Craft::$app->getRequest();
209
        // Don't add dynamic meta to console requests, they have no concept of a URI or segments
210
        if (!$request->getIsConsoleRequest()) {
211
            $response = Craft::$app->getResponse();
212
            if ($response->statusCode < 400) {
213
                self::addMetaJsonLdBreadCrumbs($siteId);
214
                if (Seomatic::$settings->addHrefLang) {
215
                    self::addMetaLinkHrefLang($uri, $siteId);
216
                }
217
                self::addSameAsMeta();
218
                $metaSiteVars = Seomatic::$plugin->metaContainers->metaSiteVars;
219
                $jsonLd = Seomatic::$plugin->jsonLd->get('identity');
220
                if ($jsonLd !== null) {
221
                    self::addOpeningHours($jsonLd, $metaSiteVars->identity);
222
                    self::addContactPoints($jsonLd, $metaSiteVars->identity);
223
                }
224
                $jsonLd = Seomatic::$plugin->jsonLd->get('creator');
225
                if ($jsonLd !== null) {
226
                    self::addOpeningHours($jsonLd, $metaSiteVars->creator);
227
                    self::addContactPoints($jsonLd, $metaSiteVars->creator);
228
                }
229
                // Allow modules/plugins a chance to add dynamic meta
230
                $event = new AddDynamicMetaEvent([
231
                    'uri' => $uri,
232
                    'siteId' => $siteId,
233
                ]);
234
                Event::trigger(static::class, self::EVENT_ADD_DYNAMIC_META, $event);
235
            }
236
        }
237
        Craft::endProfile('DynamicMeta::addDynamicMetaToContainers', __METHOD__);
238
    }
239
240
    /**
241
     * Add the OpeningHoursSpecific to the $jsonLd based on the Entity settings
242
     *
243
     * @param MetaJsonLd $jsonLd
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
244
     * @param Entity     $entity
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
245
     */
246
    public static function addOpeningHours(MetaJsonLd $jsonLd, Entity $entity)
247
    {
248
        if ($jsonLd instanceof LocalBusiness && $entity !== null) {
249
            /** @var LocalBusiness $jsonLd */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
250
            $openingHours = [];
251
            $days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
252
            $times = $entity->localBusinessOpeningHours;
253
            $index = 0;
254
            foreach ($times as $hours) {
255
                $openTime = '';
256
                $closeTime = '';
257
                if (!empty($hours['open'])) {
258
                    /** @var \DateTime $dateTime */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
259
                    try {
260
                        $dateTime = DateTimeHelper::toDateTime($hours['open']['date'], false, false);
261
                    } catch (\Exception $e) {
262
                        $dateTime = false;
263
                    }
264
                    if ($dateTime !== false) {
265
                        $openTime = $dateTime->format('H:i:s');
266
                    }
267
                }
268
                if (!empty($hours['close'])) {
269
                    /** @var \DateTime $dateTime */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
270
                    try {
271
                        $dateTime = DateTimeHelper::toDateTime($hours['close']['date'], false, false);
272
                    } catch (\Exception $e) {
273
                        $dateTime = false;
274
                    }
275
                    if ($dateTime !== false) {
276
                        $closeTime = $dateTime->format('H:i:s');
277
                    }
278
                }
279
                if ($openTime && $closeTime) {
280
                    $hours = Seomatic::$plugin->jsonLd->create([
281
                        'type' => 'OpeningHoursSpecification',
282
                        'opens' => $openTime,
283
                        'closes' => $closeTime,
284
                        'dayOfWeek' => [$days[$index]],
285
                    ], false);
286
                    $openingHours[] = $hours;
287
                }
288
                $index++;
289
            }
290
            $jsonLd->openingHoursSpecification = $openingHours;
291
        }
292
    }
293
294
    /**
295
     * Add the ContactPoint to the $jsonLd based on the Entity settings
296
     *
297
     * @param MetaJsonLd $jsonLd
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
298
     * @param Entity     $entity
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
299
     */
300
    public static function addContactPoints(MetaJsonLd $jsonLd, Entity $entity)
301
    {
302
        if ($jsonLd instanceof Organization && $entity !== null) {
303
            /** @var Organization $jsonLd */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
304
            $contactPoints = [];
305
            if ($entity->organizationContactPoints !== null && \is_array($entity->organizationContactPoints)) {
306
                foreach ($entity->organizationContactPoints as $contacts) {
307
                    /** @var ContactPoint $contact */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
308
                    $contact = Seomatic::$plugin->jsonLd->create([
309
                        'type' => 'ContactPoint',
310
                        'telephone' => $contacts['telephone'],
311
                        'contactType' => $contacts['contactType'],
312
                    ], false);
313
                    $contactPoints[] = $contact;
314
                }
315
            }
316
            $jsonLd->contactPoint = $contactPoints;
317
        }
318
    }
319
320
    /**
321
     * Add breadcrumbs to the MetaJsonLdContainer
322
     *
323
     * @param int|null $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
324
     */
325
    public static function addMetaJsonLdBreadCrumbs(int $siteId = null)
326
    {
327
        $position = 1;
328
        if ($siteId === null) {
329
            $siteId = Craft::$app->getSites()->currentSite->id
330
                ?? Craft::$app->getSites()->primarySite->id
331
                ?? 1;
332
        }
333
        $site = Craft::$app->getSites()->getSiteById($siteId);
334
        if ($site === null) {
335
            return;
336
        }
337
        try {
338
            $siteUrl = $site->hasUrls ? $site->baseUrl : Craft::$app->getSites()->getPrimarySite()->baseUrl;
339
        } catch (SiteNotFoundException $e) {
340
            $siteUrl = Craft::$app->getConfig()->general->siteUrl;
341
            Craft::error($e->getMessage(), __METHOD__);
342
        }
343
        /** @var  $crumbs BreadcrumbList */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
344
        $crumbs = Seomatic::$plugin->jsonLd->create([
345
            'type' => 'BreadcrumbList',
346
            'name' => 'Breadcrumbs',
347
            'description' => 'Breadcrumbs list',
348
        ]);
349
        /** @var Element $element */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
350
        $element = Craft::$app->getElements()->getElementByUri('__home__', $siteId);
351
        if ($element) {
0 ignored issues
show
introduced by
$element is of type craft\base\Element, thus it always evaluated to true.
Loading history...
352
            $uri = $element->uri === '__home__' ? '' : ($element->uri ?? '');
353
            try {
354
                $id = UrlHelper::siteUrl($uri, null, null, $siteId);
355
            } catch (Exception $e) {
356
                $id = $siteUrl;
357
                Craft::error($e->getMessage(), __METHOD__);
358
            }
359
            $listItem = MetaJsonLd::create('ListItem', [
360
                'position' => $position,
361
                'item' => [
362
                    '@id' => $id,
363
                    'name' => $element->title,
364
                ],
365
            ]);
366
            $crumbs->itemListElement[] = $listItem;
367
        } else {
368
            $crumbs->itemListElement[] = MetaJsonLd::create('ListItem', [
369
                'position' => $position,
370
                'item' => [
371
                    '@id' => $siteUrl,
372
                    'name' => 'Homepage',
373
                ],
374
            ]);
375
        }
376
        // Build up the segments, and look for elements that match
377
        $uri = '';
378
        $segments = Craft::$app->getRequest()->getSegments();
379
        /** @var  $lastElement Element */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
380
        $lastElement = Seomatic::$matchedElement;
381
        if ($lastElement && $element) {
0 ignored issues
show
introduced by
$element is of type craft\base\Element, thus it always evaluated to true.
Loading history...
382
            if ($lastElement->uri !== '__home__' && $element->uri) {
383
                $path = $lastElement->uri;
384
                $segments = array_values(array_filter(explode('/', $path), function ($segment) {
385
                    return $segment !== '';
386
                }));
387
            }
388
        }
389
        // Parse through the segments looking for elements that match
390
        foreach ($segments as $segment) {
391
            $uri .= $segment;
392
            /** @var Element $element */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
393
            $element = Craft::$app->getElements()->getElementByUri($uri, $siteId);
394
            if ($element && $element->uri) {
395
                $position++;
396
                $uri = $element->uri === '__home__' ? '' : $element->uri;
397
                try {
398
                    $id = UrlHelper::siteUrl($uri, null, null, $siteId);
399
                } catch (Exception $e) {
400
                    $id = $siteUrl;
401
                    Craft::error($e->getMessage(), __METHOD__);
402
                }
403
                $crumbs->itemListElement[] = MetaJsonLd::create('ListItem', [
404
                    'position' => $position,
405
                    'item' => [
406
                        '@id' => $id,
407
                        'name' => $element->title,
408
                    ],
409
                ]);
410
            }
411
            $uri .= '/';
412
        }
413
414
        Seomatic::$plugin->jsonLd->add($crumbs);
415
    }
416
417
    /**
418
     * Add meta hreflang tags if there is more than one site
419
     *
420
     * @param string   $uri
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
421
     * @param int|null $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
422
     */
423
    public static function addMetaLinkHrefLang(string $uri = null, int $siteId = null)
424
    {
425
        $siteLocalizedUrls = self::getLocalizedUrls($uri, $siteId);
426
427
        if (!empty($siteLocalizedUrls)) {
428
            $siteLocalizedUrl = $siteLocalizedUrls[0];
429
            // Add the rel=alternate tag
430
            $metaTag = Seomatic::$plugin->link->create([
431
                'rel' => 'alternate',
432
                'hreflang' => [],
433
                'href' => [],
434
            ]);
435
            // Add the x-default hreflang
436
            if (Seomatic::$settings->addXDefaultHrefLang) {
437
                $metaTag->hreflang[] = 'x-default';
438
                $metaTag->href[] = $siteLocalizedUrl['url'];
439
            }
440
            // Add the alternate language link rel's
441
            if (\count($siteLocalizedUrls) > 1) {
442
                foreach ($siteLocalizedUrls as $siteLocalizedUrl) {
443
                    $metaTag->hreflang[] = $siteLocalizedUrl['hreflangLanguage'];
444
                    $metaTag->href[] = $siteLocalizedUrl['url'];
445
                }
446
                Seomatic::$plugin->link->add($metaTag);
447
            }
448
            // Add in the og:locale:alternate tags
449
            $ogLocaleAlternate = Seomatic::$plugin->tag->get('og:locale:alternate');
450
            if (\count($siteLocalizedUrls) > 1 && $ogLocaleAlternate) {
451
                $ogContentArray = [];
452
                foreach ($siteLocalizedUrls as $siteLocalizedUrl) {
453
                    if (!\in_array($siteLocalizedUrl['ogLanguage'], $ogContentArray, true)) {
454
                        $ogContentArray[] = $siteLocalizedUrl['ogLanguage'];
455
                    }
456
                }
457
                $ogLocaleAlternate->content = $ogContentArray;
458
            }
459
        }
460
    }
461
462
    /**
463
     * Add the Same As meta tags and JSON-LD
464
     */
465
    public static function addSameAsMeta()
466
    {
467
        $metaContainers = Seomatic::$plugin->metaContainers;
468
        $sameAsUrls = [];
469
        if (!empty($metaContainers->metaSiteVars->sameAsLinks)) {
470
            $sameAsUrls = ArrayHelper::getColumn($metaContainers->metaSiteVars->sameAsLinks, 'url', false);
471
            $sameAsUrls = array_values(array_filter($sameAsUrls));
472
        }
473
        // Facebook OpenGraph
474
        $ogSeeAlso = Seomatic::$plugin->tag->get('og:see_also');
475
        if ($ogSeeAlso) {
0 ignored issues
show
introduced by
$ogSeeAlso is of type nystudio107\seomatic\models\MetaTag, thus it always evaluated to true.
Loading history...
476
            $ogSeeAlso->content = $sameAsUrls;
477
        }
478
        // Site Identity JSON-LD
479
        $identity = Seomatic::$plugin->jsonLd->get('identity');
480
        /** @var Thing $identity */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
481
        if ($identity !== null && property_exists($identity, 'sameAs')) {
482
            $identity->sameAs = $sameAsUrls;
483
        }
484
    }
485
486
    /**
487
     * Return a list of localized URLs that are in the current site's group
488
     * The current URI is used if $uri is null. Similarly, the current site is
489
     * used if $siteId is null.
490
     * The resulting array of arrays has `id`, `language`, `ogLanguage`,
491
     * `hreflangLanguage`, and `url` as keys.
492
     *
493
     * @param string|null $uri
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
494
     * @param int|null    $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
495
     *
496
     * @return array
497
     */
498
    public static function getLocalizedUrls(string $uri = null, int $siteId = null): array
499
    {
500
        $localizedUrls = [];
501
        // No pagination params for URLs
502
        $urlParams = null;
503
        // Get the request URI
504
        if ($uri === null) {
505
            $requestUri = Craft::$app->getRequest()->pathInfo;
506
        } else {
507
            $requestUri = $uri;
508
        }
509
        // Get the site to use
510
        if ($siteId === null) {
511
            try {
512
                $thisSite = Craft::$app->getSites()->getCurrentSite();
513
            } catch (SiteNotFoundException $e) {
514
                $thisSite = null;
515
                Craft::error($e->getMessage(), __METHOD__);
516
            }
517
        } else {
518
            $thisSite = Craft::$app->getSites()->getSiteById($siteId);
519
        }
520
        // Bail if we can't get a site
521
        if ($thisSite === null) {
522
            return $localizedUrls;
523
        }
524
        if (Seomatic::$settings->siteGroupsSeparate) {
525
            // Get only the sites that are in the current site's group
526
            try {
527
                $siteGroup = $thisSite->getGroup();
528
            } catch (InvalidConfigException $e) {
529
                $siteGroup = null;
530
                Craft::error($e->getMessage(), __METHOD__);
531
            }
532
            // Bail if we can't get a site group
533
            if ($siteGroup === null) {
534
                return $localizedUrls;
535
            }
536
            $sites = $siteGroup->getSites();
537
        } else {
538
            $sites = Craft::$app->getSites()->getAllSites();
539
        }
540
        $elements = Craft::$app->getElements();
541
        foreach ($sites as $site) {
542
            $includeUrl = true;
543
            $matchedElement = $elements->getElementByUri($requestUri);
544
            if ($matchedElement) {
545
                $url = $elements->getElementUriForSite($matchedElement->getId(), $site->id);
0 ignored issues
show
Bug introduced by
It seems like $matchedElement->getId() can also be of type null; however, parameter $elementId of craft\services\Elements::getElementUriForSite() does only seem to accept integer, 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

545
                $url = $elements->getElementUriForSite(/** @scrutinizer ignore-type */ $matchedElement->getId(), $site->id);
Loading history...
546
                // See if they have disabled sitemaps or robots for this entry,
547
                // and if so, don't include it in the hreflang
548
                /** @var Element $element */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
549
                $element = null;
550
                if ($url) {
551
                    $element = $elements->getElementByUri($url, $site->id, false);
552
                }
553
                if ($element !== null) {
554
                    if (isset($element->enabledForSite) && !(bool)$element->enabledForSite) {
555
                        $includeUrl = false;
556
                    }
557
                    /** @var MetaBundle $metaBundle */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
558
                    list($sourceId, $sourceBundleType, $sourceHandle, $sourceSiteId)
559
                        = Seomatic::$plugin->metaBundles->getMetaSourceFromElement($element);
560
                    $metaBundle = Seomatic::$plugin->metaBundles->getMetaBundleBySourceId(
561
                        $sourceBundleType,
562
                        $sourceId,
563
                        $sourceSiteId
564
                    );
565
                    if ($metaBundle !== null) {
566
                        // If sitemaps are off for this entry, don't include the URL
567
                        if (!$metaBundle->metaSitemapVars->sitemapUrls) {
568
                            $includeUrl = false;
569
                        }
570
                        // If robots is set tp 'none' don't include the URL
571
                        if ($metaBundle->metaGlobalVars->robots === 'none') {
572
                            $includeUrl = false;
573
                        }
574
                    }
575
                    $fieldHandles = FieldHelper::fieldsOfTypeFromElement(
576
                        $element,
577
                        FieldHelper::SEO_SETTINGS_CLASS_KEY,
578
                        true
579
                    );
580
                    foreach ($fieldHandles as $fieldHandle) {
581
                        if (!empty($element->$fieldHandle)) {
582
                            /** @var MetaBundle $metaBundle */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
583
                            $fieldMetaBundle = $element->$fieldHandle;
584
                            /** @var SeoSettings $seoSettingsField */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
585
                            $seoSettingsField = Craft::$app->getFields()->getFieldByHandle($fieldHandle);
586
                            if ($fieldMetaBundle !== null && $seoSettingsField !== null && $seoSettingsField->sitemapTabEnabled) {
587
                                // If sitemaps are off for this entry, don't include the URL
588
                                if (\in_array('sitemapUrls', $seoSettingsField->sitemapEnabledFields, false)
589
                                    && !$fieldMetaBundle->metaSitemapVars->sitemapUrls
590
                                ) {
591
                                    $includeUrl = false;
592
                                }
593
                                // If robots is set to 'none' don't include the URL
594
                                if ($fieldMetaBundle->metaGlobalVars->robots === 'none') {
595
                                    $includeUrl = false;
596
                                }
597
                            }
598
                        }
599
                    }
600
                } else {
601
                    $includeUrl = false;
602
                }
603
                $url = ($url === '__home__') ? '' : $url;
604
            } else {
605
                try {
606
                    $url = $site->hasUrls ? UrlHelper::siteUrl($requestUri, $urlParams, null, $site->id)
607
                        : Craft::$app->getSites()->getPrimarySite()->baseUrl;
608
                } catch (SiteNotFoundException $e) {
609
                    $url = '';
610
                    Craft::error($e->getMessage(), __METHOD__);
611
                } catch (Exception $e) {
612
                    $url = '';
613
                    Craft::error($e->getMessage(), __METHOD__);
614
                }
615
            }
616
            $url = $url ?? '';
617
            if (!UrlHelper::isAbsoluteUrl($url)) {
618
                try {
619
                    $url = UrlHelper::siteUrl($url, $urlParams, null, $site->id);
620
                } catch (Exception $e) {
621
                    $url = '';
622
                    Craft::error($e->getMessage(), __METHOD__);
623
                }
624
            }
625
            // Strip any query string params, and make sure we have an absolute URL with protocol
626
            if ($urlParams === null) {
627
                $url = UrlHelper::stripQueryString($url);
628
            }
629
            $url = UrlHelper::absoluteUrlWithProtocol($url);
630
631
            $url = $url ?? '';
632
            $language = $site->language;
633
            $ogLanguage = str_replace('-', '_', $language);
634
            $hreflangLanguage = $language;
635
            $hreflangLanguage = strtolower($hreflangLanguage);
636
            $hreflangLanguage = str_replace('_', '-', $hreflangLanguage);
637
            if ($includeUrl) {
638
                $localizedUrls[] = [
639
                    'id' => $site->id,
640
                    'language' => $language,
641
                    'ogLanguage' => $ogLanguage,
642
                    'hreflangLanguage' => $hreflangLanguage,
643
                    'url' => $url,
644
                ];
645
            }
646
        }
647
648
        return $localizedUrls;
649
    }
650
651
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $value should have a doc-comment as per coding-style.
Loading history...
652
     * Normalize the array of opening hours passed in
653
     *
654
     * @param $value
0 ignored issues
show
Coding Style Documentation introduced by
Missing parameter name
Loading history...
655
     */
656
    public static function normalizeTimes(&$value)
657
    {
658
        if (\is_string($value)) {
659
            $value = Json::decode($value);
660
        }
661
        $normalized = [];
662
        $times = ['open', 'close'];
663
        for ($day = 0; $day <= 6; $day++) {
664
            foreach ($times as $time) {
665
                if (isset($value[$day][$time])
666
                    && ($date = DateTimeHelper::toDateTime($value[$day][$time])) !== false
667
                ) {
668
                    $normalized[$day][$time] = $date;
669
                } else {
670
                    $normalized[$day][$time] = null;
671
                }
672
            }
673
        }
674
675
        $value = $normalized;
676
    }
677
}
678