Passed
Push — v3 ( e36664...83357b )
by Andrew
34:40 queued 23:37
created

DynamicMeta::addOpeningHours()   C

Complexity

Conditions 12
Paths 2

Size

Total Lines 48
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 156

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 12
eloc 34
c 1
b 0
f 0
nc 2
nop 2
dl 0
loc 48
ccs 0
cts 33
cp 0
crap 156
rs 6.9666

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/**
3
 * SEOmatic plugin for Craft CMS 3.x
4
 *
5
 * A turnkey SEO implementation for Craft CMS that is comprehensive, powerful,
6
 * and flexible
7
 *
8
 * @link      https://nystudio107.com
9
 * @copyright Copyright (c) 2017 nystudio107
10
 */
11
12
namespace nystudio107\seomatic\helpers;
13
14
use nystudio107\seomatic\Seomatic;
15
use nystudio107\seomatic\events\AddDynamicMetaEvent;
16
use nystudio107\seomatic\fields\SeoSettings;
17
use nystudio107\seomatic\helpers\Field as FieldHelper;
18
use nystudio107\seomatic\helpers\Text as TextHelper;
19
use nystudio107\seomatic\helpers\Localization as LocalizationHelper;
20
use nystudio107\seomatic\models\Entity;
21
use nystudio107\seomatic\models\jsonld\ContactPoint;
22
use nystudio107\seomatic\models\jsonld\LocalBusiness;
23
use nystudio107\seomatic\models\jsonld\Organization;
24
use nystudio107\seomatic\models\jsonld\BreadcrumbList;
25
use nystudio107\seomatic\models\jsonld\Thing;
26
use nystudio107\seomatic\models\MetaBundle;
27
use nystudio107\seomatic\models\MetaJsonLd;
28
use nystudio107\seomatic\services\Helper as SeomaticHelper;
29
30
31
use Craft;
32
use craft\base\Element;
33
use craft\errors\SiteNotFoundException;
34
use craft\helpers\DateTimeHelper;
35
use craft\web\twig\variables\Paginate;
36
37
use yii\base\Event;
38
use yii\base\Exception;
39
use yii\base\InvalidConfigException;
40
41
/**
42
 * @author    nystudio107
0 ignored issues
show
Coding Style introduced by
The tag in position 1 should be the @package tag
Loading history...
Coding Style introduced by
Content of the @author tag must be in the form "Display Name <[email protected]>"
Loading history...
43
 * @package   Seomatic
44
 * @since     3.0.0
45
 */
46
class DynamicMeta
47
{
48
    // Constants
49
    // =========================================================================
50
51
    /**
52
     * @event AddDynamicMetaEvent The event that is triggered when SEOmatic has
53
     *        included the standard meta containers, and gives your plugin/module
54
     *        the chance to add whatever custom dynamic meta items you like
55
     *
56
     * ---
57
     * ```php
58
     * use nystudio107\seomatic\events\AddDynamicMetaEvent;
59
     * use nystudio107\seomatic\helpers\DynamicMeta;
60
     * use yii\base\Event;
61
     * Event::on(DynamicMeta::class, DynamicMeta::EVENT_ADD_DYNAMIC_META, function(AddDynamicMetaEvent $e) {
62
     *     // Add whatever dynamic meta items to the containers as you like
63
     * });
64
     * ```
65
     */
66
    const EVENT_ADD_DYNAMIC_META = 'addDynamicMeta';
67
68
    // Static Methods
69
    // =========================================================================
70
71
72
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $stripQueryString should have a doc-comment as per coding-style.
Loading history...
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, bool $stripQueryString = true): string
81
    {
82
        // Remove the query string
83 2
        if ($stripQueryString) {
84 2
            $url = UrlHelper::stripQueryString($url);
85
        }
86 2
        $url = TextHelper::sanitizeUserInput($url);
87
88
        // If this is a >= 400 status code, set the canonical URL to nothing
89 2
        if ($checkStatus && !Craft::$app->getRequest()->getIsConsoleRequest() && Craft::$app->getResponse()->statusCode >= 400) {
90
            $url = '';
91
        }
92
93 2
        return $url;
94
    }
95
96
    /**
97
     * Paginate based on the passed in Paginate variable as returned from the
98
     * Twig {% paginate %} tag:
99
     * https://docs.craftcms.com/v3/templating/tags/paginate.html#the-pageInfo-variable
100
     *
101
     * @param Paginate $pageInfo
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
102
     */
103
    public static function paginate(Paginate $pageInfo)
104
    {
105
        if ($pageInfo !== null && $pageInfo->currentPage !== null) {
106
            // Let the meta containers know that this page is paginated
107
            Seomatic::$plugin->metaContainers->paginationPage = (string)$pageInfo->currentPage;
108
            // Set the the canonical URL to be the paginated URL
109
            // see: https://github.com/nystudio107/craft-seomatic/issues/375#issuecomment-488369209
110
            $url = $pageInfo->getPageUrl($pageInfo->currentPage);
111
            if (!empty($url)) {
112
                Seomatic::$seomaticVariable->meta->canonicalUrl = $url;
113
                $canonical = Seomatic::$seomaticVariable->link->get('canonical');
114
                if ($canonical !== null) {
115
                    $canonical->href = $url;
116
                }
117
            }
118
            // See if we should strip the query params
119
            $stripQueryParams = true;
120
            $pageTrigger = Craft::$app->getConfig()->getGeneral()->pageTrigger;
121
            // Is this query string-based pagination?
122
            if ($pageTrigger[0] === '?') {
123
                $stripQueryParams = false;
124
            }
125
126
            // Set the previous URL
127
            $url = $pageInfo->getPrevUrl();
128
            if ($stripQueryParams) {
129
                $url = preg_replace('/\?.*/', '', $url);
130
            }
131
            if (!empty($url)) {
132
                $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...
133
                    'rel' => 'prev',
134
                    'href' => $url,
135
                ]);
136
            }
137
            // Set the next URL
138
            $url = $pageInfo->getNextUrl();
139
            if ($stripQueryParams) {
140
                $url = preg_replace('/\?.*/', '', $url);
141
            }
142
            if (!empty($url)) {
143
                $metaTag = Seomatic::$plugin->link->create([
144
                    'rel' => 'next',
145
                    'href' => $url,
146
                ]);
147
            }
148
        }
149
    }
150
151
    /**
152
     * Include any headers for this request
153
     */
154
    public static function includeHttpHeaders()
155
    {
156
        self::addCspHeaders();
157
        // Don't include headers for any response code >= 400
158
        $request = Craft::$app->getRequest();
159
        if (!$request->isConsoleRequest) {
160
            $response = Craft::$app->getResponse();
161
            if ($response->statusCode >= 400 || SeomaticHelper::isPreview()) {
162
                return;
163
            }
164
        }
165
        // Assuming they have headersEnabled, add the response code to the headers
166
        if (Seomatic::$settings->headersEnabled) {
167
            $response = Craft::$app->getResponse();
168
            // X-Robots-Tag header
169
            $robots = Seomatic::$seomaticVariable->tag->get('robots');
170
            if ($robots !== null && $robots->include) {
171
                $robotsArray = $robots->renderAttributes();
172
                $content = $robotsArray['content'] ?? '';
173
                if (!empty($content)) {
174
                    // The content property can be a string or an array
175
                    if (\is_array($content)) {
176
                        $headerValue = '';
177
                        foreach ($content as $contentVal) {
178
                            $headerValue .= ($contentVal.',');
179
                        }
180
                        $headerValue = rtrim($headerValue, ',');
181
                    } else {
182
                        $headerValue = $content;
183
                    }
184
                    $response->headers->set('X-Robots-Tag', $headerValue);
185
                }
186
            }
187
            // Link canonical header
188
            $canonical = Seomatic::$seomaticVariable->link->get('canonical');
189
            if ($canonical !== null && $canonical->include) {
190
                $canonicalArray = $canonical->renderAttributes();
191
                $href = $canonicalArray['href'] ?? '';
192
                if (!empty($href)) {
193
                    // The href property can be a string or an array
194
                    if (\is_array($href)) {
195
                        $headerValue = '';
196
                        foreach ($href as $hrefVal) {
197
                            $headerValue .= ('<'.$hrefVal.'>'.',');
198
                        }
199
                        $headerValue = rtrim($headerValue, ',');
200
                    } else {
201
                        $headerValue = '<'.$href.'>';
202
                    }
203
                    $headerValue .= "; rel='canonical'";
204
                    $response->headers->add('Link', $headerValue);
205
                }
206
            }
207
            // Referrer-Policy header
208
            $referrer = Seomatic::$seomaticVariable->tag->get('referrer');
209
            if ($referrer !== null && $referrer->include) {
210
                $referrerArray = $referrer->renderAttributes();
211
                $content = $referrerArray['content'] ?? '';
212
                if (!empty($content)) {
213
                    // The content property can be a string or an array
214
                    if (\is_array($content)) {
215
                        $headerValue = '';
216
                        foreach ($content as $contentVal) {
217
                            $headerValue .= ($contentVal.',');
218
                        }
219
                        $headerValue = rtrim($headerValue, ',');
220
                    } else {
221
                        $headerValue = $content;
222
                    }
223
                    $response->headers->add('Referrer-Policy', $headerValue);
224
                }
225
            }
226
            // The X-Powered-By tag
227
            if (Seomatic::$settings->generatorEnabled) {
228
                $response->headers->add('X-Powered-By', 'SEOmatic');
229
            }
230
        }
231
    }
232
233
    /**
234
     * Get all of the CSP Nonces from containers that can have them
235
     *
236
     * @return array
237
     */
238
    public static function getCspNonces(): array
239
    {
240
        $cspNonces = [];
241
        // Add in any fixed policies from Settings
242
        if (!empty(Seomatic::$settings->cspScriptSrcPolicies)) {
243
            $fixedCsps = Seomatic::$settings->cspScriptSrcPolicies;
244
            $iterator = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($fixedCsps));
245
            foreach($iterator as $value) {
0 ignored issues
show
Coding Style introduced by
Expected "foreach (...) {\n"; found "foreach(...) {\n"
Loading history...
246
                $cspNonces[] = $value;
247
            }
248
        }
249
        // Add in any CSP nonce headers
250
        $container = Seomatic::$plugin->jsonLd->container();
251
        if ($container !== null) {
252
            $cspNonces = array_merge($cspNonces, $container->getCspNonces());
253
        }
254
        $container = Seomatic::$plugin->script->container();
255
        if ($container !== null) {
256
            $cspNonces = array_merge($cspNonces, $container->getCspNonces());
257
        }
258
259
        return $cspNonces;
260
    }
261
262
    /**
263
     * Add the Content-Security-Policy script-src headers
264
     */
265
    public static function addCspHeaders()
266
    {
267
        $cspNonces = self::getCspNonces();
268
        $container = Seomatic::$plugin->script->container();
269
        if ($container !== null) {
270
            $container->addNonceHeaders($cspNonces);
271
        }
272
    }
273
274
    /**
275
     * Add the Content-Security-Policy script-src tags
276
     */
277
    public static function addCspTags()
278
    {
279
        $cspNonces = self::getCspNonces();
280
        $container = Seomatic::$plugin->script->container();
281
        if ($container !== null) {
282
            $container->addNonceTags($cspNonces);
283
        }
284
    }
285
286
    /**
287
     * Add any custom/dynamic meta to the containers
288
     *
289
     * @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...
290
     * @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...
291
     */
292
    public static function addDynamicMetaToContainers(string $uri = null, int $siteId = null)
293
    {
294
        Craft::beginProfile('DynamicMeta::addDynamicMetaToContainers', __METHOD__);
295
        $request = Craft::$app->getRequest();
296
        // Don't add dynamic meta to console requests, they have no concept of a URI or segments
297
        if (!$request->getIsConsoleRequest()) {
298
            $response = Craft::$app->getResponse();
299
            if ($response->statusCode < 400) {
300
                self::addMetaJsonLdBreadCrumbs($siteId);
301
                if (Seomatic::$settings->addHrefLang) {
302
                    self::addMetaLinkHrefLang($uri, $siteId);
303
                }
304
                self::addSameAsMeta();
305
                $metaSiteVars = Seomatic::$plugin->metaContainers->metaSiteVars;
306
                $jsonLd = Seomatic::$plugin->jsonLd->get('identity');
307
                if ($jsonLd !== null) {
308
                    self::addOpeningHours($jsonLd, $metaSiteVars->identity);
309
                    self::addContactPoints($jsonLd, $metaSiteVars->identity);
310
                }
311
                $jsonLd = Seomatic::$plugin->jsonLd->get('creator');
312
                if ($jsonLd !== null) {
313
                    self::addOpeningHours($jsonLd, $metaSiteVars->creator);
314
                    self::addContactPoints($jsonLd, $metaSiteVars->creator);
315
                }
316
                // Allow modules/plugins a chance to add dynamic meta
317
                $event = new AddDynamicMetaEvent([
318
                    'uri' => $uri,
319
                    'siteId' => $siteId,
320
                ]);
321
                Event::trigger(static::class, self::EVENT_ADD_DYNAMIC_META, $event);
322
            }
323
        }
324
        Craft::endProfile('DynamicMeta::addDynamicMetaToContainers', __METHOD__);
325
    }
326
327
    /**
328
     * Add the OpeningHoursSpecific to the $jsonLd based on the Entity settings
329
     *
330
     * @param MetaJsonLd $jsonLd
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
331
     * @param Entity     $entity
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
332
     */
333
    public static function addOpeningHours(MetaJsonLd $jsonLd, Entity $entity)
334
    {
335
        Craft::beginProfile('DynamicMeta::addOpeningHours', __METHOD__);
336
        if ($jsonLd instanceof LocalBusiness && $entity !== null) {
337
            /** @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...
338
            $openingHours = [];
339
            $days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
340
            $times = $entity->localBusinessOpeningHours;
341
            $index = 0;
342
            foreach ($times as $hours) {
343
                $openTime = '';
344
                $closeTime = '';
345
                if (!empty($hours['open'])) {
346
                    /** @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...
347
                    try {
348
                        $dateTime = DateTimeHelper::toDateTime($hours['open']['date'], false, false);
349
                    } catch (\Exception $e) {
350
                        $dateTime = false;
351
                    }
352
                    if ($dateTime !== false) {
353
                        $openTime = $dateTime->format('H:i:s');
354
                    }
355
                }
356
                if (!empty($hours['close'])) {
357
                    /** @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...
358
                    try {
359
                        $dateTime = DateTimeHelper::toDateTime($hours['close']['date'], false, false);
360
                    } catch (\Exception $e) {
361
                        $dateTime = false;
362
                    }
363
                    if ($dateTime !== false) {
364
                        $closeTime = $dateTime->format('H:i:s');
365
                    }
366
                }
367
                if ($openTime && $closeTime) {
368
                    $hours = Seomatic::$plugin->jsonLd->create([
369
                        'type' => 'OpeningHoursSpecification',
370
                        'opens' => $openTime,
371
                        'closes' => $closeTime,
372
                        'dayOfWeek' => [$days[$index]],
373
                    ], false);
374
                    $openingHours[] = $hours;
375
                }
376
                $index++;
377
            }
378
            $jsonLd->openingHoursSpecification = $openingHours;
379
        }
380
        Craft::endProfile('DynamicMeta::addOpeningHours', __METHOD__);
381
    }
382
383
    /**
384
     * Add the ContactPoint to the $jsonLd based on the Entity settings
385
     *
386
     * @param MetaJsonLd $jsonLd
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
387
     * @param Entity     $entity
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
388
     */
389
    public static function addContactPoints(MetaJsonLd $jsonLd, Entity $entity)
390
    {
391
        Craft::beginProfile('DynamicMeta::addContactPoints', __METHOD__);
392
        if ($jsonLd instanceof Organization && $entity !== null) {
393
            /** @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...
394
            $contactPoints = [];
395
            if ($entity->organizationContactPoints !== null && \is_array($entity->organizationContactPoints)) {
396
                foreach ($entity->organizationContactPoints as $contacts) {
397
                    /** @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...
398
                    $contact = Seomatic::$plugin->jsonLd->create([
399
                        'type' => 'ContactPoint',
400
                        'telephone' => $contacts['telephone'],
401
                        'contactType' => $contacts['contactType'],
402
                    ], false);
403
                    $contactPoints[] = $contact;
404
                }
405
            }
406
            $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...
407
        }
408
        Craft::endProfile('DynamicMeta::addContactPoints', __METHOD__);
409
    }
410
411
    /**
412
     * Add breadcrumbs to the MetaJsonLdContainer
413
     *
414
     * @param int|null $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
415
     */
416
    public static function addMetaJsonLdBreadCrumbs(int $siteId = null)
417
    {
418
        Craft::beginProfile('DynamicMeta::addMetaJsonLdBreadCrumbs', __METHOD__);
419
        $position = 0;
420
        if ($siteId === null) {
421
            $siteId = Craft::$app->getSites()->currentSite->id
422
                ?? Craft::$app->getSites()->primarySite->id
423
                ?? 1;
424
        }
425
        $site = Craft::$app->getSites()->getSiteById($siteId);
426
        if ($site === null) {
427
            return;
428
        }
429
        try {
430
            $siteUrl = $site->hasUrls ? $site->baseUrl : Craft::$app->getSites()->getPrimarySite()->baseUrl;
431
        } catch (SiteNotFoundException $e) {
432
            $siteUrl = Craft::$app->getConfig()->general->siteUrl;
433
            Craft::error($e->getMessage(), __METHOD__);
434
        }
435
        if (!empty(Seomatic::$settings->siteUrlOverride)) {
436
            $siteUrl = Seomatic::$settings->siteUrlOverride;
437
        }
438
        $siteUrl = $siteUrl ?: '/';
439
        /** @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...
440
        $crumbs = Seomatic::$plugin->jsonLd->create([
441
            'type' => 'BreadcrumbList',
442
            'name' => 'Breadcrumbs',
443
            'description' => 'Breadcrumbs list',
444
        ], false);
445
        // Include the Homepage in the breadcrumbs, if includeHomepageInBreadcrumbs is true
446
        $element = null;
447
        if (Seomatic::$settings->includeHomepageInBreadcrumbs) {
448
            /** @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...
449
            $position++;
450
            $element = Craft::$app->getElements()->getElementByUri('__home__', $siteId);
451
            if ($element) {
452
                $uri = $element->uri === '__home__' ? '' : ($element->uri ?? '');
453
                try {
454
                    $id = UrlHelper::siteUrl($uri, null, null, $siteId);
455
                } catch (Exception $e) {
456
                    $id = $siteUrl;
457
                    Craft::error($e->getMessage(), __METHOD__);
458
                }
459
                $item = UrlHelper::stripQueryString($id);
460
                $item = UrlHelper::absoluteUrlWithProtocol($item);
461
                $listItem = MetaJsonLd::create('ListItem', [
462
                    'position' => $position,
463
                    'name' => $element->title,
464
                    'item' => $item,
465
                    '@id' => $id,
466
                ]);
467
                $crumbs->itemListElement[] = $listItem;
468
            } else {
469
                $item = UrlHelper::stripQueryString($siteUrl ?? '/');
470
                $item = UrlHelper::absoluteUrlWithProtocol($item);
471
                $crumbs->itemListElement[] = MetaJsonLd::create('ListItem', [
472
                    'position' => $position,
473
                    'name' => 'Homepage',
474
                    'item' => $item,
475
                    '@id' => $siteUrl,
476
                ]);
477
            }
478
        }
479
        // Build up the segments, and look for elements that match
480
        $uri = '';
481
        $segments = Craft::$app->getRequest()->getSegments();
482
        /** @var  $lastElement 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...
483
        $lastElement = Seomatic::$matchedElement;
484
        if ($lastElement && $element) {
485
            if ($lastElement->uri !== '__home__' && $element->uri) {
486
                $path = $lastElement->uri;
487
                $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

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