Passed
Push — master ( 9a16b6...526e0e )
by Nicolaas
04:08
created

SearchApi::setQuickSearchType()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 21
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 17
nc 4
nop 1
dl 0
loc 21
rs 9.7
c 0
b 0
f 0
1
<?php
2
3
namespace Sunnysideup\SiteWideSearch\Api;
4
5
use SilverStripe\Core\ClassInfo;
6
use SilverStripe\Core\Config\Configurable;
7
use SilverStripe\Core\Convert;
8
use SilverStripe\Core\Extensible;
9
use SilverStripe\Core\Injector\Injectable;
10
use SilverStripe\Core\Injector\Injector;
11
use SilverStripe\ORM\ArrayList;
12
use SilverStripe\ORM\DataObject;
13
use SilverStripe\ORM\DB;
14
use SilverStripe\ORM\FieldType\DBDatetime;
15
use SilverStripe\ORM\FieldType\DBField;
16
use SilverStripe\Security\LoginAttempt;
17
use SilverStripe\Security\MemberPassword;
18
use SilverStripe\Security\RememberLoginHash;
19
use SilverStripe\SessionManager\Models\LoginSession;
20
use SilverStripe\Versioned\ChangeSet;
21
use SilverStripe\Versioned\ChangeSetItem;
22
use SilverStripe\Versioned\Versioned;
23
use SilverStripe\View\ArrayData;
24
use Sunnysideup\SiteWideSearch\Helpers\FindClassesAndFields;
25
use Sunnysideup\SiteWideSearch\Helpers\FindEditableObjects;
26
27
class SearchApi
28
{
29
    use Extensible;
30
    use Configurable;
31
    use Injectable;
32
33
    protected $debug = false;
34
35
    protected $showReplacements = false;
36
37
    protected $isQuickSearch = false;
38
39
    protected $searchWholePhrase = false;
40
41
    protected $bypassCanMethods = false;
42
43
    protected $baseClass = DataObject::class;
44
45
    protected $quickSearchType = 'limited';
46
47
    protected $excludedClasses = [];
48
49
    protected $excludedClassesWithSubClassess = [];
50
51
    protected $includedClasses = [];
52
53
    protected $includedClassesWithSubClassess = [];
54
55
    protected $excludedFields = [];
56
57
    protected $includedFields = [];
58
59
    protected $includedClassFieldCombos = [];
60
61
    protected $defaultLists = [];
62
63
    protected $sortOverride;
64
65
    protected $words = [];
66
67
    protected $replace = '';
68
69
    private $objects = [];
70
71
    private static $limit_of_count_per_data_object = 999;
72
73
    private static $hours_back_for_recent = 48;
74
75
    private static $limit_per_class_for_recent = 5;
76
77
    private static $default_exclude_classes = [
78
        MemberPassword::class,
79
        LoginAttempt::class,
80
        ChangeSet::class,
81
        ChangeSetItem::class,
82
        RememberLoginHash::class,
83
        LoginSession::class,
84
        'SilverStripe\\UserForms\\Model\\Submission\\SubmittedFormField'
85
    ];
86
87
    private static $default_exclude_fields = [
88
        'ClassName',
89
        'LastEdited',
90
        'Created',
91
        'ID',
92
        'CanViewType',
93
        'CanEditType',
94
    ];
95
96
    private static $default_include_classes = [];
97
98
    private static $default_include_fields = [];
99
100
    private static $default_include_class_field_combos = [];
101
102
    private static $default_lists = [];
103
104
    public function setDebug(bool $b): SearchApi
105
    {
106
        $this->debug = $b;
107
108
        return $this;
109
    }
110
111
    public function setShowReplacements(bool $b): SearchApi
112
    {
113
        $this->showReplacements = $b;
114
115
        return $this;
116
    }
117
118
    protected function getCache()
119
    {
120
        return FindClassesAndFields::inst($this->baseClass);
121
    }
122
123
    public function setQuickSearchType(string $nameOrType): SearchApi
124
    {
125
        if ($nameOrType === 'all') {
126
            $this->isQuickSearch = false;
127
            $this->quickSearchType = '';
128
        } elseif ($nameOrType === 'limited') {
129
            $this->isQuickSearch = true;
130
            $this->quickSearchType = '';
131
        } elseif (class_exists($nameOrType)) {
132
            $this->quickSearchType = $nameOrType;
133
            $object = Injector::inst()->get($nameOrType);
134
            $this->setIncludedClasses($object->getClassesToSearch());
135
            $this->setIncludedFields($object->getFieldsToSearch());
136
            $this->setIncludedClassFieldCombos($object->getIncludedClassFieldCombos());
137
            $this->setDefaultLists($object->getDefaultLists());
138
            $this->setSortOverride($object->getSortOverride());
139
        } else {
140
            user_error('QuickSearchType must be either "all" or "limited" or a defined quick search class. Provided was: ' . $nameOrType);
141
        }
142
143
        return $this;
144
    }
145
146
    public function setIsQuickSearch(bool $b): SearchApi
147
    {
148
        $this->isQuickSearch = $b;
149
150
        return $this;
151
    }
152
153
    public function setSearchWholePhrase(bool $b): SearchApi
154
    {
155
        $this->searchWholePhrase = $b;
156
157
        return $this;
158
    }
159
160
    public function setBypassCanMethods(bool $b): SearchApi
161
    {
162
        $this->bypassCanMethods = $b;
163
164
        return $this;
165
    }
166
167
    public function setBaseClass(string $class): SearchApi
168
    {
169
        if (class_exists($class)) {
170
            $this->baseClass = $class;
171
        }
172
173
        return $this;
174
    }
175
176
    public function setExcludedClasses(array $a): SearchApi
177
    {
178
        $this->excludedClasses = $a;
179
180
        return $this;
181
    }
182
183
    public function setIncludedClasses(array $a): SearchApi
184
    {
185
        $this->includedClasses = $a;
186
        return $this;
187
    }
188
189
    public function setExcludedFields(array $a): SearchApi
190
    {
191
        $this->excludedFields = $a;
192
193
        return $this;
194
    }
195
196
    public function setIncludedFields(array $a): SearchApi
197
    {
198
        $this->includedFields = $a;
199
200
        return $this;
201
    }
202
203
    public function setIncludedClassFieldCombos(array $a): SearchApi
204
    {
205
        $this->includedClassFieldCombos = $a;
206
207
        return $this;
208
    }
209
210
    public function setDefaultLists(array $a): SearchApi
211
    {
212
        $this->defaultLists = $a;
213
214
        return $this;
215
    }
216
217
    public function setSortOverride(?array $a = null): SearchApi
218
    {
219
        $this->sortOverride = $a;
220
221
        return $this;
222
    }
223
224
    public function setWordsAsString(string $s): SearchApi
225
    {
226
        $s = $this->securityCheckInput($s);
227
        $this->words = explode(' ', $s);
228
229
        return $this;
230
    }
231
232
    // public function __construct()
233
    // {
234
    //     Environment::increaseTimeLimitTo(300);
235
    //     Environment::setMemoryLimitMax(-1);
236
    //     Environment::increaseMemoryLimitTo(-1);
237
    // }
238
239
    protected string $cacheHasBeenBuilt = '';
240
241
    public function buildCache(?string $word = ''): SearchApi
242
    {
243
        if ($this->cacheHasBeenBuilt !== $word) {
244
            $this->getLinksInner($word);
245
            $this->cacheHasBeenBuilt = $word;
246
        }
247
        return $this;
248
    }
249
250
    public function getLinks(?string $word = '', ?string $type = ''): ArrayList
251
    {
252
        return $this->getLinksInner($word, $type);
253
    }
254
255
    protected function getLinksInner(?string $word = '', ?string $type = ''): ArrayList
256
    {
257
        $this->initCache();
258
259
        //always do first ...
260
        $matches = $this->getMatches($word, $type);
261
262
        $list = $this->turnMatchesIntoList($matches);
263
264
        $this->saveCache();
265
        return $list;
266
    }
267
268
269
270
    public function doReplacementURL(string $word, string $replace): int
271
    {
272
        return $this->doReplacement($word, $replace, 'url');
273
    }
274
275
    public function doReplacement(string $word, string $replace, ?string $type = ''): int
276
    {
277
        $count = 0;
278
        if ($word !== '' && $word !== '0') {
279
            $this->buildCache($word);
280
            $replace = $this->securityCheckInput($replace);
281
            if (strpos('://', $word) !== false) {
282
                $type = 'url';
283
            }
284
            foreach ($this->objects as $item) {
285
                if ($item->canEdit() || $this->bypassCanMethods) {
286
                    $className = $item->ClassName;
287
                    $fields = $this->getAllValidFields($className);
288
                    foreach ($fields as $field) {
289
                        if (! $this->includeFieldTest($className, $field)) {
290
                            continue;
291
                        }
292
                        if ($type === 'url') {
293
                            $escapedFrom = preg_quote($word, '/');
294
                            // It replaces exact matches of $escapedFrom (with optional trailing slash) in $item->{$field} only if followed by space, quote, ?, #, or end of string, preserving the slash if present.
295
                            $new = preg_replace_callback(
296
                                '/\b' . $escapedFrom . '(\/?)(?=[\s"\']|\?|#|$)/',
297
                                fn($matches) => $replace . ($matches[1] ?? ''),
298
                                $item->{$field}
299
                            );
300
                        } else {
301
                            $new = str_replace($word, $replace, $item->{$field});
302
                        }
303
                        if ($new === $item->{$field}) {
304
                            continue;
305
                        }
306
                        ++$count;
307
                        $item->{$field} = $new;
308
                        $this->writeAndPublishIfAppropriate($item);
309
                        if ($this->showReplacements) {
310
                            DB::alteration_message('.... .... ' . $item->ClassName . '.' . $item->ID . ' replace ' . $word . ' with ' . $replace . ' (' . $type . ') in field ' . $field, 'changed');
311
                        }
312
                    }
313
                } else {
314
                    if ($this->showReplacements) {
315
                        DB::alteration_message('.... .... ' . $item->ClassName . '.' . $item->ID . ' cannot be edited, so no replacement done', 'deleted');
316
                    }
317
                }
318
            }
319
        }
320
321
        return $count;
322
    }
323
324
    protected function saveCache(): self
325
    {
326
        $this->getCache()->saveCache();
327
328
        return $this;
329
    }
330
331
    protected function initCache(): self
332
    {
333
        $this->getCache()->initCache();
334
335
        return $this;
336
    }
337
338
    protected function writeAndPublishIfAppropriate($item)
339
    {
340
        if ($item->hasExtension(Versioned::class)) {
341
            $myStage = Versioned::get_stage();
342
            Versioned::set_stage(Versioned::DRAFT);
343
            // is it on live and is live the same as draft
344
            $canBePublished = $item->isPublished() && ! $item->isModifiedOnDraft();
345
            $item->writeToStage(Versioned::DRAFT);
346
            if ($canBePublished) {
347
                $item->publishSingle();
348
            }
349
            Versioned::set_stage($myStage);
350
        } else {
351
            $item->write();
352
        }
353
    }
354
355
    protected function getMatches(?string $word = '', ?string $type = ''): array
0 ignored issues
show
Unused Code introduced by
The parameter $type is not used and could be removed. ( Ignorable by Annotation )

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

355
    protected function getMatches(?string $word = '', /** @scrutinizer ignore-unused */ ?string $type = ''): array

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
356
    {
357
        $startInner = 0;
358
        $startOuter = 0;
359
        if ($this->debug) {
360
            $startOuter = microtime(true);
361
        }
362
        $this->workOutInclusionsAndExclusions();
363
364
        // important to do this first
365
        if ($word) {
366
            $this->setWordsAsString($word);
367
        }
368
        $this->workOutWordsForSearching();
369
        if ($this->debug) {
370
            DB::alteration_message('Words searched for ' . print_r($this->words, 1));
0 ignored issues
show
Bug introduced by
Are you sure print_r($this->words, 1) of type string|true can be used in concatenation? ( Ignorable by Annotation )

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

370
            DB::alteration_message('Words searched for ' . /** @scrutinizer ignore-type */ print_r($this->words, 1));
Loading history...
371
        }
372
373
        $array = [];
374
375
        if (count($this->words) > 0) {
376
            foreach ($this->getAllDataObjects() as $className) {
377
                if (! $this->includeClassTest($className)) {
378
                    continue;
379
                }
380
381
                $array[$className] = [];
382
                $fields = $this->getAllValidFields($className);
383
                $filterAny = [];
384
                foreach ($fields as $field) {
385
                    if (! $this->includeFieldTest($className, $field)) {
386
                        continue;
387
                    }
388
                    $filterAny[$field . ':PartialMatch'] = $this->words;
389
                    if ($this->debug) {
390
                        DB::alteration_message(' ... ... Searching in ' . $className . '.' . $field);
391
                    }
392
                }
393
                if ([] !== $filterAny) {
394
                    if ($this->debug) {
395
                        $startInner = microtime(true);
396
                        DB::alteration_message(' ... Filter: ' . implode(', ', array_keys($filterAny)));
397
                    }
398
                    $defaultList = $this->getDefaultList($className);
399
                    if ($defaultList === []) {
400
                        $array[$className] = $className::get();
401
                    }
402
                    $array[$className] = $array[$className]->filter(['ClassName' => $className]);
403
                    $array[$className] = $array[$className]
404
                        ->filterAny($filterAny)
405
                        ->limit($this->Config()->get('limit_of_count_per_data_object'))
406
                        ->columnUnique('ID');
407
                    if ($this->debug) {
408
                        $elaps = microtime(true) - $startInner;
409
                        DB::alteration_message('search for ' . $className . ' taken : ' . $elaps);
410
                    }
411
                }
412
413
                if ($this->debug) {
414
                    DB::alteration_message(' ... No fields in ' . $className);
415
                }
416
            }
417
        } else {
418
            $array = $this->getDefaultResults();
419
        }
420
421
        if ($this->debug) {
422
            $elaps = microtime(true) - $startOuter;
423
            DB::alteration_message('seconds taken find results: ' . $elaps);
424
        }
425
426
        return $array;
427
    }
428
429
    protected function getDefaultResults(): array
430
    {
431
        $back = $this->config()->get('hours_back_for_recent') ?: 24;
432
        $limit = $this->Config()->get('limit_per_class_for_recent') ?: 5;
433
        $threshold = strtotime('-' . $back . ' hours', DBDatetime::now()->getTimestamp());
434
        if (! $threshold) {
435
            $threshold = time() - 86400;
436
        }
437
438
        $array = [];
439
        $classNames = $this->getAllDataObjects();
440
        foreach ($classNames as $className) {
441
            if ($this->includeClassTest($className)) {
442
                $array[$className] = $className::get()
443
                    ->filter('LastEdited:GreaterThan', date('Y-m-d H:i:s', $threshold))
444
                    ->sort(['LastEdited' => 'DESC'])
445
                    ->limit($limit)
446
                    ->column('ID');
447
            }
448
        }
449
450
        return $array;
451
    }
452
453
    /**
454
     * weeds out doubles
455
     */
456
    protected function turnArrayIntoObjects(array $matches, ?int $limit = 0): array
457
    {
458
        $start = 0;
459
        $fullListCheck = [];
460
461
        if (empty($this->objects)) {
462
            if ($limit === null || $limit === 0) {
463
                $limit = (int) $this->Config()->get('limit_of_count_per_data_object');
464
            }
465
466
            $this->objects = [];
467
            if ($this->debug) {
468
                DB::alteration_message('number of classes: ' . count($matches));
469
            }
470
471
            foreach ($matches as $className => $ids) {
472
                if ($this->debug) {
473
                    $start = microtime(true);
474
                    DB::alteration_message(' ... number of matches for : ' . $className . ': ' . count($ids));
475
                }
476
477
                if (count($ids) > 0) {
478
                    $className = (string) $className;
479
                    $items = $className::get()
480
                        ->filter(['ID' => $ids, 'ClassName' => $className])
481
                        ->limit($limit);
482
                    foreach ($items as $item) {
483
                        if (isset($fullListCheck[$item->ClassName][$item->ID])) {
484
                            continue;
485
                        }
486
                        if ($item->canView() || $this->bypassCanMethods) {
487
                            $fullListCheck[$item->ClassName][$item->ID] = true;
488
                            $this->objects[] = $item;
489
                        } else {
490
                            $fullListCheck[$item->ClassName][$item->ID] = false;
491
                        }
492
                    }
493
                }
494
495
                if ($this->debug) {
496
                    $elaps = microtime(true) - $start;
497
                    DB::alteration_message('seconds taken to find objects in: ' . $className . ': ' . $elaps);
498
                }
499
            }
500
        }
501
502
        return $this->objects;
503
    }
504
505
    protected function turnMatchesIntoList(array $matches): ArrayList
506
    {
507
        // helper
508
        //return values
509
        $list = ArrayList::create();
510
        $finder = Injector::inst()->get(FindEditableObjects::class);
511
        $finder->initCache(md5(serialize($this->excludedClassesWithSubClassess)))
512
            ->setExcludedClasses($this->excludedClassesWithSubClassess);
513
514
        $items = $this->turnArrayIntoObjects($matches);
515
        foreach ($items as $item) {
516
            if ($item->canView() || $this->bypassCanMethods) {
517
                $link = $finder->getLink($item);
518
                $cmsEditLink = trim($item->canEdit() ? $finder->getCMSEditLink($item) : '');
519
                $list->push(
520
                    ArrayData::create(
521
                        [
522
                            'HasLink' => (bool) $link,
523
                            'HasCMSEditLink' => (bool) $cmsEditLink,
524
                            'Link' => $link,
525
                            'CMSEditLink' => $cmsEditLink,
526
                            'ID' => $item->ID,
527
                            'LastEdited' => $item->LastEdited,
528
                            'Title' => $item->getTitle(),
529
                            'ClassName' => $item->ClassName,
530
                            'SingularName' => $item->i18n_singular_name(),
531
                            'SiteWideSearchSortValue' => $this->getSortValue($item),
532
                            'CMSThumbnail' => DBField::create_field('HTMLText', $finder->getCMSThumbnail($item)),
533
                        ]
534
                    )
535
                );
536
            }
537
        }
538
        $finder->saveCache();
539
540
        if (! empty($this->sortOverride)) {
541
            return $list->sort($this->sortOverride);
542
        } else {
543
            return $list->sort(['SiteWideSearchSortValue' => 'ASC']);
544
        }
545
    }
546
547
    protected function getSortValue($item)
548
    {
549
        $className = $item->ClassName;
550
        $fields = $this->getAllValidFields($className);
551
        $fullWords = implode(' ', $this->words);
552
553
        $done = false;
554
        $score = 0;
555
        if ($fullWords !== '' && $fullWords !== '0') {
556
            $fieldValues = [];
557
            $fieldValuesAll = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $fieldValuesAll is dead and can be removed.
Loading history...
558
            foreach ($fields as $field) {
559
                $fieldValues[$field] = strtolower(strip_tags((string) $item->{$field}));
560
            }
561
562
            $fieldValuesAll = implode(' ', $fieldValues);
563
            $testWords = array_merge(
564
                [$fullWords],
565
                $this->words
566
            );
567
            $testWords = array_unique($testWords);
568
            foreach ($testWords as $wordKey => $word) {
569
                //match a exact field to full words / one word
570
                $fullWords = ! (bool) $wordKey;
571
                if (false === $done) {
572
                    $count = 0;
573
                    foreach ($fieldValues as $fieldValue) {
574
                        ++$count;
575
                        if ($fieldValue === $word) {
576
                            $score += (int) $wordKey + $count;
577
                            $done = true;
578
579
                            break;
580
                        }
581
                    }
582
                }
583
584
                // the full string / any of the words are present?
585
                if (false === $done) {
586
                    $pos = strpos($fieldValuesAll, $word);
587
                    if (false !== $pos) {
588
                        $score += (($pos + 1) / strlen($word)) * 1000;
589
                        $done = true;
590
                    }
591
                }
592
593
                // all individual words are present
594
                if (false === $done && $fullWords) {
595
                    $score += 1000;
596
                    $allMatch = true;
597
                    foreach ($this->words as $tmpWord) {
598
                        $pos = strpos($fieldValuesAll, $tmpWord);
599
                        if (false === $pos) {
600
                            $allMatch = false;
601
602
                            break;
603
                        }
604
                    }
605
                    if ($allMatch) {
606
                        $done = true;
607
                    }
608
                }
609
            }
610
        }
611
612
        //the older the item, the higher the scoare
613
        //1104622247 = 1 jan 2005
614
        return $score + (1 / (strtotime($item->LastEdited) - 1104537600));
615
    }
616
617
    protected function workOutInclusionsAndExclusions()
618
    {
619
        $this->excludedClasses = array_unique(
620
            array_merge(
621
                $this->Config()->get('default_exclude_classes'),
622
                $this->excludedClasses
623
            )
624
        );
625
        $this->excludedClassesWithSubClassess = $this->includeSubClasses($this->excludedClasses);
626
        $this->includedClasses = array_unique(
627
            array_merge(
628
                $this->Config()->get('default_include_classes'),
629
                $this->includedClasses
630
            )
631
        );
632
        $this->includedClassesWithSubClassess = $this->includeSubClasses($this->includedClasses);
633
        $this->excludedFields = array_unique(
634
            array_merge(
635
                $this->Config()->get('default_exclude_fields'),
636
                $this->excludedFields
637
            )
638
        );
639
640
        $this->includedFields = array_unique(
641
            array_merge(
642
                $this->Config()->get('default_include_fields'),
643
                $this->includedFields
644
            )
645
        );
646
        $this->includedClassFieldCombos = array_unique(
647
            array_merge(
648
                $this->Config()->get('default_include_class_field_combos'),
649
                $this->includedClassFieldCombos
650
            )
651
        );
652
        $this->defaultLists = array_unique(
653
            array_merge(
654
                $this->Config()->get('default_lists'),
655
                $this->defaultLists
656
            )
657
        );
658
    }
659
660
    protected function workOutWordsForSearching()
661
    {
662
        if ($this->searchWholePhrase) {
663
            $this->words = [implode(' ', $this->words)];
664
        }
665
666
        if (count($this->words) === 0) {
667
            user_error('No word has been provided');
668
        }
669
670
        $this->words = array_map('trim', $this->words);
671
        $this->words = array_map('strtolower', $this->words);
672
        $this->words = array_unique($this->words);
673
        $this->words = array_filter($this->words);
674
    }
675
676
    protected function getAllDataObjects(): array
677
    {
678
        return $this->getCache()->getAllDataObjects();
679
    }
680
681
    protected function getAllValidFields(string $className): array
682
    {
683
        return $this->getCache()->getAllValidFields($className, $this->isQuickSearch, $this->includedFields, $this->includedClassFieldCombos);
684
    }
685
686
    protected function includeClassTest(string $className): bool
687
    {
688
        if (count($this->includedClassesWithSubClassess) && ! in_array($className, $this->includedClassesWithSubClassess, true)) {
689
            if ($this->debug) {
690
                DB::alteration_message(' ... Skipping as not included ' . $className);
691
            }
692
            return false;
693
        }
694
        if (count($this->excludedClassesWithSubClassess) && in_array($className, $this->excludedClassesWithSubClassess, true)) {
695
            if ($this->debug) {
696
                DB::alteration_message(' ... Skipping as excluded ' . $className);
697
            }
698
            return false;
699
        }
700
        if ($this->debug) {
701
            DB::alteration_message(' ... including ' . $className);
702
        }
703
704
        return true;
705
    }
706
707
    protected function includeFieldTest(string $className, string $field): bool
708
    {
709
        if (isset($this->includedClassFieldCombos[$className][$field])) {
710
            return true;
711
        } elseif (count($this->includedFields) > 0) {
712
            return in_array($field, $this->includedFields, true);
713
        } elseif (count($this->excludedFields) > 0) {
714
            return ! in_array($field, $this->includedFields, true);
715
        } else {
716
            return false;
717
        }
718
    }
719
720
    protected function includeSubClasses(array $classes): array
721
    {
722
        $toAdd = [];
723
        foreach ($classes as $class) {
724
            $toAdd = array_merge($toAdd, ClassInfo::subclassesFor($class, false));
725
        }
726
        return array_unique(array_merge($classes, $toAdd));
727
    }
728
729
    protected function securityCheckInput(string $word): string
730
    {
731
        $word = trim($word);
732
        return Convert::raw2sql($word);
733
    }
734
735
    protected function getDefaultList(string $className): array
736
    {
737
        return $this->defaultLists[$className] ?? [];
738
    }
739
}
740