Issues (35)

src/Control/View.php (8 issues)

1
<?php
2
3
namespace Sunnysideup\AssetsOverview\Control;
4
5
use SilverStripe\CMS\Controllers\ContentController;
6
use SilverStripe\Control\Director;
7
use SilverStripe\Control\HTTPRequest;
8
use SilverStripe\Control\HTTPResponse;
9
use SilverStripe\Control\Middleware\HTTPCacheControlMiddleware;
10
use SilverStripe\Core\Environment;
11
use SilverStripe\Core\Flushable;
12
use SilverStripe\Core\Injector\Injector;
13
use SilverStripe\Forms\CheckboxField;
14
use SilverStripe\Forms\CheckboxSetField;
15
use SilverStripe\Forms\DropdownField;
16
use SilverStripe\Forms\FieldList;
17
use SilverStripe\Forms\Form;
18
use SilverStripe\Forms\FormAction;
19
use SilverStripe\Forms\HiddenField;
20
use SilverStripe\Forms\OptionsetField;
21
use SilverStripe\Forms\TextField;
22
use SilverStripe\ORM\ArrayList;
23
use SilverStripe\ORM\FieldType\DBField;
24
use SilverStripe\Security\Permission;
25
use SilverStripe\Security\Security;
26
use SilverStripe\Versioned\Versioned;
27
use SilverStripe\View\ArrayData;
28
use SilverStripe\View\Requirements;
29
use SilverStripe\View\SSViewer;
30
use Sunnysideup\AssetsOverview\Api\AddAndRemoveFromDb;
31
use Sunnysideup\AssetsOverview\Files\AllFilesInfo;
32
use Sunnysideup\AssetsOverview\Files\OneFileInfo;
33
use Sunnysideup\AssetsOverview\Traits\FilesystemRelatedTraits;
34
35
/**
36
 * Class \Sunnysideup\AssetsOverview\Control\View
37
 *
38
 * @property \Sunnysideup\AssetsOverview\Control\View $dataRecord
39
 * @method \Sunnysideup\AssetsOverview\Control\View data()
40
 * @mixin \Sunnysideup\AssetsOverview\Control\View
41
 */
42
class View extends ContentController implements Flushable
43
{
44
    use FilesystemRelatedTraits;
45
46
    public static function get_sorters()
47
    {
48
        return self::SORTERS;
49
    }
50
51
    public static function get_filters()
52
    {
53
        return self::FILTERS;
54
    }
55
56
    public static function get_displayers()
57
    {
58
        return self::DISPLAYERS;
59
    }
60
61
62
    protected static $allFilesProvider = null;
63
64
    /**
65
     * @var string
66
     */
67
    protected string $title = '';
68
69
70
    /**
71
     * @var int
72
     */
73
    protected int $limit = 1000;
74
75
    /**
76
     * @var int
77
     */
78
    protected int $startLimit = 0;
79
80
    /**
81
     * @var int
82
     */
83
    protected int $endLimit = 0;
84
85
    /**
86
     * @var int
87
     */
88
    protected int $pageNumber = 1;
89
90
    /**
91
     * @var bool
92
     */
93
    protected bool $dryRun = false;
94
95
    /**
96
     * @var string
97
     */
98
    protected string $sorter = 'byfolder';
99
100
    /**
101
     * @var string
102
     */
103
    protected string $filter = '';
104
105
    /**
106
     * @var string
107
     */
108
    protected string $displayer = 'thumbs';
109
110
    /**
111
     * @var array
112
     */
113
    protected array $allowedExtensions = [];
114
115
    /**
116
     * Defines methods that can be called directly.
117
     *
118
     * @var array
119
     */
120
    private static $allowed_actions = [
121
        'index' => 'ADMIN',
122
        'json' => 'ADMIN',
123
        'jsonfull' => 'ADMIN',
124
        'jsonone' => 'ADMIN',
125
        'sync' => 'ADMIN',
126
        'addtodb' => 'ADMIN',
127
        'removefromdb' => 'ADMIN',
128
    ];
129
130
    public static function flush()
131
    {
132
        AllFilesInfo::flushCache();
133
    }
134
135
    private static $url_segment = 'admin/assets-overview';
136
137
    public function Link($action = null)
138
    {
139
        $str = Director::absoluteURL(DIRECTORY_SEPARATOR . $this->config()->get('url_segment'));
140
        if ($action) {
141
            $str .= DIRECTORY_SEPARATOR . $action;
142
        }
143
144
        return $str;
145
    }
146
147
    public function getTitle(): string
148
    {
149
        $this->getSortStatement();
150
        $this->getFilterStatement();
151
        $this->getPageStatement();
152
153
        if ($this->hasFilter()) {
154
            $filterStatement = '' .
155
                $this->getAllFilesInfoProvider()->getTotalFileCountFiltered() . ' files / ' .
156
                $this->getTotalFileSizeFiltered();
157
        } else {
158
            $filterStatement =
159
                $this->getTotalFileCountRaw() . ' / ' .
160
                $this->getTotalFileSizeRaw();
161
        }
162
163
        return DBField::create_field(
164
            'HTMLText',
165
            'Found ' . $filterStatement
166
        );
167
    }
168
169
    public function getSubTitle(): string
170
    {
171
        $array = array_filter(
172
            [
173
                $this->getSortStatement(),
174
                $this->getFilterStatement(),
175
                $this->getPageStatement(),
176
                $this->getTotalsStatement(),
177
            ]
178
        );
179
180
        return DBField::create_field(
181
            'HTMLText',
182
            '- ' . implode('<br /> - ', $array)
183
        );
184
    }
185
186
    public function getSortStatement(): string
187
    {
188
        return '<strong>sorted by</strong>: ' . self::SORTERS[$this->sorter]['Title'] ?? 'ERROR IN SORTER';
189
    }
190
191
    public function getFilterStatement(): string
192
    {
193
        $filterArray = array_filter(
194
            [
195
                self::FILTERS[$this->filter]['Title'] ?? '',
196
                implode(', ', $this->allowedExtensions),
197
            ]
198
        );
199
200
        return count($filterArray) ? '<strong>filtered for</strong>: ' . implode(', ', $filterArray) : '';
201
    }
202
203
    public function getPageStatement(): string
204
    {
205
        return $this->getNumberOfPages() > 1 ?
206
            '<strong>page</strong>: ' . $this->pageNumber . ' of ' . $this->getNumberOfPages() . ', showing file ' . ($this->startLimit + 1) . ' to ' . $this->endLimit
207
            :
208
            '';
209
    }
210
211
    public function getDisplayer(): string
212
    {
213
        return $this->displayer;
214
    }
215
216
    public function getFilesAsArrayList(): ArrayList
217
    {
218
        return $this->getAllFilesInfoProvider()->getFilesAsArrayList();
219
    }
220
221
    public function getFilesAsArray(): array
222
    {
223
        return $this->getFilesAsArrayList()->toArray();
224
    }
225
226
    public function getFilesAsSortedArrayList(): ArrayList
227
    {
228
        return $this->getAllFilesInfoProvider()->getFilesAsSortedArrayList();
229
    }
230
231
    public function getTotalFileCountRaw(): string
232
    {
233
        return (string) number_format($this->getAllFilesInfoProvider()->getTotalFileCountRaw());
234
    }
235
236
    public function getTotalFileCountFilteredAndFormatted(): string
237
    {
238
        return (string) number_format($this->getAllFilesInfoProvider()->getTotalFileCountFiltered());
239
    }
240
241
    public function getTotalFileSizeFiltered(): string
242
    {
243
        return (string) $this->humanFileSize($this->getAllFilesInfoProvider()->getTotalFileSizeFiltered());
244
    }
245
246
    public function getTotalFileSizeRaw(): string
247
    {
248
        return (string) $this->humanFileSize($this->getAllFilesInfoProvider()->getTotalFileSizesRaw());
249
    }
250
251
    public function index($request)
252
    {
253
        if ('rawlistfull' === $this->displayer) {
254
            $this->addMapToItems();
255
        }
256
257
        // if (false === AllFilesInfo::loadedFromCache()) {
258
        //     $url = $_SERVER['REQUEST_URI'];
259
        //     $url = str_replace('flush=', 'previousflush=', $url);
260
261
        //     $js = '<script>window.location.href = \'' . $url . '\';</script>';
262
        //     return 'go to <a href="' . $url . '">' . $url . '</a> if this page does not autoload';
263
        // }
264
265
266
        return $this->renderWith('AssetsOverview');
267
    }
268
269
    public function json($request)
270
    {
271
        return $this->sendJSON($this->getRawData());
272
    }
273
274
    public function jsonfull($request)
275
    {
276
        $array = [];
277
278
        foreach ($this->getFilesAsArray() as $item) {
279
            $array[] = $item->toMap();
280
        }
281
282
        return $this->sendJSON($array);
283
    }
284
285
    public function jsonone($request)
286
    {
287
        $array = [];
0 ignored issues
show
The assignment to $array is dead and can be removed.
Loading history...
288
        $location = $this->request->getVar('path');
289
        $obj = OneFileInfo::inst($location);
0 ignored issues
show
It seems like $location can also be of type null; however, parameter $path of Sunnysideup\AssetsOvervi...les\OneFileInfo::inst() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

289
        $obj = OneFileInfo::inst(/** @scrutinizer ignore-type */ $location);
Loading history...
290
        $obj->setNoCache(true);
291
292
        return $this->sendJSON($obj->toArray());
293
    }
294
295
    public function sync()
296
    {
297
        $obj = $this->getAddAndRemoveFromDbClass();
298
        foreach ($this->getFilesAsArray() as $item) {
299
            $obj->run($item->toMap());
300
        }
301
    }
302
303
    public function addtodb()
304
    {
305
        $obj = $this->getAddAndRemoveFromDbClass();
306
        foreach ($this->getFilesAsArray() as $item) {
307
            $obj->run($item->toMap(), 'add');
308
        }
309
    }
310
311
    public function removefromdb()
312
    {
313
        $obj = $this->getAddAndRemoveFromDbClass();
314
        foreach ($this->getFilesAsArrayList()->toArray() as $item) {
315
            $obj->run($item->toMap(), 'remove');
316
        }
317
    }
318
319
    protected function getAddAndRemoveFromDbClass(): AddAndRemoveFromDb
320
    {
321
        $obj = Injector::inst()->get(AddAndRemoveFromDb::class);
322
        return $obj->setIsDryRun($this->dryRun);
323
    }
324
325
    public function addMapToItems()
326
    {
327
        $this->isThumbList = false;
0 ignored issues
show
Bug Best Practice introduced by
The property isThumbList does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
328
        foreach ($this->getFilesAsSortedArrayList() as $group) {
329
            foreach ($group->Items as $item) {
330
                $map = $item->toMap();
331
                $item->FullFields = ArrayList::create();
332
                foreach ($map as $key => $value) {
333
                    if (false === $value) {
334
                        $value = 'no';
335
                    }
336
337
                    if (true === $value) {
338
                        $value = 'yes';
339
                    }
340
341
                    $item->FullFields->push(ArrayData::create(['Key' => $key, 'Value' => $value]));
342
                }
343
            }
344
        }
345
    }
346
347
    //#############################################
348
    // FORM
349
    //#############################################
350
    public function Form()
351
    {
352
        return $this->getForm();
353
    }
354
355
    protected function init()
356
    {
357
        parent::init();
358
        if (! Permission::check('ADMIN')) {
359
            return Security::permissionFailure($this);
360
        }
361
362
        Requirements::clear();
363
        ini_set('memory_limit', '1024M');
364
        Environment::increaseMemoryLimitTo();
365
        Environment::increaseTimeLimitTo(7200);
366
        SSViewer::config()->set('theme_enabled', false);
367
        Versioned::set_stage(Versioned::DRAFT);
368
        $this->getGetVariables();
369
        $this->getAllFilesInfoProvider();
370
    }
371
372
    protected function getTotalsStatement()
373
    {
374
        return $this->hasFilter() ? '<strong>Totals</strong>: ' .
375
            $this->getTotalFileCountRaw() . ' files / ' . $this->getTotalFileSizeRaw()
376
            : '';
377
    }
378
379
    protected function hasFilter(): bool
380
    {
381
        return $this->filter || count($this->allowedExtensions);
382
    }
383
384
    protected function getGetVariables()
385
    {
386
        $filter = $this->request->getVar('filter');
387
        if ($filter) {
388
            $this->filter = $filter;
389
        }
390
391
        $sorter = $this->request->getVar('sorter');
392
        if ($sorter) {
393
            $this->sorter = $sorter;
394
        }
395
396
        $displayer = $this->request->getVar('displayer');
397
        if ($displayer) {
398
            $this->displayer = $displayer;
399
        }
400
401
        $extensions = $this->request->getVar('extensions');
402
        if ($extensions) {
403
            if (! is_array($extensions)) {
404
                $extensions = [$extensions];
405
            }
406
407
            $this->allowedExtensions = $extensions;
408
            //make sure all are valid!
409
            $this->allowedExtensions = array_filter($this->allowedExtensions);
410
        }
411
412
        $limit = $this->request->getVar('limit');
413
        if ($limit) {
414
            $this->limit = $limit;
415
        }
416
417
        $this->pageNumber = ($this->request->getVar('page') ?: 1);
418
        $this->startLimit = $this->limit * ($this->pageNumber - 1);
419
        $this->endLimit = $this->limit * ($this->pageNumber + 0);
420
        $this->getAllFilesInfoProvider();
421
    }
422
423
    protected function sendJSON($data)
424
    {
425
        $json = json_encode($data, JSON_PRETTY_PRINT);
426
        if ($this->request->getVar('download')) {
427
            return HTTPRequest::send_file($json, 'files.json', 'text/json');
428
        }
429
        $response = (new HTTPResponse($json));
430
        $response->addHeader('Content-Type', 'application/json; charset="utf-8"');
431
        $response->addHeader('Pragma', 'no-cache');
432
        $response->addHeader('cache-control', 'no-cache, no-store, must-revalidate');
433
        $response->addHeader('Access-Control-Allow-Origin', '*');
434
        $response->addHeader('Expires', 0);
435
        HTTPCacheControlMiddleware::singleton()
436
            ->disableCache()
437
        ;
438
        $response->output();
439
        die();
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
440
    }
441
442
443
444
445
    protected function getForm(): Form
446
    {
447
        $fieldList = FieldList::create(
448
            [
449
                $this->createFormField('sorter', 'Sort by', $this->sorter, $this->getSorterList()),
450
                $this->createFormField('filter', 'Filter for errors', $this->filter, $this->getFilterList()),
451
                $this->createFormField('extensions', 'Filter by extensions', $this->allowedExtensions, $this->getExtensionList()),
452
                $this->createFormField('displayer', 'Displayed by', $this->displayer, $this->getDisplayerList()),
453
                $this->createFormField('limit', 'Items per page', $this->limit, $this->getLimitList()),
454
                $this->createFormField('page', 'Page number', $this->pageNumber, $this->getPageNumberList()),
455
                // TextField::create('compare', 'Compare With')->setDescription('add a link to a comparison file - e.g. http://oldsite.com/admin/assets-overview/test.json'),
456
            ]
457
        );
458
        $actionList = FieldList::create(
459
            [
460
                FormAction::create('index', 'Update File List'),
461
            ]
462
        );
463
464
        $form = Form::create($this, 'index', $fieldList, $actionList);
465
        $form->setFormMethod('GET', true);
466
        $form->disableSecurityToken();
467
468
        return $form;
469
    }
470
471
    protected function createFormField(string $name, string $title, $value, ?array $list = [])
472
    {
473
        $listCount = count($list);
474
        if (0 === $listCount) {
475
            $type = HiddenField::class;
476
        } elseif ('limit' === $name || 'page' === $name) {
477
            $type = DropdownField::class;
478
        } elseif ('extensions' === $name) {
479
            $type = CheckboxSetField::class;
480
        } elseif ($listCount < 20) {
481
            $type = OptionsetField::class;
482
        } else {
483
            $type = DropdownField::class;
484
        }
485
486
        $field = $type::create($name, $title)
487
            ->setValue($value);
488
        if ($listCount) {
489
            $field->setSource($list);
490
        }
491
492
        // $field->setAttribute('onchange', 'this.form.submit()');
493
494
        return $field;
495
    }
496
497
    protected function getSorterList(): array
498
    {
499
        $array = [];
500
        foreach (self::SORTERS as $key => $data) {
501
            $array[$key] = $data['Title'];
502
        }
503
504
        return $array;
505
    }
506
507
    protected function getFilterList(): array
508
    {
509
        $array = ['' => '-- no filter --'];
510
        foreach (self::FILTERS as $key => $data) {
511
            $array[$key] = $data['Title'];
512
        }
513
514
        return $array;
515
    }
516
517
    protected function getDisplayerList(): array
518
    {
519
        return self::DISPLAYERS;
520
    }
521
522
    protected function getExtensionList(): array
523
    {
524
        $list = array_filter($this->getAllFilesInfoProvider()->getAvailableExtensions());
525
        $list = ['n/a' => 'n/a'] + $list;
526
        return $list;
527
    }
528
529
    protected function getPageNumberList(): array
530
    {
531
        $list = range(1, $this->getNumberOfPages());
532
        $list = array_combine($list, $list);
533
        $list[(string) $this->pageNumber] = (string) $this->pageNumber;
534
        if (count($list) < 2) {
535
            return [];
536
        }
537
538
        return $list;
539
    }
540
541
    protected function getNumberOfPages(): int
542
    {
543
        return ceil($this->getAllFilesInfoProvider()->getTotalFileCountFiltered() / $this->limit);
0 ignored issues
show
Bug Best Practice introduced by
The expression return ceil($this->getAl...tered() / $this->limit) returns the type double which is incompatible with the type-hinted return integer.
Loading history...
544
    }
545
546
    protected function getLimitList(): array
547
    {
548
        $step = 100;
549
        $array = [];
550
        $i = 0;
0 ignored issues
show
The assignment to $i is dead and can be removed.
Loading history...
551
        $totalRaw = (int)  $this->getAllFilesInfoProvider()->getTotalFileCountRaw();
552
        $totalFiltered = (int)  $this->getAllFilesInfoProvider()->getTotalFileCountFiltered();
553
        if ($totalRaw > $step) {
554
            for ($i = $step; ($i - $step) < $totalFiltered; $i += $step) {
555
                if ($i > $this->limit && ! isset($array[$this->limit])) {
556
                    $array[$this->limit] = $this->limit;
557
                }
558
559
                $array[$i] = $i;
560
            }
561
        }
562
563
        return $array;
564
    }
565
566
567
568
    protected function getRawData(): array
569
    {
570
        return $this->getAllFilesInfoProvider()->toArray();
571
    }
572
573
    protected function getAllFilesInfoProvider(): AllFilesInfo
574
    {
575
        if (!self::$allFilesProvider) {
576
            /** @var AllFilesInfo self::$allFilesProvider */
577
            self::$allFilesProvider = AllFilesInfo::inst();
578
            self::$allFilesProvider
579
                ->setFilters(self::get_filters())
580
                ->setSorters(self::get_sorters())
581
                ->setFilter($this->filter)
582
                ->setAllowedExtensions($this->allowedExtensions)
583
                ->setSorter($this->sorter)
584
                ->setLimit($this->limit)
585
                ->setPageNumber($this->pageNumber)
586
                ->setStartLimit($this->startLimit)
587
                ->setEndLimit($this->endLimit)
588
                ->setDisplayer($this->displayer)
589
                ->getFilesAsArrayList();
590
            while ($this->startLimit > $this->filesAsArrayList->count()) {
0 ignored issues
show
Bug Best Practice introduced by
The property filesAsArrayList does not exist on Sunnysideup\AssetsOverview\Control\View. Since you implemented __get, consider adding a @property annotation.
Loading history...
The method count() 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

590
            while ($this->startLimit > $this->filesAsArrayList->/** @scrutinizer ignore-call */ count()) {

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...
591
                $this->pageNumber--;
592
                $this->startLimit = $this->limit * ($this->pageNumber - 1);
593
                $this->endLimit = $this->limit * ($this->pageNumber + 0);
594
            }
595
        }
596
        return self::$allFilesProvider;
597
    }
598
599
600
    private const SORTERS = [
601
        'byfolder' => [
602
            'Title' => 'Folder',
603
            'Sort' => 'PathFolderFromAssets',
604
            'Group' => 'PathFolderFromAssets',
605
        ],
606
        'byfilename' => [
607
            'Title' => 'Filename',
608
            'Sort' => 'PathFileName',
609
            'Group' => 'PathFileNameFirstLetter',
610
        ],
611
        'bydbtitle' => [
612
            'Title' => 'Database Title',
613
            'Sort' => 'DBTitle',
614
            'Group' => 'DBTitleFirstLetter',
615
        ],
616
        'byfilesize' => [
617
            'Title' => 'Filesize',
618
            'Sort' => 'PathFileSize',
619
            'Group' => 'HumanFileSizeRounded',
620
        ],
621
        'bylastedited' => [
622
            'Title' => 'Last Edited',
623
            'Sort' => 'DBLastEditedTS',
624
            'Group' => 'DBLastEdited',
625
        ],
626
        'byextension' => [
627
            'Title' => 'PathExtension',
628
            'Sort' => 'PathExtensionAsLower',
629
            'Group' => 'PathExtensionAsLower',
630
        ],
631
        'byisimage' => [
632
            'Title' => 'Image vs Other Files',
633
            'Sort' => 'IsImage',
634
            'Group' => 'HumanImageIsImage',
635
        ],
636
        'byclassname' => [
637
            'Title' => 'Class Name',
638
            'Sort' => 'DBClassName',
639
            'Group' => 'DBClassName',
640
        ],
641
        'bydimensions' => [
642
            'Title' => 'Dimensions (small to big)',
643
            'Sort' => 'ImagePixels',
644
            'Group' => 'HumanImageDimensions',
645
        ],
646
        'byratio' => [
647
            'Title' => 'ImageRatio',
648
            'Sort' => 'ImageRatio',
649
            'Group' => 'ImageRatio',
650
        ],
651
    ];
652
653
    private const FILTERS = [
654
        'byanyerror' => [
655
            'Title' => 'Any Error',
656
            'Field' => 'ErrorHasAnyError',
657
            'Values' => [1, true],
658
        ],
659
        'byfilesystemstatus' => [
660
            'Title' => 'Not in filesystem',
661
            'Field' => 'ErrorIsInFileSystem',
662
            'Values' => [1, true],
663
        ],
664
        'bymissingfromdatabase' => [
665
            'Title' => 'Not in database',
666
            'Field' => 'ErrorDBNotPresent',
667
            'Values' => [1, true],
668
        ],
669
        'bymissingfromlive' => [
670
            'Title' => 'Not on live site',
671
            'Field' => 'ErrorDBNotPresentLive',
672
            'Values' => [1, true],
673
        ],
674
        'bymissingfromstaging' => [
675
            'Title' => 'Not on draft site',
676
            'Field' => 'ErrorDBNotPresentStaging',
677
            'Values' => [1, true],
678
        ],
679
        'bydraftonly' => [
680
            'Title' => 'In draft only (not on live)',
681
            'Field' => 'ErrorInDraftOnly',
682
            'Values' => [1, true],
683
        ],
684
        'byliveonly' => [
685
            'Title' => 'On live only (not in draft)',
686
            'Field' => 'ErrorNotInDraft',
687
            'Values' => [1, true],
688
        ],
689
        'byfoldererror' => [
690
            'Title' => 'Folder error',
691
            'Field' => 'ErrorParentID',
692
            'Values' => [1, true],
693
        ],
694
        'bydatabaseerror' => [
695
            'Title' => 'Error in file name',
696
            'Field' => 'ErrorInFilename',
697
            'Values' => [1, true],
698
        ],
699
        'byextensionerror' => [
700
            'Title' => 'UPPER/lower case error in file type',
701
            'Field' => 'ErrorExtensionMisMatch',
702
            'Values' => [1, true],
703
        ],
704
        'byextensionallowed' => [
705
            'Title' => 'Extension not allowed',
706
            'Field' => 'ErrorInvalidExtension',
707
            'Values' => [1, true],
708
        ],
709
        'by3to4error' => [
710
            'Title' => 'Potential SS4 migration error',
711
            'Field' => 'ErrorInSs3Ss4Comparison',
712
            'Values' => [1, true],
713
        ],
714
    ];
715
    /**
716
     * @var array<string, string>
717
     */
718
    private const DISPLAYERS = [
719
        'thumbs' => 'Thumbnails',
720
        'rawlist' => 'File List',
721
        'rawlistfull' => 'Raw Data',
722
    ];
723
}
724