Issues (231)

src/Seomatic.php (1 issue)

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