Passed
Push — develop ( b326d0...aebf5c )
by Andrew
09:04
created

DynamicMeta::paginate()   B

Complexity

Conditions 7
Paths 13

Size

Total Lines 29
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

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

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