Issues (1783)

src/Retour.php (4 issues)

1
<?php
2
/**
3
 * Retour plugin for Craft CMS
4
 *
5
 * Retour allows you to intelligently redirect legacy URLs, so that you don't
6
 * lose SEO value when rebuilding & restructuring a website
7
 *
8
 * @link      https://nystudio107.com/
9
 * @copyright Copyright (c) 2018 nystudio107
10
 */
11
12
namespace nystudio107\retour;
13
14
use Craft;
15
use craft\base\Element;
16
use craft\base\Model;
17
use craft\base\Plugin;
18
use craft\events\ElementEvent;
19
use craft\events\ExceptionEvent;
20
use craft\events\PluginEvent;
21
use craft\events\RegisterCacheOptionsEvent;
22
use craft\events\RegisterComponentTypesEvent;
23
use craft\events\RegisterGqlQueriesEvent;
24
use craft\events\RegisterGqlSchemaComponentsEvent;
25
use craft\events\RegisterGqlTypesEvent;
26
use craft\events\RegisterUrlRulesEvent;
27
use craft\events\RegisterUserPermissionsEvent;
28
use craft\helpers\ElementHelper;
29
use craft\helpers\UrlHelper;
30
use craft\services\Dashboard;
31
use craft\services\Elements;
32
use craft\services\Fields;
33
use craft\services\Gql;
34
use craft\services\Plugins;
35
use craft\services\UserPermissions;
36
use craft\utilities\ClearCaches;
37
use craft\web\ErrorHandler;
38
use craft\web\twig\variables\CraftVariable;
39
use craft\web\UrlManager;
40
use nystudio107\retour\fields\ShortLink as ShortLinkField;
41
use nystudio107\retour\gql\interfaces\RetourInterface;
42
use nystudio107\retour\gql\queries\RetourQuery;
43
use nystudio107\retour\models\Settings;
44
use nystudio107\retour\services\ServicesTrait;
45
use nystudio107\retour\variables\RetourVariable;
46
use nystudio107\retour\widgets\RetourWidget;
47
use Twig\Error\RuntimeError;
48
use yii\base\Event;
49
use yii\web\HttpException;
50
51
/** @noinspection MissingPropertyAnnotationsInspection */
52
53
/**
54
 * Class Retour
55
 *
56
 * @author    nystudio107
57
 * @package   Retour
58
 * @since     3.0.0
59
 * @method Settings getSettings()
60
 */
61
class Retour extends Plugin
62
{
63
    // Traits
64
    // =========================================================================
65
66
    use ServicesTrait;
67
68
    // Constants
69
    // =========================================================================
70
71
    public const DEVMODE_CACHE_DURATION = 30;
72
73
    // Static Properties
74
    // =========================================================================
75
76
    /**
77
     * @var Retour
78
     */
79
    public static ?Plugin $plugin = null;
80
81
    /**
82
     * @var ?Settings
83
     */
84
    public static ?Settings $settings = null;
85
86
    /**
87
     * @var int
88
     */
89
    public static int $cacheDuration = 0;
90
91
    /**
92
     * @var ?HttpException
93
     */
94
    public static ?HttpException $currentException = null;
95
96
    // Public Properties
97
    // =========================================================================
98
99
    /**
100
     * @var string
101
     */
102
    public string $schemaVersion = '3.0.12';
103
104
    /**
105
     * @var bool
106
     */
107
    public bool $hasCpSection = true;
108
109
    /**
110
     * @var bool
111
     */
112
    public bool $hasCpSettings = true;
113
114
    // Public Methods
115
    // =========================================================================
116
117
    /**
118
     * @inheritdoc
119
     */
120
    public function init(): void
121
    {
122
        parent::init();
123
        self::$plugin = $this;
124
        // Initialize properties
125
        self::$settings = $this->getSettings();
126
        $this->name = self::$settings->pluginName;
127
        self::$cacheDuration = Craft::$app->getConfig()->getGeneral()->devMode
128
            ? $this::DEVMODE_CACHE_DURATION
129
            : 0;
130
        // Handle any console commands
131
        $request = Craft::$app->getRequest();
132
        if ($request->getIsConsoleRequest()) {
133
            $this->controllerNamespace = 'nystudio107\retour\console\controllers';
134
        }
135
        // Install our event listeners
136
        $this->installEventListeners();
137
        // Log that Retour has been loaded
138
        Craft::info(
139
            Craft::t(
140
                'retour',
141
                '{name} plugin loaded',
142
                ['name' => $this->name]
143
            ),
144
            __METHOD__
145
        );
146
    }
147
148
    /**
149
     * Clear all the caches!
150
     */
151
    public function clearAllCaches(): void
152
    {
153
        // Clear all of Retour's caches
154
        self::$plugin->redirects->invalidateCaches();
0 ignored issues
show
The method invalidateCaches() 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

154
        self::$plugin->redirects->/** @scrutinizer ignore-call */ 
155
                                  invalidateCaches();

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...
155
    }
156
157
    /**
158
     * @inheritdoc
159
     */
160
    public function getSettingsResponse(): mixed
161
    {
162
        // Just redirect to the plugin settings page
163
        return Craft::$app->getResponse()->redirect(UrlHelper::cpUrl('retour/settings'));
164
    }
165
166
    /**
167
     * @inheritdoc
168
     */
169
    public function getCpNavItem(): ?array
170
    {
171
        $subNavs = [];
172
        $navItem = parent::getCpNavItem();
173
        $currentUser = Craft::$app->getUser()->getIdentity();
174
        // Only show sub-navs the user has permission to view
175
        if ($currentUser->can('retour:dashboard')) {
0 ignored issues
show
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

175
        if ($currentUser->/** @scrutinizer ignore-call */ can('retour:dashboard')) {
Loading history...
176
            $subNavs['dashboard'] = [
177
                'label' => 'Dashboard',
178
                'url' => 'retour/dashboard',
179
            ];
180
        }
181
        if ($currentUser->can('retour:redirects')) {
182
            $subNavs['redirects'] = [
183
                'label' => 'Redirects',
184
                'url' => 'retour/redirects',
185
            ];
186
        }
187
        if ($currentUser->can('retour:shortlinks')) {
188
            $subNavs['shortlinks'] = [
189
                'label' => 'Short Links',
190
                'url' => 'retour/shortlinks',
191
            ];
192
        }
193
        $editableSettings = true;
194
        $general = Craft::$app->getConfig()->getGeneral();
195
        if (!$general->allowAdminChanges) {
196
            $editableSettings = false;
197
        }
198
        if ($currentUser->can('retour:settings') && $editableSettings) {
199
            $subNavs['settings'] = [
200
                'label' => 'Settings',
201
                'url' => 'retour/settings',
202
            ];
203
        }
204
        // Retour 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
205
        if (empty($subNavs)) {
206
            return null;
207
        }
208
        // A single sub nav item is redundant
209
        if (count($subNavs) === 1) {
210
            $subNavs = [];
211
        }
212
        $navItem = array_merge($navItem, [
213
            'subnav' => $subNavs,
214
        ]);
215
216
        return $navItem;
217
    }
218
219
    // Protected Methods
220
    // =========================================================================
221
222
    /**
223
     * Install our event listeners.
224
     */
225
    protected function installEventListeners(): void
226
    {
227
        // Install our event listeners only if our table schema exists
228
        if ($this->tableSchemaExists()) {
229
            $request = Craft::$app->getRequest();
230
            // Add in our event listeners that are needed for every request
231
            $this->installGlobalEventListeners();
232
            // Install only for non-console site requests
233
            if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest()) {
234
                $this->installSiteEventListeners();
235
            }
236
            // Install only for non-console Control Panel requests
237
            if ($request->getIsCpRequest() && !$request->getIsConsoleRequest()) {
238
                $this->installCpEventListeners();
239
            }
240
        }
241
        // Handler: ClearCaches::EVENT_REGISTER_CACHE_OPTIONS
242
        Event::on(
243
            ClearCaches::class,
244
            ClearCaches::EVENT_REGISTER_CACHE_OPTIONS,
245
            function(RegisterCacheOptionsEvent $event) {
246
                Craft::debug(
247
                    'ClearCaches::EVENT_REGISTER_CACHE_OPTIONS',
248
                    __METHOD__
249
                );
250
                // Register our Control Panel routes
251
                $event->options = array_merge(
252
                    $event->options,
253
                    $this->customAdminCpCacheOptions()
254
                );
255
            }
256
        );
257
        // Handler: EVENT_AFTER_INSTALL_PLUGIN
258
        Event::on(
259
            Plugins::class,
260
            Plugins::EVENT_AFTER_INSTALL_PLUGIN,
261
            function(PluginEvent $event) {
262
                if ($event->plugin === $this) {
263
                    // Invalidate our caches after we've been installed
264
                    $this->clearAllCaches();
265
                    // Send them to our welcome screen
266
                    $request = Craft::$app->getRequest();
267
                    if ($request->isCpRequest) {
268
                        Craft::$app->getResponse()->redirect(UrlHelper::cpUrl(
269
                            'retour/dashboard',
270
                            [
271
                                'showWelcome' => true,
272
                            ]
273
                        ))->send();
274
                    }
275
                }
276
            }
277
        );
278
    }
279
280
    /**
281
     * Determine whether our table schema exists or not; this is needed because
282
     * migrations such as the install migration and base_install migration may
283
     * not have been run by the time our init() method has been called
284
     *
285
     * @return bool
286
     */
287
    protected function tableSchemaExists(): bool
288
    {
289
        return (Craft::$app->db->schema->getTableSchema('{{%retour_redirects}}') !== null);
290
    }
291
292
    /**
293
     * Install global event listeners for all request types
294
     */
295
    protected function installGlobalEventListeners(): void
296
    {
297
        Event::on(
298
            CraftVariable::class,
299
            CraftVariable::EVENT_INIT,
300
            function(Event $event) {
301
                /** @var CraftVariable $variable */
302
                $variable = $event->sender;
303
                $variable->set('retour', [
304
                    'class' => RetourVariable::class,
305
                    'viteService' => $this->vite,
306
                ]);
307
            }
308
        );
309
310
        $prepareRedirectOnElementChange = function(ElementEvent $event) {
311
            /** @var Element $element */
312
            $element = $event->element;
313
            if (!$event->isNew && $element->getUrl() !== null && !$element->propagating) {
314
                $checkElementSlug = true;
315
                // Make sure the element is enabled
316
                if (!$element->enabled || !$element->getEnabledForSite()) {
317
                    $checkElementSlug = false;
318
                }
319
                // If we're running Craft 3.2 or later, also check that isn't not a draft or revision
320
                if (ElementHelper::isDraftOrRevision($element)) {
321
                    $checkElementSlug = false;
322
                }
323
                // Only do this for elements that aren't new, pass $checkElementSlug, and the user
324
                // has turned on the setting
325
                if (self::$settings->createUriChangeRedirects && $checkElementSlug) {
326
                    // Make sure this isn't a transitioning temporary draft/revision and that it's
327
                    // not propagating to other sites
328
                    if ($element->uri && !str_contains($element->uri, '__temp_')) {
329
                        Retour::$plugin->events->stashElementUris($element);
0 ignored issues
show
The method stashElementUris() 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

329
                        Retour::$plugin->events->/** @scrutinizer ignore-call */ 
330
                                                 stashElementUris($element);

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...
330
                    }
331
                }
332
            }
333
        };
334
335
        $insertRedirectOnElementChange = function(ElementEvent $event) {
336
            /** @var Element $element */
337
            $element = $event->element;
338
            if ($element !== null && !$event->isNew && $element->getUrl() !== null) {
339
                $checkElementSlug = true;
340
                // Make sure the element is enabled
341
                if (!$element->enabled || !$element->getEnabledForSite()) {
342
                    $checkElementSlug = false;
343
                }
344
                if (ElementHelper::isDraftOrRevision($element)) {
345
                    $checkElementSlug = false;
346
                }
347
                if (self::$settings->createUriChangeRedirects && $checkElementSlug) {
348
                    Retour::$plugin->events->handleElementUriChange($element);
349
                }
350
            }
351
        };
352
353
        // Handler: Elements::EVENT_BEFORE_SAVE_ELEMENT
354
        Event::on(
355
            Elements::class,
356
            Elements::EVENT_BEFORE_SAVE_ELEMENT,
357
            static function(ElementEvent $event) use ($prepareRedirectOnElementChange) {
358
                Craft::debug(
359
                    'Elements::EVENT_BEFORE_SAVE_ELEMENT',
360
                    __METHOD__
361
                );
362
                $prepareRedirectOnElementChange($event);
363
            }
364
        );
365
        // Handler: Elements::EVENT_AFTER_SAVE_ELEMENT
366
        Event::on(
367
            Elements::class,
368
            Elements::EVENT_AFTER_SAVE_ELEMENT,
369
            static function(ElementEvent $event) use ($insertRedirectOnElementChange) {
370
                Craft::debug(
371
                    'Elements::EVENT_AFTER_SAVE_ELEMENT',
372
                    __METHOD__
373
                );
374
                $insertRedirectOnElementChange($event);
375
            }
376
        );
377
        // Handler: Elements::EVENT_BEFORE_UPDATE_SLUG_AND_URI
378
        Event::on(
379
            Elements::class,
380
            Elements::EVENT_BEFORE_UPDATE_SLUG_AND_URI,
381
            static function(ElementEvent $event) use ($prepareRedirectOnElementChange) {
382
                Craft::debug(
383
                    'Elements::EVENT_BEFORE_UPDATE_SLUG_AND_URI',
384
                    __METHOD__
385
                );
386
                $prepareRedirectOnElementChange($event);
387
            }
388
        );
389
        // Handler: Elements::EVENT_AFTER_UPDATE_SLUG_AND_URI
390
        Event::on(
391
            Elements::class,
392
            Elements::EVENT_AFTER_UPDATE_SLUG_AND_URI,
393
            static function(ElementEvent $event) use ($insertRedirectOnElementChange) {
394
                Craft::debug(
395
                    'Elements::EVENT_AFTER_UPDATE_SLUG_AND_URI',
396
                    __METHOD__
397
                );
398
                $insertRedirectOnElementChange($event);
399
            }
400
        );
401
        // Handler: Plugins::EVENT_AFTER_LOAD_PLUGINS
402
        Event::on(
403
            Plugins::class,
404
            Plugins::EVENT_AFTER_LOAD_PLUGINS,
405
            function() {
406
                // Install these only after all other plugins have loaded
407
                $request = Craft::$app->getRequest();
408
                // Only respond to non-console site requests
409
                if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest()) {
410
                    $this->handleSiteRequest();
411
                }
412
                // Respond to Control Panel requests
413
                if ($request->getIsCpRequest() && !$request->getIsConsoleRequest()) {
414
                    $this->handleAdminCpRequest();
415
                }
416
            }
417
        );
418
        // Handler: Fields::EVENT_REGISTER_FIELD_TYPES
419
        Event::on(
420
            Fields::class,
421
            Fields::EVENT_REGISTER_FIELD_TYPES,
422
            function(RegisterComponentTypesEvent $event) {
423
                $event->types[] = ShortLinkField::class;
424
            }
425
        );
426
        // Handler: Gql::EVENT_REGISTER_GQL_TYPES
427
        Event::on(
428
            Gql::class,
429
            Gql::EVENT_REGISTER_GQL_TYPES,
430
            static function(RegisterGqlTypesEvent $event) {
431
                Craft::debug(
432
                    'Gql::EVENT_REGISTER_GQL_TYPES',
433
                    __METHOD__
434
                );
435
                $event->types[] = RetourInterface::class;
436
            }
437
        );
438
        // Handler: Gql::EVENT_REGISTER_GQL_QUERIES
439
        Event::on(
440
            Gql::class,
441
            Gql::EVENT_REGISTER_GQL_QUERIES,
442
            static function(RegisterGqlQueriesEvent $event) {
443
                Craft::debug(
444
                    'Gql::EVENT_REGISTER_GQL_QUERIES',
445
                    __METHOD__
446
                );
447
                $queries = RetourQuery::getQueries();
448
                foreach ($queries as $key => $value) {
449
                    $event->queries[$key] = $value;
450
                }
451
            }
452
        );
453
        // Handler: Gql::EVENT_REGISTER_SCHEMA_COMPONENTS
454
        Event::on(
455
            Gql::class,
456
            Gql::EVENT_REGISTER_GQL_SCHEMA_COMPONENTS,
457
            static function(RegisterGqlSchemaComponentsEvent $event) {
458
                Craft::debug(
459
                    'Gql::EVENT_REGISTER_GQL_SCHEMA_COMPONENTS',
460
                    __METHOD__
461
                );
462
                $label = Craft::t('retour', 'Retour');
463
                $event->queries[$label]['retour.all:read'] = ['label' => Craft::t('retour', 'Query Retour data')];
464
            }
465
        );
466
    }
467
468
    /**
469
     * Handle site requests.  We do it only after we receive the event
470
     * EVENT_AFTER_LOAD_PLUGINS so that any pending db migrations can be run
471
     * before our event listeners kick in
472
     */
473
    protected function handleSiteRequest(): void
474
    {
475
        // Handler: ErrorHandler::EVENT_BEFORE_HANDLE_EXCEPTION
476
        Event::on(
477
            ErrorHandler::class,
478
            ErrorHandler::EVENT_BEFORE_HANDLE_EXCEPTION,
479
            static function(ExceptionEvent $event) {
480
                Craft::debug(
481
                    'ErrorHandler::EVENT_BEFORE_HANDLE_EXCEPTION',
482
                    __METHOD__
483
                );
484
                $exception = $event->exception;
485
                // If this is a Twig Runtime exception, use the previous one instead
486
                if ($exception instanceof RuntimeError &&
487
                    ($previousException = $exception->getPrevious()) !== null) {
488
                    $exception = $previousException;
489
                }
490
                // If this is a 404 error, see if we can handle it
491
                if ($exception instanceof HttpException && $exception->statusCode === 404) {
492
                    self::$currentException = $exception;
493
                    Retour::$plugin->redirects->handle404();
0 ignored issues
show
The method handle404() 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

493
                    Retour::$plugin->redirects->/** @scrutinizer ignore-call */ 
494
                                                handle404();

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...
494
                }
495
            }
496
        );
497
    }
498
499
    /**
500
     * Handle Control Panel requests. We do it only after we receive the event
501
     * EVENT_AFTER_LOAD_PLUGINS so that any pending db migrations can be run
502
     * before our event listeners kick in
503
     */
504
    protected function handleAdminCpRequest(): void
505
    {
506
    }
507
508
    /**
509
     * Install site event listeners for site requests only
510
     */
511
    protected function installSiteEventListeners(): void
512
    {
513
        // Handler: UrlManager::EVENT_REGISTER_SITE_URL_RULES
514
        Event::on(
515
            UrlManager::class,
516
            UrlManager::EVENT_REGISTER_SITE_URL_RULES,
517
            function(RegisterUrlRulesEvent $event) {
518
                Craft::debug(
519
                    'UrlManager::EVENT_REGISTER_SITE_URL_RULES',
520
                    __METHOD__
521
                );
522
                // Register our Control Panel routes
523
                $event->rules = array_merge(
524
                    $event->rules,
525
                    $this->customFrontendRoutes()
526
                );
527
            }
528
        );
529
    }
530
531
    /**
532
     * Return the custom frontend routes
533
     *
534
     * @return array
535
     */
536
    protected function customFrontendRoutes(): array
537
    {
538
        return [
539
        ];
540
    }
541
542
    /**
543
     * Install site event listeners for Control Panel requests only
544
     */
545
    protected function installCpEventListeners(): void
546
    {
547
        // Handler: Dashboard::EVENT_REGISTER_WIDGET_TYPES
548
        Event::on(
549
            Dashboard::class,
550
            Dashboard::EVENT_REGISTER_WIDGET_TYPES,
551
            function(RegisterComponentTypesEvent $event) {
552
                $currentUser = Craft::$app->getUser()->getIdentity();
553
                if ($currentUser->can('accessPlugin-retour')) {
554
                    $event->types[] = RetourWidget::class;
555
                }
556
            }
557
        );
558
        // Handler: UrlManager::EVENT_REGISTER_CP_URL_RULES
559
        Event::on(
560
            UrlManager::class,
561
            UrlManager::EVENT_REGISTER_CP_URL_RULES,
562
            function(RegisterUrlRulesEvent $event) {
563
                Craft::debug(
564
                    'UrlManager::EVENT_REGISTER_CP_URL_RULES',
565
                    __METHOD__
566
                );
567
                // Register our Control Panel routes
568
                $event->rules = array_merge(
569
                    $event->rules,
570
                    $this->customAdminCpRoutes()
571
                );
572
            }
573
        );
574
        // Handler: UserPermissions::EVENT_REGISTER_PERMISSIONS
575
        Event::on(
576
            UserPermissions::class,
577
            UserPermissions::EVENT_REGISTER_PERMISSIONS,
578
            function(RegisterUserPermissionsEvent $event) {
579
                Craft::debug(
580
                    'UserPermissions::EVENT_REGISTER_PERMISSIONS',
581
                    __METHOD__
582
                );
583
                // Register our custom permissions
584
                $event->permissions[] = [
585
                    'heading' => Craft::t('retour', 'Retour'),
586
                    'permissions' => $this->customAdminCpPermissions(),
587
                ];
588
            }
589
        );
590
    }
591
592
    /**
593
     * Return the custom Control Panel routes
594
     *
595
     * @return array
596
     */
597
    protected function customAdminCpRoutes(): array
598
    {
599
        return [
600
            'retour' => '',
601
602
            'retour/redirects' => 'retour/redirects/redirects',
603
            'retour/redirects/<siteHandle:{handle}>' => 'retour/redirects/redirects',
604
605
            'retour/edit-redirect/<redirectId:\d+>' => 'retour/redirects/edit-redirect',
606
607
            'retour/add-redirect' => 'retour/redirects/edit-redirect',
608
            'retour/add-redirect/<siteId:\d+>' => 'retour/redirects/edit-redirect',
609
610
            'retour/dashboard' => 'retour/statistics/dashboard',
611
            'retour/dashboard/<siteHandle:{handle}>' => 'retour/statistics/dashboard',
612
613
            'retour/shortlinks' => 'retour/redirects/shortlinks',
614
            'retour/shortlinks/<siteHandle:{handle}>' => 'retour/redirects/shortlinks',
615
616
            'retour/settings' => 'retour/settings/plugin-settings',
617
        ];
618
    }
619
620
    /**
621
     * Returns the custom Control Panel user permissions.
622
     *
623
     * @return array
624
     */
625
    protected function customAdminCpPermissions(): array
626
    {
627
        return [
628
            'retour:dashboard' => [
629
                'label' => Craft::t('retour', 'Dashboard'),
630
            ],
631
            'retour:redirects' => [
632
                'label' => Craft::t('retour', 'Redirects'),
633
            ],
634
            'retour:shortlinks' => [
635
                'label' => Craft::t('retour', 'Short Links'),
636
            ],
637
            'retour:settings' => [
638
                'label' => Craft::t('retour', 'Settings'),
639
            ],
640
        ];
641
    }
642
643
    /**
644
     * Returns the custom Control Panel cache options.
645
     *
646
     * @return array
647
     */
648
    protected function customAdminCpCacheOptions(): array
649
    {
650
        return [
651
            [
652
                'key' => 'retour-redirect-caches',
653
                'label' => Craft::t('retour', 'Retour redirect caches'),
654
                'action' => [self::$plugin->redirects, 'invalidateCaches'],
655
            ],
656
        ];
657
    }
658
659
    /**
660
     * @inheritdoc
661
     */
662
    protected function createSettingsModel(): ?Model
663
    {
664
        return new Settings();
665
    }
666
}
667