Passed
Push — master ( edfd34...7a93b3 )
by Nicolaas
04:53
created

SearchApi::getAllValidFields()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

378
    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...
379
    {
380
        $startInner = 0;
381
        $startOuter = 0;
382
        if ($this->debug) {
383
            $startOuter = microtime(true);
384
        }
385
        $this->workOutInclusionsAndExclusions();
386
387
        // important to do this first
388
        if ($word) {
389
            $this->setWordsAsString($word);
390
        }
391
        $this->workOutWordsForSearching();
392
        if ($this->debug) {
393
            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

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