Seomatic::createSettingsModel()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 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) 2017 nystudio107
10
 */
11
12
namespace nystudio107\seomatic;
13
14
use Craft;
15
use craft\base\Element;
16
use craft\base\ElementInterface;
0 ignored issues
show
Bug introduced by
The type craft\base\ElementInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
17
use craft\base\Model;
18
use craft\base\Plugin;
19
use craft\elements\Entry;
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 nystudio107\codeeditor\autocompletes\EnvironmentVariableAutocomplete;
49
use nystudio107\codeeditor\events\RegisterCodeEditorAutocompletesEvent;
50
use nystudio107\codeeditor\events\RegisterTwigValidatorVariablesEvent;
51
use nystudio107\codeeditor\services\AutocompleteService;
52
use nystudio107\codeeditor\validators\TwigTemplateValidator;
53
use nystudio107\crafttwigsandbox\helpers\SecurityPolicy;
54
use nystudio107\crafttwigsandbox\web\SandboxView;
55
use nystudio107\fastcgicachebust\FastcgiCacheBust;
56
use nystudio107\seomatic\autocompletes\TrackingVarsAutocomplete;
57
use nystudio107\seomatic\debug\panels\SeomaticPanel;
58
use nystudio107\seomatic\fields\Seomatic_Meta as Seomatic_MetaField;
59
use nystudio107\seomatic\fields\SeoSettings as SeoSettingsField;
60
use nystudio107\seomatic\gql\arguments\SeomaticArguments;
61
use nystudio107\seomatic\gql\interfaces\SeomaticInterface;
62
use nystudio107\seomatic\gql\queries\SeomaticQuery;
63
use nystudio107\seomatic\gql\resolvers\SeomaticResolver;
64
use nystudio107\seomatic\gql\types\SeomaticEnvironmentType;
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\helpers\UrlHelper;
70
use nystudio107\seomatic\integrations\feedme\SeoSettings as SeoSettingsFeedMe;
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\Application as BaseApplication;
77
use yii\base\Event;
78
use yii\base\View as BaseView;
79
use yii\debug\Module;
80
81
/** @noinspection MissingPropertyAnnotationsInspection */
82
83
/**
84
 * Class Seomatic
85
 *
86
 * @author    nystudio107
87
 * @package   Seomatic
88
 * @since     3.0.0
89
 */
90
class Seomatic extends Plugin
91
{
92
    // Traits
93
    // =========================================================================
94
95
    use ServicesTrait;
96
97
    // Constants
98
    // =========================================================================
99
100
    public const SEOMATIC_HANDLE = 'Seomatic';
101
102
    public const DEVMODE_CACHE_DURATION = 30;
103
104
    public const SEOMATIC_PREVIEW_AUTHORIZATION_KEY = 'seomaticPreviewAuthorizationKey';
105
106
    protected const FRONTEND_SEO_FILE_LINK = 'seomatic/seo-file-link/<url:[^\/]+>/<robots:[^\/]+>/<canonical:[^\/]+>/<inline:\d+>/<fileName:[-\w\.*]+>';
107
108
    protected const FRONTEND_PREVIEW_PATH = 'seomatic/preview-social-media';
109
110
    public const SEOMATIC_EXPRESSION_FIELD_TYPE = 'SeomaticExpressionField';
111
    public const SEOMATIC_TRACKING_FIELD_TYPE = 'SeomaticTrackingField';
112
113
    // Static Properties
114
    // =========================================================================
115
116
    /**
117
     * @var ?Seomatic
118
     */
119
    public static ?Seomatic $plugin;
120
121
    /**
122
     * @var null|SeomaticVariable
123
     */
124
    public static ?SeomaticVariable $seomaticVariable = null;
125
126
    /**
127
     * @var null|Settings
128
     */
129
    public static ?Settings $settings = null;
130
131
    /**
132
     * @var null|ElementInterface
133
     */
134
    public static ?ElementInterface $matchedElement = null;
135
136
    /**
137
     * @var bool
138
     */
139
    public static bool $devMode;
140
141
    /**
142
     * @var null|View
143
     */
144
    public static ?View $view = null;
145
146
    /**
147
     * @var null|View
148
     */
149
    public static ?View $sandboxView = null;
150
151
    /**
152
     * @var string
153
     */
154
    public static string $language = '';
155
156
    /**
157
     * @var string
158
     */
159
    public static string $environment = '';
160
161
    /**
162
     * @var int
163
     */
164
    public static int $cacheDuration = 0;
165
166
    /**
167
     * @var bool
168
     */
169
    public static bool $previewingMetaContainers = false;
170
171
    /**
172
     * @var bool
173
     */
174
    public static bool $loadingMetaContainers = false;
175
176
    /**
177
     * @var bool
178
     */
179
    public static bool $savingSettings = false;
180
181
    /**
182
     * @var bool
183
     */
184
    public static bool $headlessRequest = false;
185
186
    // Public Properties
187
    // =========================================================================
188
189
    /**
190
     * @var string
191
     */
192
    public string $schemaVersion = '3.0.13';
193
194
    /**
195
     * @var bool
196
     */
197
    public bool $hasCpSection = true;
198
199
    /**
200
     * @var bool
201
     */
202
    public bool $hasCpSettings = true;
203
204
    /**
205
     * Set the matched element
206
     *
207
     * @param $element null|ElementInterface
208
     */
209
    public static function setMatchedElement(?ElementInterface $element): void
210
    {
211
        self::$matchedElement = $element;
212
        /** @var  $element Element */
213
        if ($element) {
0 ignored issues
show
introduced by
$element is of type craft\base\Element, thus it always evaluated to true.
Loading history...
214
            self::$language = MetaValueHelper::getSiteLanguage($element->siteId);
215
        } else {
216
            self::$language = MetaValueHelper::getSiteLanguage(null);
217
        }
218
        MetaValueHelper::cache();
219
    }
220
221
    // Public Methods
222
    // =========================================================================
223
224
    /**
225
     * @inheritdoc
226
     */
227
    public function init(): void
228
    {
229
        parent::init();
230
        self::$plugin = $this;
231
        // Handle any console commands
232
        $request = Craft::$app->getRequest();
233
        if ($request->getIsConsoleRequest()) {
234
            $this->controllerNamespace = 'nystudio107\seomatic\console\controllers';
235
        }
236
        // Initialize properties
237
        /** @var Settings $settings */
238
        $settings = self::$plugin->getSettings();
239
        self::$settings = $settings;
240
        self::$devMode = Craft::$app->getConfig()->getGeneral()->devMode;
241
        self::$view = Craft::$app->getView();
242
        // Use a Twig sandbox for SEOmatic rendering
243
        $securityPolicy = SecurityPolicy::createFromFile('seomatic-sandbox', '@nystudio107/seomatic');
244
        self::$sandboxView = new SandboxView(['securityPolicy' => $securityPolicy]);
245
        self::$cacheDuration = self::$devMode
246
            ? self::DEVMODE_CACHE_DURATION
247
            : self::$settings->metaCacheDuration ?? 0;
248
        self::$environment = EnvironmentHelper::determineEnvironment();
249
        MetaValueHelper::cache();
250
        $this->name = self::$settings->pluginName;
251
        // Install our event listeners
252
        $this->installEventListeners();
253
        // We're loaded
254
        Craft::info(
255
            Craft::t(
256
                'seomatic',
257
                '{name} plugin loaded',
258
                ['name' => $this->name]
259
            ),
260
            __METHOD__
261
        );
262
    }
263
264
    /**
265
     * @inheritdoc
266
     */
267
    public function getSettings(): ?Model
268
    {
269
        // For all the emojis
270
        /* @var Settings $settingsModel */
271
        $settingsModel = parent::getSettings();
272
        if ($settingsModel !== null && !self::$savingSettings) {
273
            $attributes = $settingsModel->attributes();
274
            if ($attributes !== null) {
0 ignored issues
show
introduced by
The condition $attributes !== null is always true.
Loading history...
275
                foreach ($attributes as $attribute) {
276
                    if (is_string($settingsModel->$attribute)) {
277
                        $settingsModel->$attribute = html_entity_decode(
278
                            $settingsModel->$attribute,
279
                            ENT_NOQUOTES,
280
                            'UTF-8'
281
                        );
282
                    }
283
                }
284
            }
285
            self::$savingSettings = false;
286
        }
287
288
        return $settingsModel;
289
    }
290
291
    /**
292
     * Determine whether our table schema exists or not; this is needed because
293
     * migrations such as the install migration and base_install migration may
294
     * not have been run by the time our init() method has been called
295
     *
296
     * @return bool
297
     */
298
    public function migrationsAndSchemaReady(): bool
299
    {
300
        $pluginsService = Craft::$app->getPlugins();
301
        if ($pluginsService->isPluginUpdatePending(self::$plugin)) {
302
            return false;
303
        }
304
        if (Craft::$app->db->schema->getTableSchema('{{%seomatic_metabundles}}') === null) {
305
            return false;
306
        }
307
308
        return true;
309
    }
310
311
    /**
312
     * Clear all the caches!
313
     */
314
    public function clearAllCaches(): void
315
    {
316
        // Clear all of SEOmatic's caches
317
        self::$plugin->frontendTemplates->invalidateCaches();
318
        self::$plugin->metaContainers->invalidateCaches();
319
        self::$plugin->sitemaps->invalidateCaches();
320
        SchemaHelper::invalidateCaches();
321
        // Clear the GraphQL caches too
322
        $gql = Craft::$app->getGql();
323
        if (method_exists($gql, 'invalidateCaches')) {
324
            $gql->invalidateCaches();
325
        }
326
        // If the FastCGI Cache Bust plugin is installed, clear its caches too
327
        /** @var ?FastcgiCacheBust $plugin */
328
        $plugin = Craft::$app->getPlugins()->getPlugin('fastcgi-cache-bust');
329
        if ($plugin !== null) {
330
            $plugin->cache->clearAll();
331
        }
332
    }
333
334
    // Protected Methods
335
    // =========================================================================
336
337
    /**
338
     * @inheritdoc
339
     */
340
    public function getSettingsResponse(): mixed
341
    {
342
        // Just redirect to the plugin settings page
343
        return Craft::$app->getResponse()->redirect(UrlHelper::cpUrl('seomatic/plugin'));
344
    }
345
346
    /**
347
     * @inheritdoc
348
     */
349
    public function getCpNavItem(): ?array
350
    {
351
        $subNavs = [];
352
        $navItem = parent::getCpNavItem();
353
        $request = Craft::$app->getRequest();
354
        $siteSuffix = '';
355
        if ($request->getSegment(1) === 'seomatic') {
356
            $segments = $request->getSegments();
357
            $lastSegment = end($segments);
358
            $site = Craft::$app->getSites()->getSiteByHandle($lastSegment);
359
            if ($site !== null) {
360
                $siteSuffix = '/' . $lastSegment;
361
            }
362
        }
363
        $currentUser = Craft::$app->getUser()->getIdentity();
364
        // Only show sub-navs the user has permission to view
365
        if ($currentUser->can('seomatic:dashboard')) {
0 ignored issues
show
Bug introduced by
The method can() does not exist on yii\web\IdentityInterface. It seems like you code against a sub-type of yii\web\IdentityInterface such as craft\elements\User. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

365
        if ($currentUser->/** @scrutinizer ignore-call */ can('seomatic:dashboard')) {
Loading history...
366
            $subNavs['dashboard'] = [
367
                'label' => Craft::t('seomatic', 'Dashboard'),
368
                'url' => 'seomatic/dashboard' . $siteSuffix,
369
            ];
370
        }
371
        if ($currentUser->can('seomatic:global-meta')) {
372
            $subNavs['global'] = [
373
                'label' => Craft::t('seomatic', 'Global SEO'),
374
                'url' => 'seomatic/global/general' . $siteSuffix,
375
            ];
376
        }
377
        if ($currentUser->can('seomatic:content-meta')) {
378
            $subNavs['content'] = [
379
                'label' => Craft::t('seomatic', 'Content SEO'),
380
                'url' => 'seomatic/content' . $siteSuffix,
381
            ];
382
        }
383
        if ($currentUser->can('seomatic:site-settings')) {
384
            $subNavs['site'] = [
385
                'label' => Craft::t('seomatic', 'Site Settings'),
386
                'url' => 'seomatic/site/identity' . $siteSuffix,
387
            ];
388
        }
389
        if ($currentUser->can('seomatic:tracking-scripts')) {
390
            $subNavs['tracking'] = [
391
                'label' => Craft::t('seomatic', 'Tracking Scripts'),
392
                'url' => 'seomatic/tracking/gtag' . $siteSuffix,
393
            ];
394
        }
395
        $editableSettings = true;
396
        $general = Craft::$app->getConfig()->getGeneral();
397
        if (!$general->allowAdminChanges) {
398
            $editableSettings = false;
399
        }
400
        if ($editableSettings && $currentUser->can('seomatic:plugin-settings')) {
401
            $subNavs['plugin'] = [
402
                'label' => Craft::t('seomatic', 'Plugin Settings'),
403
                'url' => 'seomatic/plugin',
404
            ];
405
        }
406
        // 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
407
        if (empty($subNavs)) {
408
            return null;
409
        }
410
        // A single sub nav item is redundant
411
        if (count($subNavs) === 1) {
412
            $subNavs = [];
413
        }
414
415
        return array_merge($navItem, [
416
            'subnav' => $subNavs,
417
        ]);
418
    }
419
420
    /**
421
     * Install our event listeners.
422
     */
423
    protected function installEventListeners(): void
424
    {
425
        // Install our event listeners only if our table schema exists
426
        if ($this->migrationsAndSchemaReady()) {
427
            // Add in our Twig extensions
428
            $seomaticTwigExtension = new SeomaticTwigExtension();
429
            self::$view->registerTwigExtension($seomaticTwigExtension);
0 ignored issues
show
Bug introduced by
The method registerTwigExtension() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

429
            self::$view->/** @scrutinizer ignore-call */ 
430
                         registerTwigExtension($seomaticTwigExtension);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
430
            self::$sandboxView->registerTwigExtension($seomaticTwigExtension);
0 ignored issues
show
Bug introduced by
The method registerTwigExtension() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

430
            self::$sandboxView->/** @scrutinizer ignore-call */ 
431
                                registerTwigExtension($seomaticTwigExtension);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
431
            $request = Craft::$app->getRequest();
432
            // Add in our event listeners that are needed for every request
433
            $this->installGlobalEventListeners();
434
            // Install only for non-console site requests
435
            if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest()) {
436
                $this->installSiteEventListeners();
437
            }
438
            // Install only for non-console Control Panel requests
439
            if ($request->getIsCpRequest() && !$request->getIsConsoleRequest()) {
440
                $this->installCpEventListeners();
441
            }
442
        }
443
        // Handler: EVENT_AFTER_INSTALL_PLUGIN
444
        Event::on(
445
            Plugins::class,
446
            Plugins::EVENT_AFTER_INSTALL_PLUGIN,
447
            function(PluginEvent $event) {
448
                if ($event->plugin === $this) {
0 ignored issues
show
introduced by
The condition $event->plugin === $this is always false.
Loading history...
449
                    // Invalidate our caches after we've been installed
450
                    $this->clearAllCaches();
451
                    // Send them to our welcome screen
452
                    $request = Craft::$app->getRequest();
453
                    if ($request->isCpRequest) {
454
                        Craft::$app->getResponse()->redirect(UrlHelper::cpUrl(
455
                            'seomatic/dashboard',
456
                            [
457
                                'showWelcome' => true,
458
                            ]
459
                        ))->send();
460
                    }
461
                }
462
            }
463
        );
464
        // Handler: ClearCaches::EVENT_REGISTER_CACHE_OPTIONS
465
        Event::on(
466
            ClearCaches::class,
467
            ClearCaches::EVENT_REGISTER_CACHE_OPTIONS,
468
            function(RegisterCacheOptionsEvent $event) {
469
                Craft::debug(
470
                    'ClearCaches::EVENT_REGISTER_CACHE_OPTIONS',
471
                    __METHOD__
472
                );
473
                // Register our Cache Options
474
                $event->options = array_merge(
475
                    $event->options,
476
                    $this->customAdminCpCacheOptions()
477
                );
478
            }
479
        );
480
        // Handler: EVENT_BEFORE_SAVE_PLUGIN_SETTINGS
481
        Event::on(
482
            Plugins::class,
483
            Plugins::EVENT_BEFORE_SAVE_PLUGIN_SETTINGS,
484
            function(PluginEvent $event) {
485
                if ($event->plugin === $this && !Craft::$app->getDb()->getSupportsMb4()) {
0 ignored issues
show
introduced by
The condition $event->plugin === $this is always false.
Loading history...
486
                    // For all the emojis
487
                    $settingsModel = $this->getSettings();
488
                    self::$savingSettings = true;
489
                    if ($settingsModel !== null) {
490
                        $attributes = $settingsModel->attributes();
491
                        if ($attributes !== null) {
492
                            foreach ($attributes as $attribute) {
493
                                if (is_string($settingsModel->$attribute)) {
494
                                    $settingsModel->$attribute =
495
                                        StringHelper::encodeMb4($settingsModel->$attribute);
496
                                }
497
                            }
498
                        }
499
                    }
500
                }
501
            }
502
        );
503
    }
504
505
    /**
506
     * Install global event listeners for all request types
507
     */
508
    protected function installGlobalEventListeners(): void
509
    {
510
        // Handler: Plugins::EVENT_AFTER_LOAD_PLUGINS
511
        Event::on(
512
            Plugins::class,
513
            Plugins::EVENT_AFTER_LOAD_PLUGINS,
514
            function() {
515
                // Delay registering SEO Elements to give other plugins a chance to load first
516
                $this->seoElements->getAllSeoElementTypes(false);
517
                // Delay installing GQL handlers to give other plugins a chance to register their own first
518
                $this->installGqlHandlers();
519
                // Install these only after all other plugins have loaded
520
                $request = Craft::$app->getRequest();
521
                // Only respond to non-console site requests
522
                if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest()) {
523
                    $this->handleSiteRequest();
524
                }
525
                // Respond to Control Panel requests
526
                if ($request->getIsCpRequest() && !$request->getIsConsoleRequest()) {
527
                    $this->handleAdminCpRequest();
528
                }
529
            }
530
        );
531
        // Handler: Fields::EVENT_REGISTER_FIELD_TYPES
532
        Event::on(
533
            Fields::class,
534
            Fields::EVENT_REGISTER_FIELD_TYPES,
535
            static function(RegisterComponentTypesEvent $event) {
536
                $event->types[] = SeoSettingsField::class;
537
                $event->types[] = Seomatic_MetaField::class;
538
            }
539
        );
540
        // Handler: Element::EVENT_AFTER_PROPAGATE
541
        Event::on(
542
            Element::class,
543
            Element::EVENT_AFTER_PROPAGATE,
544
            static function(ModelEvent $event) {
545
                Craft::debug(
546
                    'Element::EVENT_AFTER_PROPAGATE',
547
                    __METHOD__
548
                );
549
                /** @var Element $element */
550
                $element = $event->sender;
551
                self::$plugin->metaBundles->invalidateMetaBundleByElement(
552
                    $element,
553
                    $event->isNew
554
                );
555
                if ($event->isNew) {
556
                    self::$plugin->sitemaps->submitSitemapForElement($element);
557
                }
558
            }
559
        );
560
        // Handler: Elements::EVENT_AFTER_DELETE_ELEMENT
561
        Event::on(
562
            Elements::class,
563
            Elements::EVENT_AFTER_DELETE_ELEMENT,
564
            static function(ElementEvent $event) {
565
                Craft::debug(
566
                    'Elements::EVENT_AFTER_DELETE_ELEMENT',
567
                    __METHOD__
568
                );
569
                /** @var Element $element */
570
                $element = $event->element;
571
                self::$plugin->metaBundles->invalidateMetaBundleByElement(
572
                    $element,
573
                    false
574
                );
575
            }
576
        );
577
        // Add social media preview targets on Craft 3.2 or later
578
        if (self::$settings->socialMediaPreviewTarget) {
579
            // Handler: Entry::EVENT_REGISTER_PREVIEW_TARGETS
580
            Event::on(
581
                Entry::class,
582
                Element::EVENT_REGISTER_PREVIEW_TARGETS,
583
                static function(RegisterPreviewTargetsEvent $e) {
584
                    /** @var Element $element */
585
                    $element = $e->sender;
586
                    if ($element->uri !== null) {
587
                        $e->previewTargets[] = [
588
                            'label' => '📣 ' . Craft::t('seomatic', 'Social Media Preview'),
589
                            'url' => UrlHelper::siteUrl(self::FRONTEND_PREVIEW_PATH, [
590
                                'elementId' => $element->id,
591
                                'siteId' => $element->siteId,
592
                            ]),
593
                        ];
594
                        // Don't allow the preview to be accessed publicly
595
                        Craft::$app->getSession()->authorize(self::SEOMATIC_PREVIEW_AUTHORIZATION_KEY . $element->id);
596
                    }
597
                }
598
            );
599
        }
600
        // Yii2 Debug Toolbar support
601
        Event::on(
602
            Application::class,
603
            BaseApplication::EVENT_BEFORE_REQUEST,
604
            static function() {
605
                /** @var Module|null $debugModule */
606
                $debugModule = Seomatic::$settings->enableDebugToolbarPanel ? Craft::$app->getModule('debug') : null;
607
608
                if ($debugModule) {
0 ignored issues
show
introduced by
$debugModule is of type yii\debug\Module, thus it always evaluated to true.
Loading history...
609
                    $debugModule->panels['seomatic'] = new SeomaticPanel([
610
                        'id' => 'seomatic',
611
                        'module' => $debugModule,
612
                    ]);
613
                }
614
            }
615
        );
616
        // FeedMe Support
617
        if (class_exists(FeedMe::class)) {
618
            Event::on(
619
                FeedMeFields::class,
620
                FeedMeFields::EVENT_REGISTER_FEED_ME_FIELDS,
621
                static function(RegisterFeedMeFieldsEvent $e) {
622
                    Craft::debug(
623
                        'FeedMeFields::EVENT_REGISTER_FEED_ME_FIELDS',
624
                        __METHOD__
625
                    );
626
                    $e->fields[] = SeoSettingsFeedMe::class;
627
                }
628
            );
629
        }
630
        $updateMetaBundles = static function($message) {
631
            Craft::debug(
632
                $message,
633
                __METHOD__
634
            );
635
            $seoElementTypes = Seomatic::$plugin->seoElements->getAllSeoElementTypes();
636
            foreach ($seoElementTypes as $seoElementType) {
637
                $metaBundleType = $seoElementType::META_BUNDLE_TYPE ?? '';
638
639
                if ($metaBundleType) {
640
                    Seomatic::$plugin->metaBundles->resaveMetaBundles($metaBundleType);
641
                }
642
            }
643
        };
644
645
        // Handler: Elements::EVENT_AFTER_SAVE_SITE
646
        Event::on(
647
            SitesService::class,
648
            SitesService::EVENT_AFTER_SAVE_SITE,
649
            static function() use ($updateMetaBundles) {
650
                $updateMetaBundles('SitesService::EVENT_AFTER_SAVE_SITE');
651
            }
652
        );
653
654
        // Handler: Elements::EVENT_AFTER_DELETE_SITE
655
        Event::on(
656
            SitesService::class,
657
            SitesService::EVENT_AFTER_DELETE_SITE,
658
            static function() use ($updateMetaBundles) {
659
                $updateMetaBundles('SitesService::EVENT_AFTER_DELETE_SITE');
660
            }
661
        );
662
    }
663
664
    /**
665
     * Register our GraphQL handlers
666
     *
667
     * @return void
668
     */
669
    protected function installGqlHandlers(): void
670
    {
671
        // Handler: Gql::EVENT_REGISTER_GQL_TYPES
672
        Event::on(
673
            Gql::class,
674
            Gql::EVENT_REGISTER_GQL_TYPES,
675
            static function(RegisterGqlTypesEvent $event) {
676
                Craft::debug(
677
                    'Gql::EVENT_REGISTER_GQL_TYPES',
678
                    __METHOD__
679
                );
680
                $event->types[] = SeomaticInterface::class;
681
                $event->types[] = SeomaticEnvironmentType::class;
682
            }
683
        );
684
        // Handler: Gql::EVENT_REGISTER_GQL_QUERIES
685
        Event::on(
686
            Gql::class,
687
            Gql::EVENT_REGISTER_GQL_QUERIES,
688
            static function(RegisterGqlQueriesEvent $event) {
689
                Craft::debug(
690
                    'Gql::EVENT_REGISTER_GQL_QUERIES',
691
                    __METHOD__
692
                );
693
                $queries = SeomaticQuery::getQueries();
694
                foreach ($queries as $key => $value) {
695
                    $event->queries[$key] = $value;
696
                }
697
            }
698
        );
699
        // Handler: Gql::EVENT_REGISTER_GQL_SCHEMA_COMPONENTS
700
        Event::on(
701
            Gql::class,
702
            Gql::EVENT_REGISTER_GQL_SCHEMA_COMPONENTS,
703
            static function(RegisterGqlSchemaComponentsEvent $event) {
704
                Craft::debug(
705
                    'Gql::EVENT_REGISTER_GQL_SCHEMA_COMPONENTS',
706
                    __METHOD__
707
                );
708
                $label = Craft::t('seomatic', 'Seomatic');
709
                $event->queries[$label]['seomatic.all:read'] = ['label' => Craft::t('seomatic', 'Query Seomatic data')];
710
            }
711
        );
712
        // Add support for querying for SEOmatic metadata inside of element queries
713
        // Handler: TypeManager::EVENT_DEFINE_GQL_TYPE_FIELDS
714
        $knownInterfaceNames = self::$plugin->seoElements->getAllSeoElementGqlInterfaceNames();
715
        Event::on(
716
            TypeManager::class,
717
            TypeManager::EVENT_DEFINE_GQL_TYPE_FIELDS,
718
            static function(DefineGqlTypeFieldsEvent $event) use ($knownInterfaceNames) {
719
                if (in_array($event->typeName, $knownInterfaceNames, true)) {
720
                    Craft::debug(
721
                        'TypeManager::EVENT_DEFINE_GQL_TYPE_FIELDS',
722
                        __METHOD__
723
                    );
724
725
                    if (GqlHelper::canQuerySeo()) {
726
                        // Make Seomatic tags available to all entries.
727
                        $event->fields['seomatic'] = [
728
                            'name' => 'seomatic',
729
                            'type' => SeomaticInterface::getType(),
730
                            'args' => SeomaticArguments::getArguments(),
731
                            'resolve' => SeomaticResolver::class . '::resolve',
732
                            'description' => Craft::t('seomatic', 'This query is used to query for SEOmatic meta data.'),
733
                        ];
734
                    }
735
                }
736
            });
737
    }
738
739
    /**
740
     * Handle site requests.  We do it only after we receive the event
741
     * EVENT_AFTER_LOAD_PLUGINS so that any pending db migrations can be run
742
     * before our event listeners kick in
743
     */
744
    protected function handleSiteRequest(): void
745
    {
746
        // Handler: View::EVENT_END_PAGE
747
        Event::on(
748
            View::class,
749
            BaseView::EVENT_END_PAGE,
750
            static function() {
751
                Craft::debug(
752
                    'View::EVENT_END_PAGE',
753
                    __METHOD__
754
                );
755
                // The page is done rendering, include our meta containers
756
                if (self::$settings->renderEnabled && self::$seomaticVariable) {
757
                    self::$plugin->metaContainers->includeMetaContainers();
758
                }
759
            }
760
        );
761
    }
762
763
    /**
764
     * Handle Control Panel requests. We do it only after we receive the event
765
     * EVENT_AFTER_LOAD_PLUGINS so that any pending db migrations can be run
766
     * before our event listeners kick in
767
     */
768
    protected function handleAdminCpRequest(): void
769
    {
770
        // Don't cache Control Panel requests
771
        self::$cacheDuration = 1;
772
        // Prefix the Control Panel title
773
        self::$view->hook('cp.layouts.base', function(&$context) {
774
            if (self::$devMode) {
775
                $context['docTitle'] = self::$settings->devModeCpTitlePrefix . $context['docTitle'];
776
            } else {
777
                $context['docTitle'] = self::$settings->cpTitlePrefix . $context['docTitle'];
778
            }
779
        });
780
    }
781
782
    /**
783
     * Install site event listeners for site requests only
784
     */
785
    protected function installSiteEventListeners(): void
786
    {
787
        // Load the sitemap containers
788
        self::$plugin->sitemaps->loadSitemapContainers();
789
        // Load the frontend template containers
790
        self::$plugin->frontendTemplates->loadFrontendTemplateContainers();
791
        // Handler: UrlManager::EVENT_REGISTER_SITE_URL_RULES
792
        Event::on(
793
            UrlManager::class,
794
            UrlManager::EVENT_REGISTER_SITE_URL_RULES,
795
            function(RegisterUrlRulesEvent $event) {
796
                Craft::debug(
797
                    'UrlManager::EVENT_REGISTER_SITE_URL_RULES',
798
                    __METHOD__
799
                );
800
                // FileController
801
                $route = self::$plugin->handle . '/file/seo-file-link';
802
                $event->rules[self::FRONTEND_SEO_FILE_LINK] = ['route' => $route];
803
                // PreviewController
804
                $route = self::$plugin->handle . '/preview/social-media';
805
                $event->rules[self::FRONTEND_PREVIEW_PATH] = ['route' => $route];
806
                // Register our Control Panel routes
807
                $event->rules = array_merge(
808
                    $event->rules,
809
                    $this->customFrontendRoutes()
810
                );
811
            }
812
        );
813
    }
814
815
    /**
816
     * Return the custom frontend routes
817
     *
818
     * @return array
819
     */
820
    protected function customFrontendRoutes(): array
821
    {
822
        return [
823
        ];
824
    }
825
826
    /**
827
     * Install site event listeners for Control Panel requests only
828
     */
829
    protected function installCpEventListeners(): void
830
    {
831
        // Load the frontend template containers
832
        self::$plugin->frontendTemplates->loadFrontendTemplateContainers();
833
        // Handler: UrlManager::EVENT_REGISTER_CP_URL_RULES
834
        Event::on(
835
            UrlManager::class,
836
            UrlManager::EVENT_REGISTER_CP_URL_RULES,
837
            function(RegisterUrlRulesEvent $event) {
838
                Craft::debug(
839
                    'UrlManager::EVENT_REGISTER_CP_URL_RULES',
840
                    __METHOD__
841
                );
842
                // Register our Control Panel routes
843
                $event->rules = array_merge(
844
                    $event->rules,
845
                    $this->customAdminCpRoutes()
846
                );
847
            }
848
        );
849
        // Handler: UserPermissions::EVENT_REGISTER_PERMISSIONS
850
        Event::on(
851
            UserPermissions::class,
852
            UserPermissions::EVENT_REGISTER_PERMISSIONS,
853
            function(RegisterUserPermissionsEvent $event) {
854
                Craft::debug(
855
                    'UserPermissions::EVENT_REGISTER_PERMISSIONS',
856
                    __METHOD__
857
                );
858
                // Register our custom permissions
859
                $event->permissions[] = [
860
                    'heading' => Craft::t('seomatic', 'SEOmatic'),
861
                    'permissions' => $this->customAdminCpPermissions(),
862
                ];
863
            }
864
        );
865
        // Handler: AutocompleteService::EVENT_REGISTER_CODEEDITOR_AUTOCOMPLETES
866
        Event::on(AutocompleteService::class, AutocompleteService::EVENT_REGISTER_CODEEDITOR_AUTOCOMPLETES,
867
            function(RegisterCodeEditorAutocompletesEvent $event) {
868
                if ($event->fieldType === self::SEOMATIC_EXPRESSION_FIELD_TYPE) {
869
                    $event->types[] = EnvironmentVariableAutocomplete::class;
870
                }
871
                if ($event->fieldType === self::SEOMATIC_TRACKING_FIELD_TYPE) {
872
                    $event->types[] = TrackingVarsAutocomplete::class;
873
                }
874
            }
875
        );
876
        // Handler: TwigTemplateValidator::EVENT_REGISTER_TWIG_VALIDATOR_VARIABLES
877
        Event::on(TwigTemplateValidator::class,
878
            TwigTemplateValidator::EVENT_REGISTER_TWIG_VALIDATOR_VARIABLES,
879
            function(RegisterTwigValidatorVariablesEvent $event) {
880
                if (Seomatic::$seomaticVariable === null) {
881
                    Seomatic::$seomaticVariable = new SeomaticVariable();
882
                    Seomatic::$plugin->metaContainers->loadGlobalMetaContainers();
883
                    Seomatic::$seomaticVariable->init();
0 ignored issues
show
Bug introduced by
The method init() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

883
                    Seomatic::$seomaticVariable->/** @scrutinizer ignore-call */ 
884
                                                 init();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
884
                }
885
                $event->variables['seomatic'] = Seomatic::$seomaticVariable;
886
            }
887
        );
888
    }
889
890
    /**
891
     * Return the custom Control Panel routes
892
     *
893
     * @return array
894
     */
895
    protected function customAdminCpRoutes(): array
896
    {
897
        return [
898
            'seomatic' =>
899
                '',
900
            'seomatic/dashboard' =>
901
                'seomatic/settings/dashboard',
902
            'seomatic/dashboard/<siteHandle:{handle}>' =>
903
                'seomatic/settings/dashboard',
904
905
            'seomatic/global' => [
906
                'route' => 'seomatic/settings/global',
907
                'defaults' => ['subSection' => 'general'],
908
            ],
909
            'seomatic/global/<subSection:{handle}>' =>
910
                'seomatic/settings/global',
911
            'seomatic/global/<subSection:{handle}>/<siteHandle:{handle}>' =>
912
                'seomatic/settings/global',
913
914
            'seomatic/content' =>
915
                'seomatic/settings/content',
916
            'seomatic/content/<siteHandle:{handle}>' =>
917
                'seomatic/settings/content',
918
919
            'seomatic/edit-content/<subSection:{handle}>/<sourceBundleType:{handle}>/<sourceHandle:{handle}>' =>
920
                'seomatic/settings/edit-content',
921
            'seomatic/edit-content/<subSection:{handle}>/<sourceBundleType:{handle}>/<sourceHandle:{handle}>/<siteHandle:{handle}>' =>
922
                'seomatic/settings/edit-content',
923
924
            // Seemingly duplicate route needed to handle Solspace Calendar, which allows characters like -'s
925
            // in their handles
926
            'seomatic/edit-content/<subSection:{handle}>/<sourceBundleType:{handle}>/<sourceHandle:{slug}>' =>
927
                'seomatic/settings/edit-content',
928
            'seomatic/edit-content/<subSection:{handle}>/<sourceBundleType:{handle}>/<sourceHandle:{slug}>/<siteHandle:{handle}>' =>
929
                'seomatic/settings/edit-content',
930
931
            'seomatic/site' => [
932
                'route' => 'seomatic/settings/site',
933
                'defaults' => ['subSection' => 'identity'],
934
            ],
935
            'seomatic/site/<subSection:{handle}>' =>
936
                'seomatic/settings/site',
937
            'seomatic/site/<subSection:{handle}>/<siteHandle:{handle}>' =>
938
                'seomatic/settings/site',
939
940
            'seomatic/tracking' => [
941
                'route' => 'seomatic/settings/tracking',
942
                'defaults' => ['subSection' => 'googleAnalytics'],
943
            ],
944
            'seomatic/tracking/<subSection:{handle}>' =>
945
                'seomatic/settings/tracking',
946
            'seomatic/tracking/<subSection:{handle}>/<siteHandle:{handle}>' =>
947
                'seomatic/settings/tracking',
948
949
            'seomatic/plugin' =>
950
                'seomatic/settings/plugin',
951
        ];
952
    }
953
954
    /**
955
     * Returns the custom Control Panel user permissions.
956
     *
957
     * @return array
958
     * @noinspection PhpArrayShapeAttributeCanBeAddedInspection
959
     */
960
    protected function customAdminCpPermissions(): array
961
    {
962
        // The script meta containers for the global meta bundle
963
        try {
964
            $currentSiteId = Craft::$app->getSites()->getCurrentSite()->id ?? 1;
965
        } catch (SiteNotFoundException $e) {
966
            $currentSiteId = 1;
967
            Craft::error($e->getMessage(), __METHOD__);
968
        }
969
        // Dynamic permissions for the scripts
970
        $metaBundle = self::$plugin->metaBundles->getGlobalMetaBundle($currentSiteId);
971
        $scriptsPerms = [];
972
        if ($metaBundle !== null) {
973
            $scripts = self::$plugin->metaBundles->getContainerDataFromBundle(
974
                $metaBundle,
975
                MetaScriptContainer::CONTAINER_TYPE
976
            );
977
            foreach ($scripts as $scriptHandle => $scriptData) {
978
                $scriptsPerms["seomatic:tracking-scripts:$scriptHandle"] = [
979
                    'label' => Craft::t('seomatic', $scriptData->name),
980
                ];
981
            }
982
        }
983
984
        return [
985
            'seomatic:dashboard' => [
986
                'label' => Craft::t('seomatic', 'Dashboard'),
987
            ],
988
            'seomatic:global-meta' => [
989
                'label' => Craft::t('seomatic', 'Edit Global Meta'),
990
                'nested' => [
991
                    'seomatic:global-meta:general' => [
992
                        'label' => Craft::t('seomatic', 'General'),
993
                    ],
994
                    'seomatic:global-meta:twitter' => [
995
                        'label' => Craft::t('seomatic', 'Twitter'),
996
                    ],
997
                    'seomatic:global-meta:facebook' => [
998
                        'label' => Craft::t('seomatic', 'Facebook'),
999
                    ],
1000
                    'seomatic:global-meta:robots' => [
1001
                        'label' => Craft::t('seomatic', 'Robots'),
1002
                    ],
1003
                    'seomatic:global-meta:humans' => [
1004
                        'label' => Craft::t('seomatic', 'Humans'),
1005
                    ],
1006
                    'seomatic:global-meta:ads' => [
1007
                        'label' => Craft::t('seomatic', 'Ads'),
1008
                    ],
1009
                    'seomatic:global-meta:security' => [
1010
                        'label' => Craft::t('seomatic', 'Security'),
1011
                    ],
1012
                ],
1013
            ],
1014
            'seomatic:content-meta' => [
1015
                'label' => Craft::t('seomatic', 'Edit Content SEO'),
1016
                'nested' => [
1017
                    'seomatic:content-meta:general' => [
1018
                        'label' => Craft::t('seomatic', 'General'),
1019
                    ],
1020
                    'seomatic:content-meta:twitter' => [
1021
                        'label' => Craft::t('seomatic', 'Twitter'),
1022
                    ],
1023
                    'seomatic:content-meta:facebook' => [
1024
                        'label' => Craft::t('seomatic', 'Facebook'),
1025
                    ],
1026
                    'seomatic:content-meta:sitemap' => [
1027
                        'label' => Craft::t('seomatic', 'Sitemap'),
1028
                    ],
1029
                ],
1030
            ],
1031
            'seomatic:site-settings' => [
1032
                'label' => Craft::t('seomatic', 'Edit Site Settings'),
1033
                'nested' => [
1034
                    'seomatic:site-settings:identity' => [
1035
                        'label' => Craft::t('seomatic', 'Identity'),
1036
                    ],
1037
                    'seomatic:site-settings:creator' => [
1038
                        'label' => Craft::t('seomatic', 'Creator'),
1039
                    ],
1040
                    'seomatic:site-settings:social' => [
1041
                        'label' => Craft::t('seomatic', 'Social Media'),
1042
                    ],
1043
                    'seomatic:site-settings:sitemap' => [
1044
                        'label' => Craft::t('seomatic', 'Sitemap'),
1045
                    ],
1046
                    'seomatic:site-settings:miscellaneous' => [
1047
                        'label' => Craft::t('seomatic', 'Miscellaneous'),
1048
                    ],
1049
                ],
1050
            ],
1051
            'seomatic:tracking-scripts' => [
1052
                'label' => Craft::t('seomatic', 'Edit Tracking Scripts'),
1053
                'nested' => $scriptsPerms,
1054
            ],
1055
            'seomatic:plugin-settings' => [
1056
                'label' => Craft::t('seomatic', 'Edit Plugin Settings'),
1057
            ],
1058
        ];
1059
    }
1060
1061
    /**
1062
     * Returns the custom Control Panel cache options.
1063
     *
1064
     * @return array
1065
     */
1066
    protected function customAdminCpCacheOptions(): array
1067
    {
1068
        return [
1069
            // Frontend template caches
1070
            [
1071
                'key' => 'seomatic-frontendtemplate-caches',
1072
                'label' => Craft::t('seomatic', 'SEOmatic frontend template caches'),
1073
                'action' => [self::$plugin->frontendTemplates, 'invalidateCaches'],
1074
            ],
1075
            // Meta bundle caches
1076
            [
1077
                'key' => 'seomatic-metabundle-caches',
1078
                'label' => Craft::t('seomatic', 'SEOmatic metadata caches'),
1079
                'action' => [self::$plugin->metaContainers, 'invalidateCaches'],
1080
            ],
1081
            // Sitemap caches
1082
            [
1083
                'key' => 'seomatic-sitemap-caches',
1084
                'label' => Craft::t('seomatic', 'SEOmatic sitemap caches'),
1085
                'action' => [self::$plugin->sitemaps, 'invalidateCaches'],
1086
            ],
1087
            // Schema caches
1088
            [
1089
                'key' => 'seomatic-schema-caches',
1090
                'label' => Craft::t('seomatic', 'SEOmatic schema caches'),
1091
                'action' => [SchemaHelper::class, 'invalidateCaches'],
1092
            ],
1093
        ];
1094
    }
1095
1096
    /**
1097
     * @inheritdoc
1098
     */
1099
    protected function createSettingsModel(): ?Model
1100
    {
1101
        return new Settings();
1102
    }
1103
}
1104