Passed
Push — v3 ( 9618fe...01dae8 )
by Andrew
47:57 queued 20:05
created

DynamicMeta::addContactPoints()   A

Complexity

Conditions 6
Paths 3

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

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