Passed
Push — v4 ( 81628e...7020b0 )
by Andrew
43:05 queued 19:01
created

Seomatic::getCpNavItem()   F

Complexity

Conditions 13
Paths 1152

Size

Total Lines 68
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 182

Importance

Changes 0
Metric Value
eloc 45
c 0
b 0
f 0
dl 0
loc 68
ccs 0
cts 52
cp 0
rs 2.45
cc 13
nc 1152
nop 0
crap 182

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

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

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

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