Passed
Push — v3 ( e59c9d...9ad5d8 )
by Andrew
48:27 queued 25:26
created

DynamicMeta::addMetaLinkHrefLang()   C

Complexity

Conditions 13
Paths 10

Size

Total Lines 47
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 182

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 13
eloc 30
c 4
b 0
f 0
nc 10
nop 2
dl 0
loc 47
ccs 0
cts 29
cp 0
crap 182
rs 6.6166

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * 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 paginated URL
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
                $canonical = Seomatic::$seomaticVariable->link->get('canonical');
112
                if ($canonical !== null) {
113
                    $canonical->href = $url;
114
                }
115
            }
116
            // See if we should strip the query params
117
            $stripQueryParams = true;
118
            $pageTrigger = Craft::$app->getConfig()->getGeneral()->pageTrigger;
119
            // Is this query string-based pagination?
120
            if ($pageTrigger[0] === '?') {
121
                $stripQueryParams = false;
122
            }
123
124
            // Set the previous URL
125
            $url = $pageInfo->getPrevUrl();
126
            if ($stripQueryParams) {
127
                $url = preg_replace('/\?.*/', '', $url);
128
            }
129
            if (!empty($url)) {
130
                $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...
131
                    'rel' => 'prev',
132
                    'href' => $url,
133
                ]);
134
            }
135
            // Set the next URL
136
            $url = $pageInfo->getNextUrl();
137
            if ($stripQueryParams) {
138
                $url = preg_replace('/\?.*/', '', $url);
139
            }
140
            if (!empty($url)) {
141
                $metaTag = Seomatic::$plugin->link->create([
142
                    'rel' => 'next',
143
                    'href' => $url,
144
                ]);
145
            }
146
        }
147
    }
148
149
    /**
150
     * Include any headers for this request
151
     */
152
    public static function includeHttpHeaders()
153
    {
154
        self::addCspHeaders();
155
        // Don't include headers for any response code >= 400
156
        $request = Craft::$app->getRequest();
157
        if (!$request->isConsoleRequest) {
158
            $response = Craft::$app->getResponse();
159
            if ($response->statusCode >= 400 || SeomaticHelper::isPreview()) {
160
                return;
161
            }
162
        }
163
        // Assuming they have headersEnabled, add the response code to the headers
164
        if (Seomatic::$settings->headersEnabled) {
165
            $response = Craft::$app->getResponse();
166
            // X-Robots-Tag header
167
            $robots = Seomatic::$seomaticVariable->tag->get('robots');
168
            if ($robots !== null && $robots->include) {
169
                $robotsArray = $robots->renderAttributes();
170
                $content = $robotsArray['content'] ?? '';
171
                if (!empty($content)) {
172
                    // The content property can be a string or an array
173
                    if (\is_array($content)) {
174
                        $headerValue = '';
175
                        foreach ($content as $contentVal) {
176
                            $headerValue .= ($contentVal.',');
177
                        }
178
                        $headerValue = rtrim($headerValue, ',');
179
                    } else {
180
                        $headerValue = $content;
181
                    }
182
                    $response->headers->set('X-Robots-Tag', $headerValue);
183
                }
184
            }
185
            // Link canonical header
186
            $canonical = Seomatic::$seomaticVariable->link->get('canonical');
187
            if ($canonical !== null && $canonical->include) {
188
                $canonicalArray = $canonical->renderAttributes();
189
                $href = $canonicalArray['href'] ?? '';
190
                if (!empty($href)) {
191
                    // The href property can be a string or an array
192
                    if (\is_array($href)) {
193
                        $headerValue = '';
194
                        foreach ($href as $hrefVal) {
195
                            $headerValue .= ('<'.$hrefVal.'>'.',');
196
                        }
197
                        $headerValue = rtrim($headerValue, ',');
198
                    } else {
199
                        $headerValue = '<'.$href.'>';
200
                    }
201
                    $headerValue .= "; rel='canonical'";
202
                    $response->headers->add('Link', $headerValue);
203
                }
204
            }
205
            // Referrer-Policy header
206
            $referrer = Seomatic::$seomaticVariable->tag->get('referrer');
207
            if ($referrer !== null && $referrer->include) {
208
                $referrerArray = $referrer->renderAttributes();
209
                $content = $referrerArray['content'] ?? '';
210
                if (!empty($content)) {
211
                    // The content property can be a string or an array
212
                    if (\is_array($content)) {
213
                        $headerValue = '';
214
                        foreach ($content as $contentVal) {
215
                            $headerValue .= ($contentVal.',');
216
                        }
217
                        $headerValue = rtrim($headerValue, ',');
218
                    } else {
219
                        $headerValue = $content;
220
                    }
221
                    $response->headers->add('Referrer-Policy', $headerValue);
222
                }
223
            }
224
            // The X-Powered-By tag
225
            if (Seomatic::$settings->generatorEnabled) {
226
                $response->headers->add('X-Powered-By', 'SEOmatic');
227
            }
228
        }
229
    }
230
231
    /**
232
     * Get all of the CSP Nonces from containers that can have them
233
     *
234
     * @return array
235
     */
236
    public static function getCspNonces(): array
237
    {
238
        $cspNonces = [];
239
        // Add in any fixed policies from Settings
240
        if (!empty(Seomatic::$settings->cspScriptSrcPolicies)) {
241
            $fixedCsps = Seomatic::$settings->cspScriptSrcPolicies;
242
            $iterator = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($fixedCsps));
243
            foreach($iterator as $value) {
0 ignored issues
show
Coding Style introduced by
Expected "foreach (...) {\n"; found "foreach(...) {\n"
Loading history...
244
                $cspNonces[] = $value;
245
            }
246
        }
247
        // Add in any CSP nonce headers
248
        $container = Seomatic::$plugin->jsonLd->container();
249
        if ($container !== null) {
250
            $cspNonces = array_merge($cspNonces, $container->getCspNonces());
251
        }
252
        $container = Seomatic::$plugin->script->container();
253
        if ($container !== null) {
254
            $cspNonces = array_merge($cspNonces, $container->getCspNonces());
255
        }
256
257
        return $cspNonces;
258
    }
259
260
    /**
261
     * Add the Content-Security-Policy script-src headers
262
     */
263
    public static function addCspHeaders()
264
    {
265
        $cspNonces = self::getCspNonces();
266
        $container = Seomatic::$plugin->script->container();
267
        if ($container !== null) {
268
            $container->addNonceHeaders($cspNonces);
269
        }
270
    }
271
272
    /**
273
     * Add the Content-Security-Policy script-src tags
274
     */
275
    public static function addCspTags()
276
    {
277
        $cspNonces = self::getCspNonces();
278
        $container = Seomatic::$plugin->script->container();
279
        if ($container !== null) {
280
            $container->addNonceTags($cspNonces);
281
        }
282
    }
283
284
    /**
285
     * Add any custom/dynamic meta to the containers
286
     *
287
     * @param string|null $uri     The URI of the route to add dynamic metadata for
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces after parameter name; 5 found
Loading history...
288
     * @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...
289
     */
290
    public static function addDynamicMetaToContainers(string $uri = null, int $siteId = null)
291
    {
292
        Craft::beginProfile('DynamicMeta::addDynamicMetaToContainers', __METHOD__);
293
        $request = Craft::$app->getRequest();
294
        // Don't add dynamic meta to console requests, they have no concept of a URI or segments
295
        if (!$request->getIsConsoleRequest()) {
296
            $response = Craft::$app->getResponse();
297
            if ($response->statusCode < 400) {
298
                self::addMetaJsonLdBreadCrumbs($siteId);
299
                if (Seomatic::$settings->addHrefLang) {
300
                    self::addMetaLinkHrefLang($uri, $siteId);
301
                }
302
                self::addSameAsMeta();
303
                $metaSiteVars = Seomatic::$plugin->metaContainers->metaSiteVars;
304
                $jsonLd = Seomatic::$plugin->jsonLd->get('identity');
305
                if ($jsonLd !== null) {
306
                    self::addOpeningHours($jsonLd, $metaSiteVars->identity);
307
                    self::addContactPoints($jsonLd, $metaSiteVars->identity);
308
                }
309
                $jsonLd = Seomatic::$plugin->jsonLd->get('creator');
310
                if ($jsonLd !== null) {
311
                    self::addOpeningHours($jsonLd, $metaSiteVars->creator);
312
                    self::addContactPoints($jsonLd, $metaSiteVars->creator);
313
                }
314
                // Allow modules/plugins a chance to add dynamic meta
315
                $event = new AddDynamicMetaEvent([
316
                    'uri' => $uri,
317
                    'siteId' => $siteId,
318
                ]);
319
                Event::trigger(static::class, self::EVENT_ADD_DYNAMIC_META, $event);
320
            }
321
        }
322
        Craft::endProfile('DynamicMeta::addDynamicMetaToContainers', __METHOD__);
323
    }
324
325
    /**
326
     * Add the OpeningHoursSpecific to the $jsonLd based on the Entity settings
327
     *
328
     * @param MetaJsonLd $jsonLd
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
329
     * @param Entity     $entity
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
330
     */
331
    public static function addOpeningHours(MetaJsonLd $jsonLd, Entity $entity)
332
    {
333
        Craft::beginProfile('DynamicMeta::addOpeningHours', __METHOD__);
334
        if ($jsonLd instanceof LocalBusiness && $entity !== null) {
335
            /** @var LocalBusiness $jsonLd */
0 ignored issues
show
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
336
            $openingHours = [];
337
            $days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
338
            $times = $entity->localBusinessOpeningHours;
339
            $index = 0;
340
            foreach ($times as $hours) {
341
                $openTime = '';
342
                $closeTime = '';
343
                if (!empty($hours['open'])) {
344
                    /** @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...
345
                    try {
346
                        $dateTime = DateTimeHelper::toDateTime($hours['open']['date'], false, false);
347
                    } catch (\Exception $e) {
348
                        $dateTime = false;
349
                    }
350
                    if ($dateTime !== false) {
351
                        $openTime = $dateTime->format('H:i:s');
352
                    }
353
                }
354
                if (!empty($hours['close'])) {
355
                    /** @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...
356
                    try {
357
                        $dateTime = DateTimeHelper::toDateTime($hours['close']['date'], false, false);
358
                    } catch (\Exception $e) {
359
                        $dateTime = false;
360
                    }
361
                    if ($dateTime !== false) {
362
                        $closeTime = $dateTime->format('H:i:s');
363
                    }
364
                }
365
                if ($openTime && $closeTime) {
366
                    $hours = Seomatic::$plugin->jsonLd->create([
367
                        'type' => 'OpeningHoursSpecification',
368
                        'opens' => $openTime,
369
                        'closes' => $closeTime,
370
                        'dayOfWeek' => [$days[$index]],
371
                    ], false);
372
                    $openingHours[] = $hours;
373
                }
374
                $index++;
375
            }
376
            $jsonLd->openingHoursSpecification = $openingHours;
377
        }
378
        Craft::endProfile('DynamicMeta::addOpeningHours', __METHOD__);
379
    }
380
381
    /**
382
     * Add the ContactPoint to the $jsonLd based on the Entity settings
383
     *
384
     * @param MetaJsonLd $jsonLd
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
385
     * @param Entity     $entity
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
386
     */
387
    public static function addContactPoints(MetaJsonLd $jsonLd, Entity $entity)
388
    {
389
        Craft::beginProfile('DynamicMeta::addContactPoints', __METHOD__);
390
        if ($jsonLd instanceof Organization && $entity !== null) {
391
            /** @var Organization $jsonLd */
0 ignored issues
show
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
392
            $contactPoints = [];
393
            if ($entity->organizationContactPoints !== null && \is_array($entity->organizationContactPoints)) {
394
                foreach ($entity->organizationContactPoints as $contacts) {
395
                    /** @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...
396
                    $contact = Seomatic::$plugin->jsonLd->create([
397
                        'type' => 'ContactPoint',
398
                        'telephone' => $contacts['telephone'],
399
                        'contactType' => $contacts['contactType'],
400
                    ], false);
401
                    $contactPoints[] = $contact;
402
                }
403
            }
404
            $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...
405
        }
406
        Craft::endProfile('DynamicMeta::addContactPoints', __METHOD__);
407
    }
408
409
    /**
410
     * Add breadcrumbs to the MetaJsonLdContainer
411
     *
412
     * @param int|null $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
413
     */
414
    public static function addMetaJsonLdBreadCrumbs(int $siteId = null)
415
    {
416
        Craft::beginProfile('DynamicMeta::addMetaJsonLdBreadCrumbs', __METHOD__);
417
        $position = 0;
418
        if ($siteId === null) {
419
            $siteId = Craft::$app->getSites()->currentSite->id
420
                ?? Craft::$app->getSites()->primarySite->id
421
                ?? 1;
422
        }
423
        $site = Craft::$app->getSites()->getSiteById($siteId);
424
        if ($site === null) {
425
            return;
426
        }
427
        try {
428
            $siteUrl = $site->hasUrls ? $site->baseUrl : Craft::$app->getSites()->getPrimarySite()->baseUrl;
429
        } catch (SiteNotFoundException $e) {
430
            $siteUrl = Craft::$app->getConfig()->general->siteUrl;
431
            Craft::error($e->getMessage(), __METHOD__);
432
        }
433
        if (!empty(Seomatic::$settings->siteUrlOverride)) {
434
            $siteUrl = Seomatic::$settings->siteUrlOverride;
435
        }
436
        $siteUrl = $siteUrl ?: '/';
437
        /** @var  $crumbs BreadcrumbList */
0 ignored issues
show
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
438
        $crumbs = Seomatic::$plugin->jsonLd->create([
439
            'type' => 'BreadcrumbList',
440
            'name' => 'Breadcrumbs',
441
            'description' => 'Breadcrumbs list',
442
        ], false);
443
        // Include the Homepage in the breadcrumbs, if includeHomepageInBreadcrumbs is true
444
        $element = null;
445
        if (Seomatic::$settings->includeHomepageInBreadcrumbs) {
446
            /** @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...
447
            $position++;
448
            $element = Craft::$app->getElements()->getElementByUri('__home__', $siteId);
449
            if ($element) {
450
                $uri = $element->uri === '__home__' ? '' : ($element->uri ?? '');
451
                try {
452
                    $id = UrlHelper::siteUrl($uri, null, null, $siteId);
453
                } catch (Exception $e) {
454
                    $id = $siteUrl;
455
                    Craft::error($e->getMessage(), __METHOD__);
456
                }
457
                $item = UrlHelper::stripQueryString($id);
458
                $item = UrlHelper::absoluteUrlWithProtocol($item);
459
                $listItem = MetaJsonLd::create('ListItem', [
460
                    'position' => $position,
461
                    'name' => $element->title,
462
                    'item' => $item,
463
                    '@id' => $id,
464
                ]);
465
                $crumbs->itemListElement[] = $listItem;
466
            } else {
467
                $item = UrlHelper::stripQueryString($siteUrl ?? '/');
468
                $item = UrlHelper::absoluteUrlWithProtocol($item);
469
                $crumbs->itemListElement[] = MetaJsonLd::create('ListItem', [
470
                    'position' => $position,
471
                    'name' => 'Homepage',
472
                    'item' => $item,
473
                    '@id' => $siteUrl,
474
                ]);
475
            }
476
        }
477
        // Build up the segments, and look for elements that match
478
        $uri = '';
479
        $segments = Craft::$app->getRequest()->getSegments();
480
        /** @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...
481
        $lastElement = Seomatic::$matchedElement;
482
        if ($lastElement && $element) {
483
            if ($lastElement->uri !== '__home__' && $element->uri) {
484
                $path = $lastElement->uri;
485
                $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

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