Passed
Push — develop ( d6387f...a70265 )
by Andrew
09:10
created

DynamicMeta   F

Complexity

Total Complexity 143

Size/Duplication

Total Lines 725
Duplicated Lines 0 %

Test Coverage

Coverage 1.3%

Importance

Changes 12
Bugs 0 Features 0
Metric Value
eloc 390
c 12
b 0
f 0
dl 0
loc 725
ccs 5
cts 386
cp 0.013
rs 2
wmc 143

14 Methods

Rating   Name   Duplication   Size   Complexity  
A sanitizeUrl() 0 12 3
B addDynamicMetaToContainers() 0 33 6
A addCspHeaders() 0 6 2
F addMetaJsonLdBreadCrumbs() 0 97 19
A addSameAsMeta() 0 18 5
A addCspTags() 0 6 2
F getLocalizedUrls() 0 153 35
B addMetaLinkHrefLang() 0 36 11
B paginate() 0 43 10
A normalizeTimes() 0 20 6
A addContactPoints() 0 17 6
C addOpeningHours() 0 45 12
F includeHttpHeaders() 0 75 21
A getCspNonces() 0 22 5

How to fix   Complexity   

Complex Class

Complex classes like DynamicMeta often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DynamicMeta, and based on these observations, apply Extract Interface, too.

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
        if ($jsonLd instanceof LocalBusiness && $entity !== null) {
334
            /** @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...
335
            $openingHours = [];
336
            $days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
337
            $times = $entity->localBusinessOpeningHours;
338
            $index = 0;
339
            foreach ($times as $hours) {
340
                $openTime = '';
341
                $closeTime = '';
342
                if (!empty($hours['open'])) {
343
                    /** @var \DateTime $dateTime */
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...
344
                    try {
345
                        $dateTime = DateTimeHelper::toDateTime($hours['open']['date'], false, false);
346
                    } catch (\Exception $e) {
347
                        $dateTime = false;
348
                    }
349
                    if ($dateTime !== false) {
350
                        $openTime = $dateTime->format('H:i:s');
351
                    }
352
                }
353
                if (!empty($hours['close'])) {
354
                    /** @var \DateTime $dateTime */
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...
355
                    try {
356
                        $dateTime = DateTimeHelper::toDateTime($hours['close']['date'], false, false);
357
                    } catch (\Exception $e) {
358
                        $dateTime = false;
359
                    }
360
                    if ($dateTime !== false) {
361
                        $closeTime = $dateTime->format('H:i:s');
362
                    }
363
                }
364
                if ($openTime && $closeTime) {
365
                    $hours = Seomatic::$plugin->jsonLd->create([
366
                        'type' => 'OpeningHoursSpecification',
367
                        'opens' => $openTime,
368
                        'closes' => $closeTime,
369
                        'dayOfWeek' => [$days[$index]],
370
                    ], false);
371
                    $openingHours[] = $hours;
372
                }
373
                $index++;
374
            }
375
            $jsonLd->openingHoursSpecification = $openingHours;
376
        }
377
    }
378
379
    /**
380
     * Add the ContactPoint to the $jsonLd based on the Entity settings
381
     *
382
     * @param MetaJsonLd $jsonLd
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
383
     * @param Entity     $entity
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
384
     */
385
    public static function addContactPoints(MetaJsonLd $jsonLd, Entity $entity)
386
    {
387
        if ($jsonLd instanceof Organization && $entity !== null) {
388
            /** @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...
389
            $contactPoints = [];
390
            if ($entity->organizationContactPoints !== null && \is_array($entity->organizationContactPoints)) {
391
                foreach ($entity->organizationContactPoints as $contacts) {
392
                    /** @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...
393
                    $contact = Seomatic::$plugin->jsonLd->create([
394
                        'type' => 'ContactPoint',
395
                        'telephone' => $contacts['telephone'],
396
                        'contactType' => $contacts['contactType'],
397
                    ], false);
398
                    $contactPoints[] = $contact;
399
                }
400
            }
401
            $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...
402
        }
403
    }
404
405
    /**
406
     * Add breadcrumbs to the MetaJsonLdContainer
407
     *
408
     * @param int|null $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
409
     */
410
    public static function addMetaJsonLdBreadCrumbs(int $siteId = null)
411
    {
412
        $position = 1;
413
        if ($siteId === null) {
414
            $siteId = Craft::$app->getSites()->currentSite->id
415
                ?? Craft::$app->getSites()->primarySite->id
416
                ?? 1;
417
        }
418
        $site = Craft::$app->getSites()->getSiteById($siteId);
419
        if ($site === null) {
420
            return;
421
        }
422
        try {
423
            $siteUrl = $site->hasUrls ? $site->baseUrl : Craft::$app->getSites()->getPrimarySite()->baseUrl;
424
        } catch (SiteNotFoundException $e) {
425
            $siteUrl = Craft::$app->getConfig()->general->siteUrl;
426
            Craft::error($e->getMessage(), __METHOD__);
427
        }
428
        if (!empty(Seomatic::$settings->siteUrlOverride)) {
429
            $siteUrl = Seomatic::$settings->siteUrlOverride;
430
        }
431
        $siteUrl = $siteUrl ?: '/';
432
        /** @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...
433
        $crumbs = Seomatic::$plugin->jsonLd->create([
434
            'type' => 'BreadcrumbList',
435
            'name' => 'Breadcrumbs',
436
            'description' => 'Breadcrumbs list',
437
        ]);
438
        /** @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...
439
        $element = Craft::$app->getElements()->getElementByUri('__home__', $siteId);
440
        if ($element) {
0 ignored issues
show
introduced by
$element is of type craft\base\Element, thus it always evaluated to true.
Loading history...
441
            $uri = $element->uri === '__home__' ? '' : ($element->uri ?? '');
442
            try {
443
                $id = UrlHelper::siteUrl($uri, null, null, $siteId);
444
            } catch (Exception $e) {
445
                $id = $siteUrl;
446
                Craft::error($e->getMessage(), __METHOD__);
447
            }
448
            $item = UrlHelper::stripQueryString($id);
449
            $item = UrlHelper::absoluteUrlWithProtocol($item);
450
            $listItem = MetaJsonLd::create('ListItem', [
451
                'position' => $position,
452
                'name' => $element->title,
453
                'item' => $item,
454
                '@id' => $id,
455
            ]);
456
            $crumbs->itemListElement[] = $listItem;
457
        } else {
458
            $item = UrlHelper::stripQueryString($siteUrl ?? '/');
459
            $item = UrlHelper::absoluteUrlWithProtocol($item);
460
            $crumbs->itemListElement[] = MetaJsonLd::create('ListItem', [
461
                'position' => $position,
462
                'name' => 'Homepage',
463
                'item' => $item,
464
                '@id' => $siteUrl,
465
            ]);
466
        }
467
        // Build up the segments, and look for elements that match
468
        $uri = '';
469
        $segments = Craft::$app->getRequest()->getSegments();
470
        /** @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...
471
        $lastElement = Seomatic::$matchedElement;
472
        if ($lastElement && $element) {
0 ignored issues
show
introduced by
$element is of type craft\base\Element, thus it always evaluated to true.
Loading history...
473
            if ($lastElement->uri !== '__home__' && $element->uri) {
474
                $path = $lastElement->uri;
475
                $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

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