Passed
Push — develop ( 7364aa...89429b )
by Andrew
09:18
created

Seomatic   F

Complexity

Total Complexity 88

Size/Duplication

Total Lines 1064
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 8
Bugs 4 Features 0
Metric Value
wmc 88
eloc 520
c 8
b 4
f 0
dl 0
loc 1064
ccs 0
cts 442
cp 0
rs 2

19 Methods

Rating   Name   Duplication   Size   Complexity  
A createSettingsModel() 0 3 1
F getCpNavItem() 0 63 12
A getSettingsResponse() 0 4 1
A getSettings() 0 21 6
A clearAllCaches() 0 18 4
C installEventListeners() 0 71 14
A installSiteEventListeners() 0 25 1
A customAdminCpRoutes() 0 56 1
A customAdminCpCacheOptions() 0 26 1
A setMatchedElement() 0 10 2
A migrationsAndSchemaReady() 0 11 3
A handleAdminCpRequest() 0 10 2
B installGqlHandlers() 0 81 8
A installCpEventListeners() 0 54 4
B handleSiteRequest() 0 44 7
A customFrontendRoutes() 0 3 1
A init() 0 37 4
B customAdminCpPermissions() 0 96 4
C installGlobalEventListeners() 0 136 12

How to fix   Complexity   

Complex Class

Complex classes like Seomatic often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Seomatic, and based on these observations, apply Extract Interface, too.

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