Seomatic::getSettings()   A
last analyzed

Complexity

Conditions 6
Paths 3

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

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

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