Passed
Push — develop ( f23408...b9e9fb )
by Andrew
09:01
created

Seomatic::getSettings()   A

Complexity

Conditions 6
Paths 3

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
cc 6
eloc 12
nc 3
nop 0
dl 0
loc 21
ccs 0
cts 13
cp 0
crap 42
rs 9.2222
c 0
b 0
f 0
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_END_PAGE
670
        Event::on(
671
            View::class,
672
            View::EVENT_END_PAGE,
673
            function () {
674
                Craft::debug(
675
                    'View::EVENT_END_PAGE',
676
                    __METHOD__
677
                );
678
                // The page is done rendering, include our meta containers
679
                if (self::$settings->renderEnabled && self::$seomaticVariable) {
680
                    self::$plugin->metaContainers->includeMetaContainers();
681
                }
682
            }
683
        );
684
    }
685
686
    /**
687
     * Handle Control Panel requests. We do it only after we receive the event
688
     * EVENT_AFTER_LOAD_PLUGINS so that any pending db migrations can be run
689
     * before our event listeners kick in
690
     */
691
    protected function handleAdminCpRequest()
692
    {
693
        // Don't cache Control Panel requests
694
        self::$cacheDuration = 1;
695
        // Prefix the Control Panel title
696
        self::$view->hook('cp.layouts.base', function (&$context) {
697
            if (self::$devMode) {
698
                $context['docTitle'] = self::$settings->devModeCpTitlePrefix . $context['docTitle'];
699
            } else {
700
                $context['docTitle'] = self::$settings->cpTitlePrefix . $context['docTitle'];
701
            }
702
        });
703
    }
704
705
    /**
706
     * Install site event listeners for site requests only
707
     */
708
    protected function installSiteEventListeners()
709
    {
710
        // Load the sitemap containers
711
        self::$plugin->sitemaps->loadSitemapContainers();
712
        // Load the frontend template containers
713
        self::$plugin->frontendTemplates->loadFrontendTemplateContainers();
714
        // Handler: UrlManager::EVENT_REGISTER_SITE_URL_RULES
715
        Event::on(
716
            UrlManager::class,
717
            UrlManager::EVENT_REGISTER_SITE_URL_RULES,
718
            function (RegisterUrlRulesEvent $event) {
719
                Craft::debug(
720
                    'UrlManager::EVENT_REGISTER_SITE_URL_RULES',
721
                    __METHOD__
722
                );
723
                // FileController
724
                $route = self::$plugin->handle . '/file/seo-file-link';
725
                $event->rules[self::FRONTEND_SEO_FILE_LINK] = ['route' => $route];
726
                // PreviewController
727
                $route = self::$plugin->handle . '/preview/social-media';
728
                $event->rules[self::FRONTEND_PREVIEW_PATH] = ['route' => $route];
729
                // Register our Control Panel routes
730
                $event->rules = array_merge(
731
                    $event->rules,
732
                    $this->customFrontendRoutes()
733
                );
734
            }
735
        );
736
    }
737
738
    /**
739
     * Return the custom frontend routes
740
     *
741
     * @return array
742
     */
743
    protected function customFrontendRoutes(): array
744
    {
745
        return [
746
        ];
747
    }
748
749
    /**
750
     * Install site event listeners for Control Panel requests only
751
     */
752
    protected function installCpEventListeners()
753
    {
754
        // Load the frontend template containers
755
        self::$plugin->frontendTemplates->loadFrontendTemplateContainers();
756
        // Handler: UrlManager::EVENT_REGISTER_CP_URL_RULES
757
        Event::on(
758
            UrlManager::class,
759
            UrlManager::EVENT_REGISTER_CP_URL_RULES,
760
            function (RegisterUrlRulesEvent $event) {
761
                Craft::debug(
762
                    'UrlManager::EVENT_REGISTER_CP_URL_RULES',
763
                    __METHOD__
764
                );
765
                // Register our Control Panel routes
766
                $event->rules = array_merge(
767
                    $event->rules,
768
                    $this->customAdminCpRoutes()
769
                );
770
            }
771
        );
772
        // Handler: UserPermissions::EVENT_REGISTER_PERMISSIONS
773
        Event::on(
774
            UserPermissions::class,
775
            UserPermissions::EVENT_REGISTER_PERMISSIONS,
776
            function (RegisterUserPermissionsEvent $event) {
777
                Craft::debug(
778
                    'UserPermissions::EVENT_REGISTER_PERMISSIONS',
779
                    __METHOD__
780
                );
781
                // Register our custom permissions
782
                $event->permissions[Craft::t('seomatic', 'SEOmatic')] = $this->customAdminCpPermissions();
783
            }
784
        );
785
        // Handler: AutocompleteService::EVENT_REGISTER_CODEEDITOR_AUTOCOMPLETES
786
        Event::on(AutocompleteService::class, AutocompleteService::EVENT_REGISTER_CODEEDITOR_AUTOCOMPLETES,
787
            function (RegisterCodeEditorAutocompletesEvent $event) {
788
                if ($event->fieldType === self::SEOMATIC_EXPRESSION_FIELD_TYPE) {
789
                    $event->types[] = EnvironmentVariableAutocomplete::class;
790
                }
791
                if ($event->fieldType === self::SEOMATIC_TRACKING_FIELD_TYPE) {
792
                    $event->types[] = TrackingVarsAutocomplete::class;
793
                }
794
            }
795
        );
796
        // Handler: TwigTemplateValidator::EVENT_REGISTER_TWIG_VALIDATOR_VARIABLES
797
        Event::on(TwigTemplateValidator::class,
798
            TwigTemplateValidator::EVENT_REGISTER_TWIG_VALIDATOR_VARIABLES,
799
            function (RegisterTwigValidatorVariablesEvent $event) {
800
                if (Seomatic::$seomaticVariable === null) {
801
                    Seomatic::$seomaticVariable = new SeomaticVariable();
802
                    Seomatic::$plugin->metaContainers->loadGlobalMetaContainers();
803
                    Seomatic::$seomaticVariable->init();
804
                }
805
                $event->variables['seomatic'] = Seomatic::$seomaticVariable;
806
            }
807
        );
808
    }
809
810
    /**
811
     * Return the custom Control Panel routes
812
     *
813
     * @return array
814
     */
815
    protected function customAdminCpRoutes(): array
816
    {
817
        return [
818
            'seomatic' =>
819
                'seomatic/settings/dashboard',
820
            'seomatic/dashboard' =>
821
                'seomatic/settings/dashboard',
822
            'seomatic/dashboard/<siteHandle:{handle}>' =>
823
                'seomatic/settings/dashboard',
824
825
            'seomatic/global' => [
826
                'route' => 'seomatic/settings/global',
827
                'defaults' => ['subSection' => 'general'],
828
            ],
829
            'seomatic/global/<subSection:{handle}>' =>
830
                'seomatic/settings/global',
831
            'seomatic/global/<subSection:{handle}>/<siteHandle:{handle}>' =>
832
                'seomatic/settings/global',
833
834
            'seomatic/content' =>
835
                'seomatic/settings/content',
836
            'seomatic/content/<siteHandle:{handle}>' =>
837
                'seomatic/settings/content',
838
839
            'seomatic/edit-content/<subSection:{handle}>/<sourceBundleType:{handle}>/<sourceHandle:{handle}>' =>
840
                'seomatic/settings/edit-content',
841
            'seomatic/edit-content/<subSection:{handle}>/<sourceBundleType:{handle}>/<sourceHandle:{handle}>/<siteHandle:{handle}>' =>
842
                'seomatic/settings/edit-content',
843
844
            // Seemingly duplicate route needed to handle Solspace Calendar, which allows characters like -'s
845
            // in their handles
846
            'seomatic/edit-content/<subSection:{handle}>/<sourceBundleType:{handle}>/<sourceHandle:{slug}>' =>
847
                'seomatic/settings/edit-content',
848
            'seomatic/edit-content/<subSection:{handle}>/<sourceBundleType:{handle}>/<sourceHandle:{slug}>/<siteHandle:{handle}>' =>
849
                'seomatic/settings/edit-content',
850
851
            'seomatic/site' => [
852
                'route' => 'seomatic/settings/site',
853
                'defaults' => ['subSection' => 'identity'],
854
            ],
855
            'seomatic/site/<subSection:{handle}>' =>
856
                'seomatic/settings/site',
857
            'seomatic/site/<subSection:{handle}>/<siteHandle:{handle}>' =>
858
                'seomatic/settings/site',
859
860
            'seomatic/tracking' => [
861
                'route' => 'seomatic/settings/tracking',
862
                'defaults' => ['subSection' => 'googleAnalytics'],
863
            ],
864
            'seomatic/tracking/<subSection:{handle}>' =>
865
                'seomatic/settings/tracking',
866
            'seomatic/tracking/<subSection:{handle}>/<siteHandle:{handle}>' =>
867
                'seomatic/settings/tracking',
868
869
            'seomatic/plugin' =>
870
                'seomatic/settings/plugin',
871
        ];
872
    }
873
874
    /**
875
     * Returns the custom Control Panel user permissions.
876
     *
877
     * @return array
878
     */
879
    protected function customAdminCpPermissions(): array
880
    {
881
        // The script meta containers for the global meta bundle
882
        try {
883
            $currentSiteId = Craft::$app->getSites()->getCurrentSite()->id ?? 1;
884
        } catch (SiteNotFoundException $e) {
885
            $currentSiteId = 1;
886
        }
887
        // Dynamic permissions for the scripts
888
        $metaBundle = self::$plugin->metaBundles->getGlobalMetaBundle($currentSiteId);
889
        $scriptsPerms = [];
890
        if ($metaBundle !== null) {
891
            $scripts = self::$plugin->metaBundles->getContainerDataFromBundle(
892
                $metaBundle,
893
                MetaScriptContainer::CONTAINER_TYPE
894
            );
895
            foreach ($scripts as $scriptHandle => $scriptData) {
896
                $scriptsPerms["seomatic:tracking-scripts:${scriptHandle}"] = [
897
                    'label' => Craft::t('seomatic', $scriptData->name),
898
                ];
899
            }
900
        }
901
902
        return [
903
            'seomatic:dashboard' => [
904
                'label' => Craft::t('seomatic', 'Dashboard'),
905
            ],
906
            'seomatic:global-meta' => [
907
                'label' => Craft::t('seomatic', 'Edit Global Meta'),
908
                'nested' => [
909
                    'seomatic:global-meta:general' => [
910
                        'label' => Craft::t('seomatic', 'General'),
911
                    ],
912
                    'seomatic:global-meta:twitter' => [
913
                        'label' => Craft::t('seomatic', 'Twitter'),
914
                    ],
915
                    'seomatic:global-meta:facebook' => [
916
                        'label' => Craft::t('seomatic', 'Facebook'),
917
                    ],
918
                    'seomatic:global-meta:robots' => [
919
                        'label' => Craft::t('seomatic', 'Robots'),
920
                    ],
921
                    'seomatic:global-meta:humans' => [
922
                        'label' => Craft::t('seomatic', 'Humans'),
923
                    ],
924
                    'seomatic:global-meta:ads' => [
925
                        'label' => Craft::t('seomatic', 'Ads'),
926
                    ],
927
                    'seomatic:global-meta:security' => [
928
                        'label' => Craft::t('seomatic', 'Security'),
929
                    ],
930
                ],
931
            ],
932
            'seomatic:content-meta' => [
933
                'label' => Craft::t('seomatic', 'Edit Content SEO'),
934
                'nested' => [
935
                    'seomatic:content-meta:general' => [
936
                        'label' => Craft::t('seomatic', 'General'),
937
                    ],
938
                    'seomatic:content-meta:twitter' => [
939
                        'label' => Craft::t('seomatic', 'Twitter'),
940
                    ],
941
                    'seomatic:content-meta:facebook' => [
942
                        'label' => Craft::t('seomatic', 'Facebook'),
943
                    ],
944
                    'seomatic:content-meta:sitemap' => [
945
                        'label' => Craft::t('seomatic', 'Sitemap'),
946
                    ],
947
                ],
948
            ],
949
            'seomatic:site-settings' => [
950
                'label' => Craft::t('seomatic', 'Edit Site Settings'),
951
                'nested' => [
952
                    'seomatic:site-settings:identity' => [
953
                        'label' => Craft::t('seomatic', 'Identity'),
954
                    ],
955
                    'seomatic:site-settings:creator' => [
956
                        'label' => Craft::t('seomatic', 'Creator'),
957
                    ],
958
                    'seomatic:site-settings:social' => [
959
                        'label' => Craft::t('seomatic', 'Social Media'),
960
                    ],
961
                    'seomatic:site-settings:sitemap' => [
962
                        'label' => Craft::t('seomatic', 'Sitemap'),
963
                    ],
964
                    'seomatic:site-settings:miscellaneous' => [
965
                        'label' => Craft::t('seomatic', 'Miscellaneous'),
966
                    ],
967
                ],
968
            ],
969
            'seomatic:tracking-scripts' => [
970
                'label' => Craft::t('seomatic', 'Edit Tracking Scripts'),
971
                'nested' => $scriptsPerms,
972
            ],
973
            'seomatic:plugin-settings' => [
974
                'label' => Craft::t('seomatic', 'Edit Plugin Settings'),
975
            ],
976
        ];
977
    }
978
979
    /**
980
     * Clear all the caches!
981
     */
982
    public function clearAllCaches()
983
    {
984
        // Clear all of SEOmatic's caches
985
        self::$plugin->frontendTemplates->invalidateCaches();
986
        self::$plugin->metaContainers->invalidateCaches();
987
        self::$plugin->sitemaps->invalidateCaches();
988
        SchemaHelper::invalidateCaches();
989
        // If they are using Craft 3.3 or later, clear the GraphQL caches too
990
        if (self::$craft33) {
991
            $gql = Craft::$app->getGql();
992
            if (method_exists($gql, 'invalidateCaches')) {
993
                $gql->invalidateCaches();
994
            }
995
        }
996
        // If the FastCGI Cache Bust plugin is installed, clear its caches too
997
        $plugin = Craft::$app->getPlugins()->getPlugin('fastcgi-cache-bust');
998
        if ($plugin !== null) {
999
            FastcgiCacheBust::$plugin->cache->clearAll();
1000
        }
1001
    }
1002
1003
    /**
1004
     * Returns the custom Control Panel cache options.
1005
     *
1006
     * @return array
1007
     */
1008
    protected function customAdminCpCacheOptions(): array
1009
    {
1010
        return [
1011
            // Frontend template caches
1012
            [
1013
                'key' => 'seomatic-frontendtemplate-caches',
1014
                'label' => Craft::t('seomatic', 'SEOmatic frontend template caches'),
1015
                'action' => [self::$plugin->frontendTemplates, 'invalidateCaches'],
1016
            ],
1017
            // Meta bundle caches
1018
            [
1019
                'key' => 'seomatic-metabundle-caches',
1020
                'label' => Craft::t('seomatic', 'SEOmatic metadata caches'),
1021
                'action' => [self::$plugin->metaContainers, 'invalidateCaches'],
1022
            ],
1023
            // Sitemap caches
1024
            [
1025
                'key' => 'seomatic-sitemap-caches',
1026
                'label' => Craft::t('seomatic', 'SEOmatic sitemap caches'),
1027
                'action' => [self::$plugin->sitemaps, 'invalidateCaches'],
1028
            ],
1029
            // Schema caches
1030
            [
1031
                'key' => 'seomatic-schema-caches',
1032
                'label' => Craft::t('seomatic', 'SEOmatic schema caches'),
1033
                'action' => [SchemaHelper::class, 'invalidateCaches'],
1034
            ],
1035
        ];
1036
    }
1037
1038
    /**
1039
     * @inheritdoc
1040
     */
1041
    public function getSettingsResponse()
1042
    {
1043
        // Just redirect to the plugin settings page
1044
        Craft::$app->getResponse()->redirect(UrlHelper::cpUrl('seomatic/plugin'));
1045
    }
1046
1047
    /**
1048
     * @inheritdoc
1049
     */
1050
    public function getCpNavItem()
1051
    {
1052
        $subNavs = [];
1053
        $navItem = parent::getCpNavItem();
1054
        /** @var User $currentUser */
1055
        $request = Craft::$app->getRequest();
1056
        $siteSuffix = '';
1057
        if ($request->getSegment(1) === 'seomatic') {
1058
            $segments = $request->getSegments();
1059
            $lastSegment = end($segments);
1060
            $site = Craft::$app->getSites()->getSiteByHandle($lastSegment);
1061
            if ($site !== null) {
1062
                $siteSuffix = '/' . $lastSegment;
1063
            }
1064
        }
1065
        $currentUser = Craft::$app->getUser()->getIdentity();
1066
        // Only show sub-navs the user has permission to view
1067
        if ($currentUser->can('seomatic:dashboard')) {
1068
            $subNavs['dashboard'] = [
1069
                'label' => Craft::t('seomatic', 'Dashboard'),
1070
                'url' => 'seomatic/dashboard' . $siteSuffix,
1071
            ];
1072
        }
1073
        if ($currentUser->can('seomatic:global-meta')) {
1074
            $subNavs['global'] = [
1075
                'label' => Craft::t('seomatic', 'Global SEO'),
1076
                'url' => 'seomatic/global/general' . $siteSuffix,
1077
            ];
1078
        }
1079
        if ($currentUser->can('seomatic:content-meta')) {
1080
            $subNavs['content'] = [
1081
                'label' => Craft::t('seomatic', 'Content SEO'),
1082
                'url' => 'seomatic/content' . $siteSuffix,
1083
            ];
1084
        }
1085
        if ($currentUser->can('seomatic:site-settings')) {
1086
            $subNavs['site'] = [
1087
                'label' => Craft::t('seomatic', 'Site Settings'),
1088
                'url' => 'seomatic/site/identity' . $siteSuffix,
1089
            ];
1090
        }
1091
        if ($currentUser->can('seomatic:tracking-scripts')) {
1092
            $subNavs['tracking'] = [
1093
                'label' => Craft::t('seomatic', 'Tracking Scripts'),
1094
                'url' => 'seomatic/tracking/gtag' . $siteSuffix,
1095
            ];
1096
        }
1097
        $editableSettings = true;
1098
        $general = Craft::$app->getConfig()->getGeneral();
1099
        if (self::$craft31 && !$general->allowAdminChanges) {
1100
            $editableSettings = false;
1101
        }
1102
        if ($currentUser->can('seomatic:plugin-settings') && $editableSettings) {
1103
            $subNavs['plugin'] = [
1104
                'label' => Craft::t('seomatic', 'Plugin Settings'),
1105
                'url' => 'seomatic/plugin',
1106
            ];
1107
        }
1108
        $navItem = array_merge($navItem, [
1109
            'subnav' => $subNavs,
1110
        ]);
1111
1112
        return $navItem;
1113
    }
1114
1115
    /**
1116
     * @inheritdoc
1117
     */
1118
    protected function createSettingsModel()
1119
    {
1120
        return new Settings();
1121
    }
1122
}
1123