Passed
Push — develop-v4 ( c90bff...1003e3 )
by Andrew
11:42
created

Seomatic::handleSiteRequest()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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