Passed
Push — develop-v4 ( 01c404...563ce7 )
by Andrew
18:55 queued 08:57
created

Seomatic::handleSiteRequest()   B

Complexity

Conditions 7
Paths 1

Size

Total Lines 44
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 27
dl 0
loc 44
ccs 0
cts 35
cp 0
rs 8.5546
c 1
b 0
f 0
cc 7
nc 1
nop 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;
13
14
use Craft;
15
use craft\base\Element;
16
use craft\base\ElementInterface;
17
use craft\base\Model;
18
use craft\base\Plugin;
19
use craft\elements\Entry;
20
use craft\elements\User;
21
use craft\errors\SiteNotFoundException;
22
use craft\events\DefineGqlTypeFieldsEvent;
23
use craft\events\ElementEvent;
24
use craft\events\PluginEvent;
25
use craft\events\RegisterCacheOptionsEvent;
26
use craft\events\RegisterComponentTypesEvent;
27
use craft\events\RegisterGqlQueriesEvent;
28
use craft\events\RegisterGqlSchemaComponentsEvent;
29
use craft\events\RegisterGqlTypesEvent;
30
use craft\events\RegisterPreviewTargetsEvent;
31
use craft\events\RegisterUrlRulesEvent;
32
use craft\events\RegisterUserPermissionsEvent;
33
use craft\feedme\events\RegisterFeedMeFieldsEvent;
34
use craft\feedme\Plugin as FeedMe;
35
use craft\feedme\services\Fields as FeedMeFields;
36
use craft\gql\TypeManager;
37
use craft\helpers\StringHelper;
38
use craft\helpers\UrlHelper;
39
use craft\services\Elements;
40
use craft\services\Fields;
41
use craft\services\Gql;
42
use craft\services\Plugins;
43
use craft\services\Sites as SitesService;
44
use craft\services\UserPermissions;
45
use craft\utilities\ClearCaches;
46
use craft\web\UrlManager;
47
use craft\web\View;
48
use nystudio107\codeeditor\autocompletes\EnvironmentVariableAutocomplete;
49
use nystudio107\codeeditor\events\RegisterCodeEditorAutocompletesEvent;
50
use nystudio107\codeeditor\events\RegisterTwigValidatorVariablesEvent;
51
use nystudio107\codeeditor\services\AutocompleteService;
52
use nystudio107\codeeditor\validators\TwigTemplateValidator;
53
use nystudio107\fastcgicachebust\FastcgiCacheBust;
54
use nystudio107\seomatic\autocompletes\TrackingVarsAutocomplete;
55
use nystudio107\seomatic\fields\Seomatic_Meta as Seomatic_MetaField;
56
use nystudio107\seomatic\fields\SeoSettings as SeoSettingsField;
57
use nystudio107\seomatic\gql\arguments\SeomaticArguments;
58
use nystudio107\seomatic\gql\interfaces\SeomaticInterface;
59
use nystudio107\seomatic\gql\queries\SeomaticQuery;
60
use nystudio107\seomatic\gql\resolvers\SeomaticResolver;
61
use nystudio107\seomatic\gql\types\SeomaticEnvironmentType;
62
use nystudio107\seomatic\helpers\Environment;
63
use nystudio107\seomatic\helpers\Environment as EnvironmentHelper;
64
use nystudio107\seomatic\helpers\Gql as GqlHelper;
65
use nystudio107\seomatic\helpers\MetaValue as MetaValueHelper;
66
use nystudio107\seomatic\helpers\Schema as SchemaHelper;
67
use nystudio107\seomatic\integrations\feedme\SeoSettings as SeoSettingsFeedMe;
68
use nystudio107\seomatic\models\MetaScriptContainer;
69
use nystudio107\seomatic\models\Settings;
70
use nystudio107\seomatic\services\ServicesTrait;
71
use nystudio107\seomatic\twigextensions\SeomaticTwigExtension;
72
use nystudio107\seomatic\variables\SeomaticVariable;
73
use yii\base\Event;
74
use yii\base\View as BaseView;
75
use yii\web\View as YiiView;
76
77
/** @noinspection MissingPropertyAnnotationsInspection */
78
79
/**
80
 * Class Seomatic
81
 *
82
 * @author    nystudio107
83
 * @package   Seomatic
84
 * @since     3.0.0
85
 */
86
class Seomatic extends Plugin
87
{
88
    // Traits
89
    // =========================================================================
90
91
    use ServicesTrait;
92
93
    // Constants
94
    // =========================================================================
95
96
    public const SEOMATIC_HANDLE = 'Seomatic';
97
98
    public const DEVMODE_CACHE_DURATION = 30;
99
100
    public const SEOMATIC_PREVIEW_AUTHORIZATION_KEY = 'seomaticPreviewAuthorizationKey';
101
102
    protected const FRONTEND_SEO_FILE_LINK = 'seomatic/seo-file-link/<url:[^\/]+>/<robots:[^\/]+>/<canonical:[^\/]+>/<inline:\d+>/<fileName:[-\w\.*]+>';
103
104
    protected const FRONTEND_PREVIEW_PATH = 'seomatic/preview-social-media';
105
106
    const SEOMATIC_EXPRESSION_FIELD_TYPE = 'SeomaticExpressionField';
107
    const SEOMATIC_TRACKING_FIELD_TYPE = 'SeomaticTrackingField';
108
109
    // Static Properties
110
    // =========================================================================
111
112
    /**
113
     * @var null|Seomatic
114
     */
115
    public static ?Seomatic $plugin;
116
117
    /**
118
     * @var null|SeomaticVariable
119
     */
120
    public static ?SeomaticVariable $seomaticVariable = null;
121
122
    /**
123
     * @var null|Settings
124
     */
125
    public static ?Settings $settings = null;
126
127
    /**
128
     * @var null|ElementInterface
129
     */
130
    public static ?ElementInterface $matchedElement = null;
131
132
    /**
133
     * @var bool
134
     */
135
    public static bool $devMode;
136
137
    /**
138
     * @var null|View
139
     */
140
    public static ?View $view = null;
141
142
    /**
143
     * @var string
144
     */
145
    public static string $language = '';
146
147
    /**
148
     * @var string
149
     */
150
    public static string $environment = '';
151
152
    /**
153
     * @var int
154
     */
155
    public static int $cacheDuration = 0;
156
157
    /**
158
     * @var bool
159
     */
160
    public static bool $previewingMetaContainers = false;
161
162
    /**
163
     * @var bool
164
     */
165
    public static bool $loadingMetaContainers = false;
166
167
    /**
168
     * @var bool
169
     */
170
    public static bool $savingSettings = false;
171
172
    /**
173
     * @var bool
174
     */
175
    public static bool $headlessRequest = false;
176
177
    // Public Properties
178
    // =========================================================================
179
180
    /**
181
     * @var string
182
     */
183
    public string $schemaVersion = '3.0.12';
184
185
    /**
186
     * @var bool
187
     */
188
    public bool $hasCpSection = true;
189
190
    /**
191
     * @var bool
192
     */
193
    public bool $hasCpSettings = true;
194
195
    /**
196
     * Set the matched element
197
     *
198
     * @param $element null|ElementInterface
199
     */
200
    public static function setMatchedElement(?ElementInterface $element): void
201
    {
202
        self::$matchedElement = $element;
203
        /** @var  $element Element */
204
        if ($element) {
0 ignored issues
show
introduced by
$element is of type craft\base\Element, thus it always evaluated to true.
Loading history...
205
            self::$language = MetaValueHelper::getSiteLanguage($element->siteId);
206
        } else {
207
            self::$language = MetaValueHelper::getSiteLanguage(null);
208
        }
209
        MetaValueHelper::cache();
210
    }
211
212
    // Public Methods
213
    // =========================================================================
214
215
    /**
216
     * @inheritdoc
217
     */
218
    public function init(): void
219
    {
220
        parent::init();
221
        self::$plugin = $this;
222
        // Handle any console commands
223
        $request = Craft::$app->getRequest();
224
        if ($request->getIsConsoleRequest()) {
225
            $this->controllerNamespace = 'nystudio107\seomatic\console\controllers';
226
        }
227
        // Initialize properties
228
        self::$settings = self::$plugin->getSettings();
229
        self::$devMode = Craft::$app->getConfig()->getGeneral()->devMode;
230
        self::$view = Craft::$app->getView();
231
        self::$cacheDuration = self::$devMode
232
            ? self::DEVMODE_CACHE_DURATION
233
            : self::$settings->metaCacheDuration ?? 0;
234
        self::$environment = EnvironmentHelper::determineEnvironment();
235
        MetaValueHelper::cache();
236
        $this->name = self::$settings->pluginName;
237
        // Install our event listeners
238
        $this->installEventListeners();
239
        // We're loaded
240
        Craft::info(
241
            Craft::t(
242
                'seomatic',
243
                '{name} plugin loaded',
244
                ['name' => $this->name]
245
            ),
246
            __METHOD__
247
        );
248
    }
249
250
    /**
251
     * @inheritdoc
252
     */
253
    public function getSettings(): ?Model
254
    {
255
        // For all the emojis
256
        /* @var Settings $settingsModel */
257
        $settingsModel = parent::getSettings();
258
        if ($settingsModel !== null && !self::$savingSettings) {
259
            $attributes = $settingsModel->attributes();
260
            if ($attributes !== null) {
0 ignored issues
show
introduced by
The condition $attributes !== null is always true.
Loading history...
261
                foreach ($attributes as $attribute) {
262
                    if (is_string($settingsModel->$attribute)) {
263
                        $settingsModel->$attribute = html_entity_decode(
264
                            $settingsModel->$attribute,
265
                            ENT_NOQUOTES,
266
                            'UTF-8'
267
                        );
268
                    }
269
                }
270
            }
271
            self::$savingSettings = false;
272
        }
273
274
        return $settingsModel;
275
    }
276
277
    /**
278
     * Determine whether our table schema exists or not; this is needed because
279
     * migrations such as the install migration and base_install migration may
280
     * not have been run by the time our init() method has been called
281
     *
282
     * @return bool
283
     */
284
    public function migrationsAndSchemaReady(): bool
285
    {
286
        $pluginsService = Craft::$app->getPlugins();
287
        if ($pluginsService->isPluginUpdatePending(self::$plugin)) {
288
            return false;
289
        }
290
        if (Craft::$app->db->schema->getTableSchema('{{%seomatic_metabundles}}') === null) {
291
            return false;
292
        }
293
294
        return true;
295
    }
296
297
    /**
298
     * Clear all the caches!
299
     */
300
    public function clearAllCaches(): void
301
    {
302
        // Clear all of SEOmatic's caches
303
        self::$plugin->frontendTemplates->invalidateCaches();
304
        self::$plugin->metaContainers->invalidateCaches();
305
        self::$plugin->sitemaps->invalidateCaches();
306
        SchemaHelper::invalidateCaches();
307
        // Clear the GraphQL caches too
308
        $gql = Craft::$app->getGql();
309
        if (method_exists($gql, 'invalidateCaches')) {
310
            $gql->invalidateCaches();
311
        }
312
        // If the FastCGI Cache Bust plugin is installed, clear its caches too
313
        $plugin = Craft::$app->getPlugins()->getPlugin('fastcgi-cache-bust');
314
        if ($plugin !== null) {
315
            FastcgiCacheBust::$plugin->cache->clearAll();
316
        }
317
    }
318
319
    // Protected Methods
320
    // =========================================================================
321
322
    /**
323
     * @inheritdoc
324
     */
325
    public function getSettingsResponse(): mixed
326
    {
327
        // Just redirect to the plugin settings page
328
        return Craft::$app->getResponse()->redirect(UrlHelper::cpUrl('seomatic/plugin'));
329
    }
330
331
    /**
332
     * @inheritdoc
333
     */
334
    public function getCpNavItem(): ?array
335
    {
336
        $subNavs = [];
337
        $navItem = parent::getCpNavItem();
338
        /** @var User $currentUser */
339
        $request = Craft::$app->getRequest();
340
        $siteSuffix = '';
341
        if ($request->getSegment(1) === 'seomatic') {
342
            $segments = $request->getSegments();
343
            $lastSegment = end($segments);
344
            $site = Craft::$app->getSites()->getSiteByHandle($lastSegment);
345
            if ($site !== null) {
346
                $siteSuffix = '/' . $lastSegment;
347
            }
348
        }
349
        $currentUser = Craft::$app->getUser()->getIdentity();
350
        // Only show sub-navs the user has permission to view
351
        if ($currentUser->can('seomatic:dashboard')) {
352
            $subNavs['dashboard'] = [
353
                'label' => Craft::t('seomatic', 'Dashboard'),
354
                'url' => 'seomatic/dashboard' . $siteSuffix,
355
            ];
356
        }
357
        if ($currentUser->can('seomatic:global-meta')) {
358
            $subNavs['global'] = [
359
                'label' => Craft::t('seomatic', 'Global SEO'),
360
                'url' => 'seomatic/global/general' . $siteSuffix,
361
            ];
362
        }
363
        if ($currentUser->can('seomatic:content-meta')) {
364
            $subNavs['content'] = [
365
                'label' => Craft::t('seomatic', 'Content SEO'),
366
                'url' => 'seomatic/content' . $siteSuffix,
367
            ];
368
        }
369
        if ($currentUser->can('seomatic:site-settings')) {
370
            $subNavs['site'] = [
371
                'label' => Craft::t('seomatic', 'Site Settings'),
372
                'url' => 'seomatic/site/identity' . $siteSuffix,
373
            ];
374
        }
375
        if ($currentUser->can('seomatic:tracking-scripts')) {
376
            $subNavs['tracking'] = [
377
                'label' => Craft::t('seomatic', 'Tracking Scripts'),
378
                'url' => 'seomatic/tracking/gtag' . $siteSuffix,
379
            ];
380
        }
381
        $editableSettings = true;
382
        $general = Craft::$app->getConfig()->getGeneral();
383
        if (!$general->allowAdminChanges) {
384
            $editableSettings = false;
385
        }
386
        if ($editableSettings && $currentUser->can('seomatic:plugin-settings')) {
387
            $subNavs['plugin'] = [
388
                'label' => Craft::t('seomatic', 'Plugin Settings'),
389
                'url' => 'seomatic/plugin',
390
            ];
391
        }
392
393
        return array_merge($navItem, [
394
            'subnav' => $subNavs,
395
        ]);
396
    }
397
398
    /**
399
     * Install our event listeners.
400
     */
401
    protected function installEventListeners(): void
402
    {
403
        // Install our event listeners only if our table schema exists
404
        if ($this->migrationsAndSchemaReady()) {
405
            // Add in our Twig extensions
406
            self::$view->registerTwigExtension(new SeomaticTwigExtension);
407
            $request = Craft::$app->getRequest();
408
            // Add in our event listeners that are needed for every request
409
            $this->installGlobalEventListeners();
410
            // Install only for non-console site requests
411
            if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest()) {
412
                $this->installSiteEventListeners();
413
            }
414
            // Install only for non-console Control Panel requests
415
            if ($request->getIsCpRequest() && !$request->getIsConsoleRequest()) {
416
                $this->installCpEventListeners();
417
            }
418
        }
419
        // Handler: EVENT_AFTER_INSTALL_PLUGIN
420
        Event::on(
421
            Plugins::class,
422
            Plugins::EVENT_AFTER_INSTALL_PLUGIN,
423
            function (PluginEvent $event) {
424
                if ($event->plugin === $this) {
0 ignored issues
show
introduced by
The condition $event->plugin === $this is always false.
Loading history...
425
                    // Invalidate our caches after we've been installed
426
                    $this->clearAllCaches();
427
                    // Send them to our welcome screen
428
                    $request = Craft::$app->getRequest();
429
                    if ($request->isCpRequest) {
430
                        Craft::$app->getResponse()->redirect(UrlHelper::cpUrl(
431
                            'seomatic/dashboard',
432
                            [
433
                                'showWelcome' => true,
434
                            ]
435
                        ))->send();
436
                    }
437
                }
438
            }
439
        );
440
        // Handler: ClearCaches::EVENT_REGISTER_CACHE_OPTIONS
441
        Event::on(
442
            ClearCaches::class,
443
            ClearCaches::EVENT_REGISTER_CACHE_OPTIONS,
444
            function (RegisterCacheOptionsEvent $event) {
445
                Craft::debug(
446
                    'ClearCaches::EVENT_REGISTER_CACHE_OPTIONS',
447
                    __METHOD__
448
                );
449
                // Register our Cache Options
450
                $event->options = array_merge(
451
                    $event->options,
452
                    $this->customAdminCpCacheOptions()
453
                );
454
            }
455
        );
456
        // Handler: EVENT_BEFORE_SAVE_PLUGIN_SETTINGS
457
        Event::on(
458
            Plugins::class,
459
            Plugins::EVENT_BEFORE_SAVE_PLUGIN_SETTINGS,
460
            function (PluginEvent $event) {
461
                if ($event->plugin === $this && !Craft::$app->getDb()->getSupportsMb4()) {
0 ignored issues
show
introduced by
The condition $event->plugin === $this is always false.
Loading history...
462
                    // For all the emojis
463
                    $settingsModel = $this->getSettings();
464
                    self::$savingSettings = true;
465
                    if ($settingsModel !== null) {
466
                        $attributes = $settingsModel->attributes();
467
                        if ($attributes !== null) {
468
                            foreach ($attributes as $attribute) {
469
                                if (is_string($settingsModel->$attribute)) {
470
                                    $settingsModel->$attribute =
471
                                        StringHelper::encodeMb4($settingsModel->$attribute);
472
                                }
473
                            }
474
                        }
475
                    }
476
                }
477
            }
478
        );
479
    }
480
481
    /**
482
     * Install global event listeners for all request types
483
     */
484
    protected function installGlobalEventListeners(): void
485
    {
486
        // Handler: Plugins::EVENT_AFTER_LOAD_PLUGINS
487
        Event::on(
488
            Plugins::class,
489
            Plugins::EVENT_AFTER_LOAD_PLUGINS,
490
            function () {
491
                // Delay registering SEO Elements to give other plugins a chance to load first
492
                $this->seoElements->getAllSeoElementTypes(false);
493
                // Delay installing GQL handlers to give other plugins a chance to register their own first
494
                $this->installGqlHandlers();
495
                // Install these only after all other plugins have loaded
496
                $request = Craft::$app->getRequest();
497
                // Only respond to non-console site requests
498
                if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest()) {
499
                    $this->handleSiteRequest();
500
                }
501
                // Respond to Control Panel requests
502
                if ($request->getIsCpRequest() && !$request->getIsConsoleRequest()) {
503
                    $this->handleAdminCpRequest();
504
                }
505
            }
506
        );
507
        // Handler: Fields::EVENT_REGISTER_FIELD_TYPES
508
        Event::on(
509
            Fields::class,
510
            Fields::EVENT_REGISTER_FIELD_TYPES,
511
            static function (RegisterComponentTypesEvent $event) {
512
                $event->types[] = SeoSettingsField::class;
513
                $event->types[] = Seomatic_MetaField::class;
514
            }
515
        );
516
        // Handler: Elements::EVENT_AFTER_SAVE_ELEMENT
517
        Event::on(
518
            Elements::class,
519
            Elements::EVENT_AFTER_SAVE_ELEMENT,
520
            static function (ElementEvent $event) {
521
                Craft::debug(
522
                    'Elements::EVENT_AFTER_SAVE_ELEMENT',
523
                    __METHOD__
524
                );
525
                /** @var  $element Element */
526
                $element = $event->element;
527
                self::$plugin->metaBundles->invalidateMetaBundleByElement(
528
                    $element,
529
                    $event->isNew
530
                );
531
                if ($event->isNew) {
532
                    self::$plugin->sitemaps->submitSitemapForElement($element);
533
                }
534
            }
535
        );
536
        // Handler: Elements::EVENT_AFTER_DELETE_ELEMENT
537
        Event::on(
538
            Elements::class,
539
            Elements::EVENT_AFTER_DELETE_ELEMENT,
540
            static function (ElementEvent $event) {
541
                Craft::debug(
542
                    'Elements::EVENT_AFTER_DELETE_ELEMENT',
543
                    __METHOD__
544
                );
545
                /** @var  $element Element */
546
                $element = $event->element;
547
                self::$plugin->metaBundles->invalidateMetaBundleByElement(
548
                    $element,
549
                    false
550
                );
551
            }
552
        );
553
        // Add social media preview targets on Craft 3.2 or later
554
        if (self::$settings->socialMediaPreviewTarget) {
555
            // Handler: Entry::EVENT_REGISTER_PREVIEW_TARGETS
556
            Event::on(
557
                Entry::class,
558
                Element::EVENT_REGISTER_PREVIEW_TARGETS,
559
                static function (RegisterPreviewTargetsEvent $e) {
560
                    /** @var Element $element */
561
                    $element = $e->sender;
562
                    if ($element->uri !== null) {
563
                        $e->previewTargets[] = [
564
                            'label' => '📣 ' . Craft::t('seomatic', 'Social Media Preview'),
565
                            'url' => UrlHelper::siteUrl(self::FRONTEND_PREVIEW_PATH, [
566
                                'elementId' => $element->id,
567
                                'siteId' => $element->siteId,
568
                            ]),
569
                        ];
570
                        // Don't allow the preview to be accessed publicly
571
                        Craft::$app->getSession()->authorize(self::SEOMATIC_PREVIEW_AUTHORIZATION_KEY . $element->id);
572
                    }
573
                }
574
            );
575
        }
576
        // FeedMe Support
577
        if (class_exists(FeedMe::class)) {
578
            Event::on(
579
                FeedMeFields::class,
580
                FeedMeFields::EVENT_REGISTER_FEED_ME_FIELDS,
581
                static function (RegisterFeedMeFieldsEvent $e) {
582
                    Craft::debug(
583
                        'FeedMeFields::EVENT_REGISTER_FEED_ME_FIELDS',
584
                        __METHOD__
585
                    );
586
                    $e->fields[] = SeoSettingsFeedMe::class;
587
                }
588
            );
589
        }
590
        $updateMetaBundles = static function ($message) {
591
            Craft::debug(
592
                $message,
593
                __METHOD__
594
            );
595
            $seoElementTypes = Seomatic::$plugin->seoElements->getAllSeoElementTypes();
596
            foreach ($seoElementTypes as $seoElementType) {
597
                $metaBundleType = $seoElementType::META_BUNDLE_TYPE ?? '';
598
599
                if ($metaBundleType) {
600
                    Seomatic::$plugin->metaBundles->resaveMetaBundles($metaBundleType);
601
                }
602
            }
603
        };
604
605
        // Handler: Elements::EVENT_AFTER_SAVE_SITE
606
        Event::on(
607
            SitesService::class,
608
            SitesService::EVENT_AFTER_SAVE_SITE,
609
            static function () use ($updateMetaBundles) {
610
                $updateMetaBundles('SitesService::EVENT_AFTER_SAVE_SITE');
611
            }
612
        );
613
614
        // Handler: Elements::EVENT_AFTER_DELETE_SITE
615
        Event::on(
616
            SitesService::class,
617
            SitesService::EVENT_AFTER_DELETE_SITE,
618
            static function () use ($updateMetaBundles) {
619
                $updateMetaBundles('SitesService::EVENT_AFTER_DELETE_SITE');
620
            }
621
        );
622
    }
623
624
    /**
625
     * Register our GraphQL handlers
626
     *
627
     * @return void
628
     */
629
    protected function installGqlHandlers(): void
630
    {
631
        // Handler: Gql::EVENT_REGISTER_GQL_TYPES
632
        Event::on(
633
            Gql::class,
634
            Gql::EVENT_REGISTER_GQL_TYPES,
635
            static function (RegisterGqlTypesEvent $event) {
636
                Craft::debug(
637
                    'Gql::EVENT_REGISTER_GQL_TYPES',
638
                    __METHOD__
639
                );
640
                $event->types[] = SeomaticInterface::class;
641
                $event->types[] = SeomaticEnvironmentType::class;
642
            }
643
        );
644
        // Handler: Gql::EVENT_REGISTER_GQL_QUERIES
645
        Event::on(
646
            Gql::class,
647
            Gql::EVENT_REGISTER_GQL_QUERIES,
648
            static function (RegisterGqlQueriesEvent $event) {
649
                Craft::debug(
650
                    'Gql::EVENT_REGISTER_GQL_QUERIES',
651
                    __METHOD__
652
                );
653
                $queries = SeomaticQuery::getQueries();
654
                foreach ($queries as $key => $value) {
655
                    $event->queries[$key] = $value;
656
                }
657
            }
658
        );
659
        // Handler: Gql::EVENT_REGISTER_GQL_SCHEMA_COMPONENTS
660
        Event::on(
661
            Gql::class,
662
            Gql::EVENT_REGISTER_GQL_SCHEMA_COMPONENTS,
663
            static function (RegisterGqlSchemaComponentsEvent $event) {
664
                Craft::debug(
665
                    'Gql::EVENT_REGISTER_GQL_SCHEMA_COMPONENTS',
666
                    __METHOD__
667
                );
668
                $label = Craft::t('seomatic', 'Seomatic');
669
                $event->queries[$label]['seomatic.all:read'] = ['label' => Craft::t('seomatic', 'Query Seomatic data')];
670
            }
671
        );
672
        // Add support for querying for SEOmatic metadata inside of element queries
673
        // Handler: TypeManager::EVENT_DEFINE_GQL_TYPE_FIELDS
674
        $knownInterfaceNames = self::$plugin->seoElements->getAllSeoElementGqlInterfaceNames();
675
        Event::on(
676
            TypeManager::class,
677
            TypeManager::EVENT_DEFINE_GQL_TYPE_FIELDS,
678
            static function (DefineGqlTypeFieldsEvent $event) use ($knownInterfaceNames) {
679
                if (in_array($event->typeName, $knownInterfaceNames, true)) {
680
                    Craft::debug(
681
                        'TypeManager::EVENT_DEFINE_GQL_TYPE_FIELDS',
682
                        __METHOD__
683
                    );
684
685
                    if (GqlHelper::canQuerySeo()) {
686
                        // Make Seomatic tags available to all entries.
687
                        $event->fields['seomatic'] = [
688
                            'name' => 'seomatic',
689
                            'type' => SeomaticInterface::getType(),
690
                            'args' => SeomaticArguments::getArguments(),
691
                            'resolve' => SeomaticResolver::class . '::resolve',
692
                            'description' => Craft::t('seomatic', 'This query is used to query for SEOmatic meta data.')
693
                        ];
694
                    }
695
                }
696
            });
697
    }
698
699
    /**
700
     * Handle site requests.  We do it only after we receive the event
701
     * EVENT_AFTER_LOAD_PLUGINS so that any pending db migrations can be run
702
     * before our event listeners kick in
703
     */
704
    protected function handleSiteRequest(): void
705
    {
706
        // Handler: View::EVENT_BEGIN_BODY
707
        Event::on(
708
            View::class,
709
            YiiView::EVENT_BEGIN_BODY,
710
            static function () {
711
                Craft::debug(
712
                    'View::EVENT_BEGIN_BODY',
713
                    __METHOD__
714
                );
715
                // The <body> placeholder tag has just rendered, include any script HTML
716
                if (self::$settings->renderEnabled && self::$seomaticVariable) {
717
                    self::$plugin->metaContainers->includeScriptBodyHtml(YiiView::POS_BEGIN);
718
                }
719
            }
720
        );
721
        // Handler: View::EVENT_END_BODY
722
        Event::on(
723
            View::class,
724
            YiiView::EVENT_END_BODY,
725
            static function () {
726
                Craft::debug(
727
                    'View::EVENT_END_BODY',
728
                    __METHOD__
729
                );
730
                // The </body> placeholder tag is about to be rendered, include any script HTML
731
                if (self::$settings->renderEnabled && self::$seomaticVariable) {
732
                    self::$plugin->metaContainers->includeScriptBodyHtml(YiiView::POS_END);
733
                }
734
            }
735
        );
736
        // Handler: View::EVENT_END_PAGE
737
        Event::on(
738
            View::class,
739
            BaseView::EVENT_END_PAGE,
740
            static function () {
741
                Craft::debug(
742
                    'View::EVENT_END_PAGE',
743
                    __METHOD__
744
                );
745
                // The page is done rendering, include our meta containers
746
                if (self::$settings->renderEnabled && self::$seomaticVariable) {
747
                    self::$plugin->metaContainers->includeMetaContainers();
748
                }
749
            }
750
        );
751
    }
752
753
    /**
754
     * Handle Control Panel requests. We do it only after we receive the event
755
     * EVENT_AFTER_LOAD_PLUGINS so that any pending db migrations can be run
756
     * before our event listeners kick in
757
     */
758
    protected function handleAdminCpRequest(): void
759
    {
760
        // Don't cache Control Panel requests
761
        self::$cacheDuration = 1;
762
        // Prefix the Control Panel title
763
        self::$view->hook('cp.layouts.base', function (&$context) {
764
            if (self::$devMode) {
765
                $context['docTitle'] = self::$settings->devModeCpTitlePrefix . $context['docTitle'];
766
            } else {
767
                $context['docTitle'] = self::$settings->cpTitlePrefix . $context['docTitle'];
768
            }
769
        });
770
    }
771
772
    /**
773
     * Install site event listeners for site requests only
774
     */
775
    protected function installSiteEventListeners(): void
776
    {
777
        // Load the sitemap containers
778
        self::$plugin->sitemaps->loadSitemapContainers();
779
        // Load the frontend template containers
780
        self::$plugin->frontendTemplates->loadFrontendTemplateContainers();
781
        // Handler: UrlManager::EVENT_REGISTER_SITE_URL_RULES
782
        Event::on(
783
            UrlManager::class,
784
            UrlManager::EVENT_REGISTER_SITE_URL_RULES,
785
            function (RegisterUrlRulesEvent $event) {
786
                Craft::debug(
787
                    'UrlManager::EVENT_REGISTER_SITE_URL_RULES',
788
                    __METHOD__
789
                );
790
                // FileController
791
                $route = self::$plugin->handle . '/file/seo-file-link';
792
                $event->rules[self::FRONTEND_SEO_FILE_LINK] = ['route' => $route];
793
                // PreviewController
794
                $route = self::$plugin->handle . '/preview/social-media';
795
                $event->rules[self::FRONTEND_PREVIEW_PATH] = ['route' => $route];
796
                // Register our Control Panel routes
797
                $event->rules = array_merge(
798
                    $event->rules,
799
                    $this->customFrontendRoutes()
800
                );
801
            }
802
        );
803
    }
804
805
    /**
806
     * Return the custom frontend routes
807
     *
808
     * @return array
809
     */
810
    protected function customFrontendRoutes(): array
811
    {
812
        return [
813
        ];
814
    }
815
816
    /**
817
     * Install site event listeners for Control Panel requests only
818
     */
819
    protected function installCpEventListeners(): void
820
    {
821
        // Load the frontend template containers
822
        self::$plugin->frontendTemplates->loadFrontendTemplateContainers();
823
        // Handler: UrlManager::EVENT_REGISTER_CP_URL_RULES
824
        Event::on(
825
            UrlManager::class,
826
            UrlManager::EVENT_REGISTER_CP_URL_RULES,
827
            function (RegisterUrlRulesEvent $event) {
828
                Craft::debug(
829
                    'UrlManager::EVENT_REGISTER_CP_URL_RULES',
830
                    __METHOD__
831
                );
832
                // Register our Control Panel routes
833
                $event->rules = array_merge(
834
                    $event->rules,
835
                    $this->customAdminCpRoutes()
836
                );
837
            }
838
        );
839
        // Handler: UserPermissions::EVENT_REGISTER_PERMISSIONS
840
        Event::on(
841
            UserPermissions::class,
842
            UserPermissions::EVENT_REGISTER_PERMISSIONS,
843
            function (RegisterUserPermissionsEvent $event) {
844
                Craft::debug(
845
                    'UserPermissions::EVENT_REGISTER_PERMISSIONS',
846
                    __METHOD__
847
                );
848
                // Register our custom permissions
849
                $event->permissions[] = [
850
                    'heading' => Craft::t('seomatic', 'SEOmatic'),
851
                    'permissions' => $this->customAdminCpPermissions(),
852
                ];
853
            }
854
        );
855
        // Handler: AutocompleteService::EVENT_REGISTER_CODEEDITOR_AUTOCOMPLETES
856
        Event::on(AutocompleteService::class, AutocompleteService::EVENT_REGISTER_CODEEDITOR_AUTOCOMPLETES,
857
            function (RegisterCodeEditorAutocompletesEvent $event) {
858
                if ($event->fieldType === self::SEOMATIC_EXPRESSION_FIELD_TYPE) {
859
                    $event->types[] = EnvironmentVariableAutocomplete::class;
860
                }
861
                if ($event->fieldType === self::SEOMATIC_TRACKING_FIELD_TYPE) {
862
                    $event->types[] = TrackingVarsAutocomplete::class;
863
                }
864
            }
865
        );
866
        // Handler: TwigTemplateValidator::EVENT_REGISTER_TWIG_VALIDATOR_VARIABLES
867
        Event::on(TwigTemplateValidator::class,
868
            TwigTemplateValidator::EVENT_REGISTER_TWIG_VALIDATOR_VARIABLES,
869
            function (RegisterTwigValidatorVariablesEvent $event) {
870
                if (Seomatic::$seomaticVariable === null) {
871
                    Seomatic::$seomaticVariable = new SeomaticVariable();
872
                    Seomatic::$plugin->metaContainers->loadGlobalMetaContainers();
873
                    Seomatic::$seomaticVariable->init();
874
                }
875
                $event->variables['seomatic'] = Seomatic::$seomaticVariable;
876
            }
877
        );
878
    }
879
880
    /**
881
     * Return the custom Control Panel routes
882
     *
883
     * @return array
884
     */
885
    protected function customAdminCpRoutes(): array
886
    {
887
        return [
888
            'seomatic' =>
889
                'seomatic/settings/dashboard',
890
            'seomatic/dashboard' =>
891
                'seomatic/settings/dashboard',
892
            'seomatic/dashboard/<siteHandle:{handle}>' =>
893
                'seomatic/settings/dashboard',
894
895
            'seomatic/global' => [
896
                'route' => 'seomatic/settings/global',
897
                'defaults' => ['subSection' => 'general'],
898
            ],
899
            'seomatic/global/<subSection:{handle}>' =>
900
                'seomatic/settings/global',
901
            'seomatic/global/<subSection:{handle}>/<siteHandle:{handle}>' =>
902
                'seomatic/settings/global',
903
904
            'seomatic/content' =>
905
                'seomatic/settings/content',
906
            'seomatic/content/<siteHandle:{handle}>' =>
907
                'seomatic/settings/content',
908
909
            'seomatic/edit-content/<subSection:{handle}>/<sourceBundleType:{handle}>/<sourceHandle:{handle}>' =>
910
                'seomatic/settings/edit-content',
911
            'seomatic/edit-content/<subSection:{handle}>/<sourceBundleType:{handle}>/<sourceHandle:{handle}>/<siteHandle:{handle}>' =>
912
                'seomatic/settings/edit-content',
913
914
            // Seemingly duplicate route needed to handle Solspace Calendar, which allows characters like -'s
915
            // in their handles
916
            'seomatic/edit-content/<subSection:{handle}>/<sourceBundleType:{handle}>/<sourceHandle:{slug}>' =>
917
                'seomatic/settings/edit-content',
918
            'seomatic/edit-content/<subSection:{handle}>/<sourceBundleType:{handle}>/<sourceHandle:{slug}>/<siteHandle:{handle}>' =>
919
                'seomatic/settings/edit-content',
920
921
            'seomatic/site' => [
922
                'route' => 'seomatic/settings/site',
923
                'defaults' => ['subSection' => 'identity'],
924
            ],
925
            'seomatic/site/<subSection:{handle}>' =>
926
                'seomatic/settings/site',
927
            'seomatic/site/<subSection:{handle}>/<siteHandle:{handle}>' =>
928
                'seomatic/settings/site',
929
930
            'seomatic/tracking' => [
931
                'route' => 'seomatic/settings/tracking',
932
                'defaults' => ['subSection' => 'googleAnalytics'],
933
            ],
934
            'seomatic/tracking/<subSection:{handle}>' =>
935
                'seomatic/settings/tracking',
936
            'seomatic/tracking/<subSection:{handle}>/<siteHandle:{handle}>' =>
937
                'seomatic/settings/tracking',
938
939
            'seomatic/plugin' =>
940
                'seomatic/settings/plugin',
941
        ];
942
    }
943
944
    /**
945
     * Returns the custom Control Panel user permissions.
946
     *
947
     * @return array
948
     * @noinspection PhpArrayShapeAttributeCanBeAddedInspection
949
     */
950
    protected function customAdminCpPermissions(): array
951
    {
952
        // The script meta containers for the global meta bundle
953
        try {
954
            $currentSiteId = Craft::$app->getSites()->getCurrentSite()->id ?? 1;
955
        } catch (SiteNotFoundException $e) {
956
            $currentSiteId = 1;
957
            Craft::error($e->getMessage(), __METHOD__);
958
        }
959
        // Dynamic permissions for the scripts
960
        $metaBundle = self::$plugin->metaBundles->getGlobalMetaBundle($currentSiteId);
961
        $scriptsPerms = [];
962
        if ($metaBundle !== null) {
963
            $scripts = self::$plugin->metaBundles->getContainerDataFromBundle(
964
                $metaBundle,
965
                MetaScriptContainer::CONTAINER_TYPE
966
            );
967
            foreach ($scripts as $scriptHandle => $scriptData) {
968
                $scriptsPerms["seomatic:tracking-scripts:$scriptHandle"] = [
969
                    'label' => Craft::t('seomatic', $scriptData->name),
970
                ];
971
            }
972
        }
973
974
        return [
975
            'seomatic:dashboard' => [
976
                'label' => Craft::t('seomatic', 'Dashboard'),
977
            ],
978
            'seomatic:global-meta' => [
979
                'label' => Craft::t('seomatic', 'Edit Global Meta'),
980
                'nested' => [
981
                    'seomatic:global-meta:general' => [
982
                        'label' => Craft::t('seomatic', 'General'),
983
                    ],
984
                    'seomatic:global-meta:twitter' => [
985
                        'label' => Craft::t('seomatic', 'Twitter'),
986
                    ],
987
                    'seomatic:global-meta:facebook' => [
988
                        'label' => Craft::t('seomatic', 'Facebook'),
989
                    ],
990
                    'seomatic:global-meta:robots' => [
991
                        'label' => Craft::t('seomatic', 'Robots'),
992
                    ],
993
                    'seomatic:global-meta:humans' => [
994
                        'label' => Craft::t('seomatic', 'Humans'),
995
                    ],
996
                    'seomatic:global-meta:ads' => [
997
                        'label' => Craft::t('seomatic', 'Ads'),
998
                    ],
999
                    'seomatic:global-meta:security' => [
1000
                        'label' => Craft::t('seomatic', 'Security'),
1001
                    ],
1002
                ],
1003
            ],
1004
            'seomatic:content-meta' => [
1005
                'label' => Craft::t('seomatic', 'Edit Content SEO'),
1006
                'nested' => [
1007
                    'seomatic:content-meta:general' => [
1008
                        'label' => Craft::t('seomatic', 'General'),
1009
                    ],
1010
                    'seomatic:content-meta:twitter' => [
1011
                        'label' => Craft::t('seomatic', 'Twitter'),
1012
                    ],
1013
                    'seomatic:content-meta:facebook' => [
1014
                        'label' => Craft::t('seomatic', 'Facebook'),
1015
                    ],
1016
                    'seomatic:content-meta:sitemap' => [
1017
                        'label' => Craft::t('seomatic', 'Sitemap'),
1018
                    ],
1019
                ],
1020
            ],
1021
            'seomatic:site-settings' => [
1022
                'label' => Craft::t('seomatic', 'Edit Site Settings'),
1023
                'nested' => [
1024
                    'seomatic:site-settings:identity' => [
1025
                        'label' => Craft::t('seomatic', 'Identity'),
1026
                    ],
1027
                    'seomatic:site-settings:creator' => [
1028
                        'label' => Craft::t('seomatic', 'Creator'),
1029
                    ],
1030
                    'seomatic:site-settings:social' => [
1031
                        'label' => Craft::t('seomatic', 'Social Media'),
1032
                    ],
1033
                    'seomatic:site-settings:sitemap' => [
1034
                        'label' => Craft::t('seomatic', 'Sitemap'),
1035
                    ],
1036
                    'seomatic:site-settings:miscellaneous' => [
1037
                        'label' => Craft::t('seomatic', 'Miscellaneous'),
1038
                    ],
1039
                ],
1040
            ],
1041
            'seomatic:tracking-scripts' => [
1042
                'label' => Craft::t('seomatic', 'Edit Tracking Scripts'),
1043
                'nested' => $scriptsPerms,
1044
            ],
1045
            'seomatic:plugin-settings' => [
1046
                'label' => Craft::t('seomatic', 'Edit Plugin Settings'),
1047
            ],
1048
        ];
1049
    }
1050
1051
    /**
1052
     * Returns the custom Control Panel cache options.
1053
     *
1054
     * @return array
1055
     */
1056
    protected function customAdminCpCacheOptions(): array
1057
    {
1058
        return [
1059
            // Frontend template caches
1060
            [
1061
                'key' => 'seomatic-frontendtemplate-caches',
1062
                'label' => Craft::t('seomatic', 'SEOmatic frontend template caches'),
1063
                'action' => [self::$plugin->frontendTemplates, 'invalidateCaches'],
1064
            ],
1065
            // Meta bundle caches
1066
            [
1067
                'key' => 'seomatic-metabundle-caches',
1068
                'label' => Craft::t('seomatic', 'SEOmatic metadata caches'),
1069
                'action' => [self::$plugin->metaContainers, 'invalidateCaches'],
1070
            ],
1071
            // Sitemap caches
1072
            [
1073
                'key' => 'seomatic-sitemap-caches',
1074
                'label' => Craft::t('seomatic', 'SEOmatic sitemap caches'),
1075
                'action' => [self::$plugin->sitemaps, 'invalidateCaches'],
1076
            ],
1077
            // Schema caches
1078
            [
1079
                'key' => 'seomatic-schema-caches',
1080
                'label' => Craft::t('seomatic', 'SEOmatic schema caches'),
1081
                'action' => [SchemaHelper::class, 'invalidateCaches'],
1082
            ],
1083
        ];
1084
    }
1085
1086
    /**
1087
     * @inheritdoc
1088
     */
1089
    protected function createSettingsModel(): ?Model
1090
    {
1091
        return new Settings();
1092
    }
1093
}
1094