Test Failed
Push — develop ( 0859fa...bfaabe )
by Andrew
09:47
created

DynamicMeta::paginate()   A

Complexity

Conditions 6
Paths 9

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 6
eloc 15
c 3
b 0
f 0
nc 9
nop 1
dl 0
loc 25
ccs 0
cts 16
cp 0
crap 42
rs 9.2222
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\helpers\Text as TextHelper;
19
use nystudio107\seomatic\helpers\Localization as LocalizationHelper;
20
use nystudio107\seomatic\models\Entity;
21
use nystudio107\seomatic\models\jsonld\ContactPoint;
22
use nystudio107\seomatic\models\jsonld\LocalBusiness;
23
use nystudio107\seomatic\models\jsonld\Organization;
24
use nystudio107\seomatic\models\jsonld\BreadcrumbList;
25
use nystudio107\seomatic\models\jsonld\Thing;
26
use nystudio107\seomatic\models\MetaBundle;
27
use nystudio107\seomatic\models\MetaJsonLd;
28
use nystudio107\seomatic\services\Helper as SeomaticHelper;
29
30
31
use Craft;
32
use craft\base\Element;
33
use craft\errors\SiteNotFoundException;
34
use craft\helpers\DateTimeHelper;
35
use craft\web\twig\variables\Paginate;
36
37
use yii\base\Event;
38
use yii\base\Exception;
39
use yii\base\InvalidConfigException;
40
41
/**
42
 * @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...
43
 * @package   Seomatic
44
 * @since     3.0.0
45
 */
46
class DynamicMeta
47
{
48
    // Constants
49
    // =========================================================================
50
51
    /**
52
     * @event AddDynamicMetaEvent The event that is triggered when SEOmatic has
53
     *        included the standard meta containers, and gives your plugin/module
54
     *        the chance to add whatever custom dynamic meta items you like
55
     *
56
     * ---
57
     * ```php
58
     * use nystudio107\seomatic\events\AddDynamicMetaEvent;
59
     * use nystudio107\seomatic\helpers\DynamicMeta;
60
     * use yii\base\Event;
61
     * Event::on(DynamicMeta::class, DynamicMeta::EVENT_ADD_DYNAMIC_META, function(AddDynamicMetaEvent $e) {
62
     *     // Add whatever dynamic meta items to the containers as you like
63
     * });
64
     * ```
65
     */
66
    const EVENT_ADD_DYNAMIC_META = 'addDynamicMeta';
67
68
    // Static Methods
69
    // =========================================================================
70
71
72
    /**
73
     * Return a sanitized URL with the query string stripped
74
     *
75
     * @param string $url
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
76
     * @param bool $checkStatus
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 3 spaces after parameter type; 1 found
Loading history...
77
     *
78
     * @return string
79
     */
80 2
    public static function sanitizeUrl(string $url, bool $checkStatus = true): string
81
    {
82
        // Remove the query string
83 2
        $url = UrlHelper::stripQueryString($url);
84 2
        $url = TextHelper::sanitizeUserInput($url);
85
86
        // If this is a >= 400 status code, set the canonical URL to nothing
87 2
        if ($checkStatus && Craft::$app->getResponse()->statusCode >= 400) {
88
            $url = '';
89
        }
90
91 2
        return $url;
92
    }
93
94
    /**
95
     * Paginate based on the passed in Paginate variable as returned from the
96
     * Twig {% paginate %} tag:
97
     * https://docs.craftcms.com/v3/templating/tags/paginate.html#the-pageInfo-variable
98
     *
99
     * @param Paginate $pageInfo
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
100
     */
101
    public static function paginate(Paginate $pageInfo)
102
    {
103
        if ($pageInfo !== null && $pageInfo->currentPage !== null) {
104
            // Let the meta containers know that this page is paginated
105
            Seomatic::$plugin->metaContainers->paginationPage = (string)$pageInfo->currentPage;
106
            // Set the the canonical URL to be the first page of the paginated pages
107
            // see: https://github.com/nystudio107/craft-seomatic/issues/375#issuecomment-488369209
108
            $url = $pageInfo->getPageUrl($pageInfo->currentPage);
109
            if (!empty($url)) {
110
                Seomatic::$seomaticVariable->meta->canonicalUrl = $url;
111
            }
112
            // Set the previous URL
113
            $url = $pageInfo->getPrevUrl();
114
            if (!empty($url)) {
115
                $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...
116
                    'rel' => 'prev',
117
                    'href' => $url,
118
                ]);
119
            }
120
            // Set the next URL
121
            $url = $pageInfo->getNextUrl();
122
            if (!empty($url)) {
123
                $metaTag = Seomatic::$plugin->link->create([
124
                    'rel' => 'next',
125
                    'href' => $url,
126
                ]);
127
            }
128
        }
129
    }
130
131
    /**
132
     * Include any headers for this request
133
     */
134
    public static function includeHttpHeaders()
135
    {
136
        self::addCspHeaders();
137
        // Don't include headers for any response code >= 400
138
        $request = Craft::$app->getRequest();
139
        if (!$request->isConsoleRequest) {
140
            $response = Craft::$app->getResponse();
141
            if ($response->statusCode >= 400 || SeomaticHelper::isPreview()) {
142
                return;
143
            }
144
        }
145
        // Assuming they have headersEnabled, add the response code to the headers
146
        if (Seomatic::$settings->headersEnabled) {
147
            $response = Craft::$app->getResponse();
148
            // X-Robots-Tag header
149
            $robots = Seomatic::$seomaticVariable->tag->get('robots');
150
            if ($robots !== null && $robots->include) {
151
                $robotsArray = $robots->renderAttributes();
152
                $content = $robotsArray['content'] ?? '';
153
                if (!empty($content)) {
154
                    // The content property can be a string or an array
155
                    if (\is_array($content)) {
156
                        $headerValue = '';
157
                        foreach ($content as $contentVal) {
158
                            $headerValue .= ($contentVal.',');
159
                        }
160
                        $headerValue = rtrim($headerValue, ',');
161
                    } else {
162
                        $headerValue = $content;
163
                    }
164
                    $response->headers->set('X-Robots-Tag', $headerValue);
165
                }
166
            }
167
            // Link canonical header
168
            $canonical = Seomatic::$seomaticVariable->link->get('canonical');
169
            if ($canonical !== null && $canonical->include) {
170
                $canonicalArray = $canonical->renderAttributes();
171
                $href = $canonicalArray['href'] ?? '';
172
                if (!empty($href)) {
173
                    // The href property can be a string or an array
174
                    if (\is_array($href)) {
175
                        $headerValue = '';
176
                        foreach ($href as $hrefVal) {
177
                            $headerValue .= ('<'.$hrefVal.'>'.',');
178
                        }
179
                        $headerValue = rtrim($headerValue, ',');
180
                    } else {
181
                        $headerValue = '<'.$href.'>';
182
                    }
183
                    $headerValue .= "; rel='canonical'";
184
                    $response->headers->add('Link', $headerValue);
185
                }
186
            }
187
            // Referrer-Policy header
188
            $referrer = Seomatic::$seomaticVariable->tag->get('referrer');
189
            if ($referrer !== null && $referrer->include) {
190
                $referrerArray = $referrer->renderAttributes();
191
                $content = $referrerArray['content'] ?? '';
192
                if (!empty($content)) {
193
                    // The content property can be a string or an array
194
                    if (\is_array($content)) {
195
                        $headerValue = '';
196
                        foreach ($content as $contentVal) {
197
                            $headerValue .= ($contentVal.',');
198
                        }
199
                        $headerValue = rtrim($headerValue, ',');
200
                    } else {
201
                        $headerValue = $content;
202
                    }
203
                    $response->headers->add('Referrer-Policy', $headerValue);
204
                }
205
            }
206
            // The X-Powered-By tag
207
            if (Seomatic::$settings->generatorEnabled) {
208
                $response->headers->add('X-Powered-By', 'SEOmatic');
209
            }
210
        }
211
    }
212
213
    /**
214
     * Get all of the CSP Nonces from containers that can have them
215
     *
216
     * @return array
217
     */
218
    public static function getCspNonces(): array
219
    {
220
        $cspNonces = [];
221
        // Add in any fixed policies from Settings
222
        if (!empty(Seomatic::$settings->cspScriptSrcPolicies)) {
223
            $fixedCsps = Seomatic::$settings->cspScriptSrcPolicies;
224
            $iterator = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($fixedCsps));
225
            foreach($iterator as $value) {
0 ignored issues
show
Coding Style introduced by
Expected "foreach (...) {\n"; found "foreach(...) {\n"
Loading history...
226
                $cspNonces[] = $value;
227
            }
228
        }
229
        // Add in any CSP nonce headers
230
        $container = Seomatic::$plugin->jsonLd->container();
231
        if ($container !== null) {
232
            $cspNonces = array_merge($cspNonces, $container->getCspNonces());
233
        }
234
        $container = Seomatic::$plugin->script->container();
235
        if ($container !== null) {
236
            $cspNonces = array_merge($cspNonces, $container->getCspNonces());
237
        }
238
239
        return $cspNonces;
240
    }
241
242
    /**
243
     * Add the Content-Security-Policy script-src headers
244
     */
245
    public static function addCspHeaders()
246
    {
247
        $cspNonces = self::getCspNonces();
248
        $container = Seomatic::$plugin->script->container();
249
        if ($container !== null) {
250
            $container->addNonceHeaders($cspNonces);
251
        }
252
    }
253
254
    /**
255
     * Add the Content-Security-Policy script-src tags
256
     */
257
    public static function addCspTags()
258
    {
259
        $cspNonces = self::getCspNonces();
260
        $container = Seomatic::$plugin->script->container();
261
        if ($container !== null) {
262
            $container->addNonceTags($cspNonces);
263
        }
264
    }
265
266
    /**
267
     * Add any custom/dynamic meta to the containers
268
     *
269
     * @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...
270
     * @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...
271
     */
272
    public static function addDynamicMetaToContainers(string $uri = null, int $siteId = null)
273
    {
274
        Craft::beginProfile('DynamicMeta::addDynamicMetaToContainers', __METHOD__);
275
        $request = Craft::$app->getRequest();
276
        // Don't add dynamic meta to console requests, they have no concept of a URI or segments
277
        if (!$request->getIsConsoleRequest()) {
278
            $response = Craft::$app->getResponse();
279
            if ($response->statusCode < 400) {
280
                self::addMetaJsonLdBreadCrumbs($siteId);
281
                if (Seomatic::$settings->addHrefLang) {
282
                    self::addMetaLinkHrefLang($uri, $siteId);
283
                }
284
                self::addSameAsMeta();
285
                $metaSiteVars = Seomatic::$plugin->metaContainers->metaSiteVars;
286
                $jsonLd = Seomatic::$plugin->jsonLd->get('identity');
287
                if ($jsonLd !== null) {
288
                    self::addOpeningHours($jsonLd, $metaSiteVars->identity);
289
                    self::addContactPoints($jsonLd, $metaSiteVars->identity);
290
                }
291
                $jsonLd = Seomatic::$plugin->jsonLd->get('creator');
292
                if ($jsonLd !== null) {
293
                    self::addOpeningHours($jsonLd, $metaSiteVars->creator);
294
                    self::addContactPoints($jsonLd, $metaSiteVars->creator);
295
                }
296
                // Allow modules/plugins a chance to add dynamic meta
297
                $event = new AddDynamicMetaEvent([
298
                    'uri' => $uri,
299
                    'siteId' => $siteId,
300
                ]);
301
                Event::trigger(static::class, self::EVENT_ADD_DYNAMIC_META, $event);
302
            }
303
        }
304
        Craft::endProfile('DynamicMeta::addDynamicMetaToContainers', __METHOD__);
305
    }
306
307
    /**
308
     * Add the OpeningHoursSpecific to the $jsonLd based on the Entity settings
309
     *
310
     * @param MetaJsonLd $jsonLd
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
311
     * @param Entity     $entity
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
312
     */
313
    public static function addOpeningHours(MetaJsonLd $jsonLd, Entity $entity)
314
    {
315
        if ($jsonLd instanceof LocalBusiness && $entity !== null) {
316
            /** @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...
317
            $openingHours = [];
318
            $days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
319
            $times = $entity->localBusinessOpeningHours;
320
            $index = 0;
321
            foreach ($times as $hours) {
322
                $openTime = '';
323
                $closeTime = '';
324
                if (!empty($hours['open'])) {
325
                    /** @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...
326
                    try {
327
                        $dateTime = DateTimeHelper::toDateTime($hours['open']['date'], false, false);
328
                    } catch (\Exception $e) {
329
                        $dateTime = false;
330
                    }
331
                    if ($dateTime !== false) {
332
                        $openTime = $dateTime->format('H:i:s');
333
                    }
334
                }
335
                if (!empty($hours['close'])) {
336
                    /** @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...
337
                    try {
338
                        $dateTime = DateTimeHelper::toDateTime($hours['close']['date'], false, false);
339
                    } catch (\Exception $e) {
340
                        $dateTime = false;
341
                    }
342
                    if ($dateTime !== false) {
343
                        $closeTime = $dateTime->format('H:i:s');
344
                    }
345
                }
346
                if ($openTime && $closeTime) {
347
                    $hours = Seomatic::$plugin->jsonLd->create([
348
                        'type' => 'OpeningHoursSpecification',
349
                        'opens' => $openTime,
350
                        'closes' => $closeTime,
351
                        'dayOfWeek' => [$days[$index]],
352
                    ], false);
353
                    $openingHours[] = $hours;
354
                }
355
                $index++;
356
            }
357
            $jsonLd->openingHoursSpecification = $openingHours;
358
        }
359
    }
360
361
    /**
362
     * Add the ContactPoint to the $jsonLd based on the Entity settings
363
     *
364
     * @param MetaJsonLd $jsonLd
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
365
     * @param Entity     $entity
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
366
     */
367
    public static function addContactPoints(MetaJsonLd $jsonLd, Entity $entity)
368
    {
369
        if ($jsonLd instanceof Organization && $entity !== null) {
370
            /** @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...
371
            $contactPoints = [];
372
            if ($entity->organizationContactPoints !== null && \is_array($entity->organizationContactPoints)) {
373
                foreach ($entity->organizationContactPoints as $contacts) {
374
                    /** @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...
375
                    $contact = Seomatic::$plugin->jsonLd->create([
376
                        'type' => 'ContactPoint',
377
                        'telephone' => $contacts['telephone'],
378
                        'contactType' => $contacts['contactType'],
379
                    ], false);
380
                    $contactPoints[] = $contact;
381
                }
382
            }
383
            $jsonLd->contactPoint = $contactPoints;
0 ignored issues
show
Documentation Bug introduced by
It seems like $contactPoints of type array or nystudio107\seomatic\models\jsonld\ContactPoint[] is incompatible with the declared type nystudio107\seomatic\models\jsonld\ContactPoint of property $contactPoint.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
384
        }
385
    }
386
387
    /**
388
     * Add breadcrumbs to the MetaJsonLdContainer
389
     *
390
     * @param int|null $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
391
     */
392
    public static function addMetaJsonLdBreadCrumbs(int $siteId = null)
393
    {
394
        $position = 1;
395
        if ($siteId === null) {
396
            $siteId = Craft::$app->getSites()->currentSite->id
397
                ?? Craft::$app->getSites()->primarySite->id
398
                ?? 1;
399
        }
400
        $site = Craft::$app->getSites()->getSiteById($siteId);
401
        if ($site === null) {
402
            return;
403
        }
404
        try {
405
            $siteUrl = $site->hasUrls ? $site->baseUrl : Craft::$app->getSites()->getPrimarySite()->baseUrl;
406
        } catch (SiteNotFoundException $e) {
407
            $siteUrl = Craft::$app->getConfig()->general->siteUrl;
408
            Craft::error($e->getMessage(), __METHOD__);
409
        }
410
        if (!empty(Seomatic::$settings->siteUrlOverride)) {
411
            $siteUrl = Seomatic::$settings->siteUrlOverride;
412
        }
413
        $siteUrl = $siteUrl ?: '/';
414
        /** @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...
415
        $crumbs = Seomatic::$plugin->jsonLd->create([
416
            'type' => 'BreadcrumbList',
417
            'name' => 'Breadcrumbs',
418
            'description' => 'Breadcrumbs list',
419
        ]);
420
        /** @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...
421
        $element = Craft::$app->getElements()->getElementByUri('__home__', $siteId);
422
        if ($element) {
0 ignored issues
show
introduced by
$element is of type craft\base\Element, thus it always evaluated to true.
Loading history...
423
            $uri = $element->uri === '__home__' ? '' : ($element->uri ?? '');
424
            try {
425
                $id = UrlHelper::siteUrl($uri, null, null, $siteId);
426
            } catch (Exception $e) {
427
                $id = $siteUrl;
428
                Craft::error($e->getMessage(), __METHOD__);
429
            }
430
            $item = UrlHelper::stripQueryString($id);
431
            $item = UrlHelper::absoluteUrlWithProtocol($item);
432
            $listItem = MetaJsonLd::create('ListItem', [
433
                'position' => $position,
434
                'name' => $element->title,
435
                'item' => $item,
436
                '@id' => $id,
437
            ]);
438
            $crumbs->itemListElement[] = $listItem;
439
        } else {
440
            $item = UrlHelper::stripQueryString($siteUrl ?? '/');
441
            $item = UrlHelper::absoluteUrlWithProtocol($item);
442
            $crumbs->itemListElement[] = MetaJsonLd::create('ListItem', [
443
                'position' => $position,
444
                'name' => 'Homepage',
445
                'item' => $item,
446
                '@id' => $siteUrl,
447
            ]);
448
        }
449
        // Build up the segments, and look for elements that match
450
        $uri = '';
451
        $segments = Craft::$app->getRequest()->getSegments();
452
        /** @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...
453
        $lastElement = Seomatic::$matchedElement;
454
        if ($lastElement && $element) {
0 ignored issues
show
introduced by
$element is of type craft\base\Element, thus it always evaluated to true.
Loading history...
455
            if ($lastElement->uri !== '__home__' && $element->uri) {
456
                $path = $lastElement->uri;
457
                $segments = array_values(array_filter(explode('/', $path), function ($segment) {
0 ignored issues
show
Bug introduced by
It seems like $path can also be of type null; however, parameter $string of explode() 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

457
                $segments = array_values(array_filter(explode('/', /** @scrutinizer ignore-type */ $path), function ($segment) {
Loading history...
458
                    return $segment !== '';
459
                }));
460
            }
461
        }
462
        // Parse through the segments looking for elements that match
463
        foreach ($segments as $segment) {
464
            $uri .= $segment;
465
            /** @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...
466
            $element = Craft::$app->getElements()->getElementByUri($uri, $siteId);
467
            if ($element && $element->uri) {
468
                $position++;
469
                $uri = $element->uri === '__home__' ? '' : $element->uri;
470
                try {
471
                    $id = UrlHelper::siteUrl($uri, null, null, $siteId);
472
                } catch (Exception $e) {
473
                    $id = $siteUrl;
474
                    Craft::error($e->getMessage(), __METHOD__);
475
                }
476
                $item = UrlHelper::stripQueryString($id);
477
                $item = UrlHelper::absoluteUrlWithProtocol($item);
478
                $crumbs->itemListElement[] = MetaJsonLd::create('ListItem', [
479
                    'position' => $position,
480
                    'name' => $element->title,
481
                    'item' => $item,
482
                    '@id' => $id,
483
                ]);
484
            }
485
            $uri .= '/';
486
        }
487
488
        Seomatic::$plugin->jsonLd->add($crumbs);
489
    }
490
491
    /**
492
     * Add meta hreflang tags if there is more than one site
493
     *
494
     * @param string   $uri
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
495
     * @param int|null $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
496
     */
497
    public static function addMetaLinkHrefLang(string $uri = null, int $siteId = null)
498
    {
499
        $siteLocalizedUrls = self::getLocalizedUrls($uri, $siteId);
500
501
        if (!empty($siteLocalizedUrls)) {
502
            // Add the rel=alternate tag
503
            $metaTag = Seomatic::$plugin->link->create([
504
                'rel' => 'alternate',
505
                'hreflang' => [],
506
                'href' => [],
507
            ]);
508
            // Add the alternate language link rel's
509
            if (\count($siteLocalizedUrls) > 1) {
510
                foreach ($siteLocalizedUrls as $siteLocalizedUrl) {
511
                    $metaTag->hreflang[] = $siteLocalizedUrl['hreflangLanguage'];
512
                    $metaTag->href[] = $siteLocalizedUrl['url'];
513
                    // Add the x-default hreflang
514
                    if ($siteLocalizedUrl['primary'] && Seomatic::$settings->addXDefaultHrefLang) {
515
                        $metaTag->hreflang[] = 'x-default';
516
                        $metaTag->href[] = $siteLocalizedUrl['url'];
517
518
                    }
519
                }
520
                Seomatic::$plugin->link->add($metaTag);
521
            }
522
            // Add in the og:locale:alternate tags
523
            $ogLocaleAlternate = Seomatic::$plugin->tag->get('og:locale:alternate');
524
            if (\count($siteLocalizedUrls) > 1 && $ogLocaleAlternate) {
525
                $ogContentArray = [];
526
                foreach ($siteLocalizedUrls as $siteLocalizedUrl) {
527
                    if (!\in_array($siteLocalizedUrl['ogLanguage'], $ogContentArray, true) &&
528
                    Craft::$app->language !== $siteLocalizedUrl['language']) {
0 ignored issues
show
Coding Style introduced by
Multi-line IF statement not indented correctly; expected 24 spaces but found 20
Loading history...
Coding Style introduced by
Each line in a multi-line IF statement must begin with a boolean operator
Loading history...
Coding Style introduced by
Closing parenthesis of a multi-line IF statement must be on a new line
Loading history...
529
                        $ogContentArray[] = $siteLocalizedUrl['ogLanguage'];
530
                    }
531
                }
532
                $ogLocaleAlternate->content = $ogContentArray;
533
            }
534
        }
535
    }
536
537
    /**
538
     * Add the Same As meta tags and JSON-LD
539
     */
540
    public static function addSameAsMeta()
541
    {
542
        $metaContainers = Seomatic::$plugin->metaContainers;
543
        $sameAsUrls = [];
544
        if (!empty($metaContainers->metaSiteVars->sameAsLinks)) {
545
            $sameAsUrls = ArrayHelper::getColumn($metaContainers->metaSiteVars->sameAsLinks, 'url', false);
546
            $sameAsUrls = array_values(array_filter($sameAsUrls));
547
        }
548
        // Facebook OpenGraph
549
        $ogSeeAlso = Seomatic::$plugin->tag->get('og:see_also');
550
        if ($ogSeeAlso) {
0 ignored issues
show
introduced by
$ogSeeAlso is of type nystudio107\seomatic\models\MetaTag, thus it always evaluated to true.
Loading history...
551
            $ogSeeAlso->content = $sameAsUrls;
552
        }
553
        // Site Identity JSON-LD
554
        $identity = Seomatic::$plugin->jsonLd->get('identity');
555
        /** @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...
556
        if ($identity !== null && property_exists($identity, 'sameAs')) {
557
            $identity->sameAs = $sameAsUrls;
558
        }
559
    }
560
561
    /**
562
     * Return a list of localized URLs that are in the current site's group
563
     * The current URI is used if $uri is null. Similarly, the current site is
564
     * used if $siteId is null.
565
     * The resulting array of arrays has `id`, `language`, `ogLanguage`,
566
     * `hreflangLanguage`, and `url` as keys.
567
     *
568
     * @param string|null $uri
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
569
     * @param int|null    $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
570
     *
571
     * @return array
572
     */
573
    public static function getLocalizedUrls(string $uri = null, int $siteId = null): array
574
    {
575
        $localizedUrls = [];
576
        // No pagination params for URLs
577
        $urlParams = null;
578
        // Get the request URI
579
        if ($uri === null) {
580
            $requestUri = Craft::$app->getRequest()->pathInfo;
581
        } else {
582
            $requestUri = $uri;
583
        }
584
        // Get the site to use
585
        if ($siteId === null) {
586
            try {
587
                $thisSite = Craft::$app->getSites()->getCurrentSite();
588
            } catch (SiteNotFoundException $e) {
589
                $thisSite = null;
590
                Craft::error($e->getMessage(), __METHOD__);
591
            }
592
        } else {
593
            $thisSite = Craft::$app->getSites()->getSiteById($siteId);
594
        }
595
        // Bail if we can't get a site
596
        if ($thisSite === null) {
597
            return $localizedUrls;
598
        }
599
        if (Seomatic::$settings->siteGroupsSeparate) {
600
            // Get only the sites that are in the current site's group
601
            try {
602
                $siteGroup = $thisSite->getGroup();
603
            } catch (InvalidConfigException $e) {
604
                $siteGroup = null;
605
                Craft::error($e->getMessage(), __METHOD__);
606
            }
607
            // Bail if we can't get a site group
608
            if ($siteGroup === null) {
609
                return $localizedUrls;
610
            }
611
            $sites = $siteGroup->getSites();
612
        } else {
613
            $sites = Craft::$app->getSites()->getAllSites();
614
        }
615
        $elements = Craft::$app->getElements();
616
        foreach ($sites as $site) {
617
            $includeUrl = true;
618
            $matchedElement = $elements->getElementByUri($requestUri, $thisSite->id, true);
619
            if ($matchedElement) {
620
                $url = $elements->getElementUriForSite($matchedElement->getId(), $site->id);
621
                // See if they have disabled sitemaps or robots for this entry,
622
                // and if so, don't include it in the hreflang
623
                /** @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...
624
                $element = null;
625
                if ($url) {
626
                    $element = $elements->getElementByUri($url, $site->id, false);
627
                }
628
                if ($element !== null) {
629
                    if (isset($element->enabledForSite) && !(bool)$element->enabledForSite) {
630
                        $includeUrl = false;
631
                    }
632
                    /** @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...
633
                    list($sourceId, $sourceBundleType, $sourceHandle, $sourceSiteId, $typeId)
634
                        = Seomatic::$plugin->metaBundles->getMetaSourceFromElement($element);
635
                    $metaBundle = Seomatic::$plugin->metaBundles->getMetaBundleBySourceId(
636
                        $sourceBundleType,
637
                        $sourceId,
638
                        $sourceSiteId
639
                    );
640
                    if ($metaBundle !== null) {
641
                        // If sitemaps are off for this entry, don't include the URL
642
                        if (!$metaBundle->metaSitemapVars->sitemapUrls) {
643
                            $includeUrl = false;
644
                        }
645
                        // If robots is set tp 'none' don't include the URL
646
                        if ($metaBundle->metaGlobalVars->robots === 'none' || $metaBundle->metaGlobalVars->robots === 'noindex') {
647
                            $includeUrl = false;
648
                        }
649
                    }
650
                    $fieldHandles = FieldHelper::fieldsOfTypeFromElement(
651
                        $element,
652
                        FieldHelper::SEO_SETTINGS_CLASS_KEY,
653
                        true
654
                    );
655
                    foreach ($fieldHandles as $fieldHandle) {
656
                        if (!empty($element->$fieldHandle)) {
657
                            /** @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...
658
                            $fieldMetaBundle = $element->$fieldHandle;
659
                            /** @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...
660
                            $seoSettingsField = Craft::$app->getFields()->getFieldByHandle($fieldHandle);
661
                            if ($fieldMetaBundle !== null && $seoSettingsField !== null && $seoSettingsField->sitemapTabEnabled) {
662
                                // If sitemaps are off for this entry, don't include the URL
663
                                if (\in_array('sitemapUrls', $seoSettingsField->sitemapEnabledFields, false)
664
                                    && !$fieldMetaBundle->metaSitemapVars->sitemapUrls
665
                                ) {
666
                                    $includeUrl = false;
667
                                }
668
                                // If robots is set to 'none' don't include the URL
669
                                if ($fieldMetaBundle->metaGlobalVars->robots === 'none' || $fieldMetaBundle->metaGlobalVars->robots === 'noindex') {
670
                                    $includeUrl = false;
671
                                }
672
                            }
673
                        }
674
                    }
675
                } else {
676
                    $includeUrl = false;
677
                }
678
                $url = ($url === '__home__') ? '' : $url;
679
            } else {
680
                try {
681
                    $url = $site->hasUrls ? UrlHelper::siteUrl($requestUri, $urlParams, null, $site->id)
682
                        : Craft::$app->getSites()->getPrimarySite()->baseUrl;
683
                } catch (SiteNotFoundException $e) {
684
                    $url = '';
685
                    Craft::error($e->getMessage(), __METHOD__);
686
                } catch (Exception $e) {
687
                    $url = '';
688
                    Craft::error($e->getMessage(), __METHOD__);
689
                }
690
            }
691
            $url = $url ?? '';
692
            if (!UrlHelper::isAbsoluteUrl($url)) {
693
                try {
694
                    $url = UrlHelper::siteUrl($url, $urlParams, null, $site->id);
695
                } catch (Exception $e) {
696
                    $url = '';
697
                    Craft::error($e->getMessage(), __METHOD__);
698
                }
699
            }
700
            // Strip any query string params, and make sure we have an absolute URL with protocol
701
            if ($urlParams === null) {
702
                $url = UrlHelper::stripQueryString($url);
703
            }
704
            $url = UrlHelper::absoluteUrlWithProtocol($url);
705
706
            $url = $url ?? '';
707
            $url = self::sanitizeUrl($url);
708
            $language = $site->language;
709
            $ogLanguage = LocalizationHelper::normalizeOgLocaleLanguage($language);
710
            $hreflangLanguage = $language;
711
            $hreflangLanguage = strtolower($hreflangLanguage);
712
            $hreflangLanguage = str_replace('_', '-', $hreflangLanguage);
713
            if ($includeUrl) {
714
                $localizedUrls[] = [
715
                    'id' => $site->id,
716
                    'language' => $language,
717
                    'ogLanguage' => $ogLanguage,
718
                    'hreflangLanguage' => $hreflangLanguage,
719
                    'url' => $url,
720
                    'primary' => $site->primary,
721
                ];
722
            }
723
        }
724
725
        return $localizedUrls;
726
    }
727
728
    /**
729
     * Normalize the array of opening hours passed in
730
     *
731
     * @param $value
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
732
     */
733
    public static function normalizeTimes(&$value)
734
    {
735
        if (\is_string($value)) {
736
            $value = Json::decode($value);
737
        }
738
        $normalized = [];
739
        $times = ['open', 'close'];
740
        for ($day = 0; $day <= 6; $day++) {
741
            foreach ($times as $time) {
742
                if (isset($value[$day][$time])
743
                    && ($date = DateTimeHelper::toDateTime($value[$day][$time])) !== false
744
                ) {
745
                    $normalized[$day][$time] = (array)($date);
746
                } else {
747
                    $normalized[$day][$time] = null;
748
                }
749
            }
750
        }
751
752
        $value = $normalized;
753
    }
754
}
755