Scrutinizer GitHub App not installed

We could not synchronize checks via GitHub's checks API since Scrutinizer's GitHub App is not installed for this repository.

Install GitHub App

GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

Helper::whereExpression()   B
last analyzed

Complexity

Conditions 7
Paths 6

Size

Total Lines 25
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 2 Features 0
Metric Value
cc 7
eloc 18
c 4
b 2
f 0
nc 6
nop 2
dl 0
loc 25
rs 8.8333
1
<?php
2
3
/**
4
 * (c) Kitodo. Key to digital objects e.V. <[email protected]>
5
 *
6
 * This file is part of the Kitodo and TYPO3 projects.
7
 *
8
 * @license GNU General Public License version 3 or later.
9
 * For the full copyright and license information, please read the
10
 * LICENSE.txt file that was distributed with this source code.
11
 */
12
13
namespace Kitodo\Dlf\Common;
14
15
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
16
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
17
use TYPO3\CMS\Core\Core\Environment;
18
use TYPO3\CMS\Core\Database\ConnectionPool;
19
use TYPO3\CMS\Core\Http\Uri;
20
use TYPO3\CMS\Core\Http\ApplicationType;
21
use TYPO3\CMS\Core\Http\RequestFactory;
22
use TYPO3\CMS\Core\Log\LogManager;
23
use TYPO3\CMS\Core\Context\Context;
24
use TYPO3\CMS\Core\Localization\LanguageService;
25
use TYPO3\CMS\Core\Messaging\FlashMessage;
26
use TYPO3\CMS\Core\Messaging\FlashMessageService;
27
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
28
use TYPO3\CMS\Core\Resource\MimeTypeCollection;
29
use TYPO3\CMS\Core\Utility\ArrayUtility;
30
use TYPO3\CMS\Core\Utility\GeneralUtility;
31
use TYPO3\CMS\Core\Utility\MathUtility;
32
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
33
use TYPO3\CMS\Core\DataHandling\DataHandler;
34
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
35
36
/**
37
 * Helper class for the 'dlf' extension
38
 *
39
 * @package TYPO3
40
 * @subpackage dlf
41
 *
42
 * @access public
43
 */
44
class Helper
45
{
46
    /**
47
     * @access public
48
     * @static
49
     * @var string The extension key
50
     */
51
    public static string $extKey = 'dlf';
52
53
    /**
54
     * @access protected
55
     * @static
56
     * @var string This holds the cipher algorithm
57
     *
58
     * @see openssl_get_cipher_methods() for options
59
     */
60
    protected static string $cipherAlgorithm = 'aes-256-ctr';
61
62
    /**
63
     * @access protected
64
     * @static
65
     * @var string This holds the hash algorithm
66
     *
67
     * @see openssl_get_md_methods() for options
68
     */
69
    protected static string $hashAlgorithm = 'sha256';
70
71
    /**
72
     * @access protected
73
     * @static
74
     * @var array The locallang array for flash messages
75
     */
76
    protected static array $messages = [];
77
78
    /**
79
     * Generates a flash message and adds it to a message queue.
80
     *
81
     * @access public
82
     *
83
     * @static
84
     *
85
     * @param string $message The body of the message
86
     * @param string $title The title of the message
87
     * @param int $severity The message's severity
88
     * @param bool $session Should the message be saved in the user's session?
89
     * @param string $queue The queue's unique identifier
90
     *
91
     * @return FlashMessageQueue The queue the message was added to
92
     */
93
    public static function addMessage(string $message, string $title, int $severity, bool $session = false, string $queue = 'kitodo.default.flashMessages'): FlashMessageQueue
94
    {
95
        $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
96
        $flashMessageQueue = $flashMessageService->getMessageQueueByIdentifier($queue);
97
        $flashMessage = GeneralUtility::makeInstance(
98
            FlashMessage::class,
99
            $message,
100
            $title,
101
            $severity,
102
            $session
103
        );
104
        $flashMessageQueue->enqueue($flashMessage);
105
        return $flashMessageQueue;
106
    }
107
108
    /**
109
     * Check if given identifier is a valid identifier of the German National Library
110
     *
111
     * @access public
112
     *
113
     * @static
114
     *
115
     * @param string $id The identifier to check
116
     * @param string $type What type is the identifier supposed to be? Possible values: PPN, IDN, PND, ZDB, SWD, GKD
117
     *
118
     * @return bool Is $id a valid GNL identifier of the given $type?
119
     */
120
    public static function checkIdentifier(string $id, string $type): bool
121
    {
122
        $digits = substr($id, 0, 8);
123
        $checksum = self::getChecksum($digits);
124
        switch (strtoupper($type)) {
125
            case 'PPN':
126
            case 'IDN':
127
            case 'PND':
128
                if ($checksum == 10) {
129
                    $checksum = 'X';
130
                }
131
                if (!preg_match('/\d{8}[\dX]{1}/i', $id)) {
132
                    return false;
133
                } elseif (strtoupper(substr($id, -1, 1)) != $checksum) {
134
                    return false;
135
                }
136
                break;
137
            case 'ZDB':
138
                if ($checksum == 10) {
139
                    $checksum = 'X';
140
                }
141
                if (!preg_match('/\d{8}-[\dX]{1}/i', $id)) {
142
                    return false;
143
                } elseif (strtoupper(substr($id, -1, 1)) != $checksum) {
144
                    return false;
145
                }
146
                break;
147
            case 'SWD':
148
                $checksum = 11 - $checksum;
149
                if (!preg_match('/\d{8}-\d{1}/i', $id)) {
150
                    return false;
151
                } elseif ($checksum == 10) {
152
                    //TODO: Binary operation "+" between string and 1 results in an error.
153
                    // @phpstan-ignore-next-line
154
                    return self::checkIdentifier(($digits + 1) . substr($id, -2, 2), 'SWD');
155
                } elseif (substr($id, -1, 1) != $checksum) {
156
                    return false;
157
                }
158
                break;
159
            case 'GKD':
160
                $checksum = 11 - $checksum;
161
                if ($checksum == 10) {
162
                    $checksum = 'X';
163
                }
164
                if (!preg_match('/\d{8}-[\dX]{1}/i', $id)) {
165
                    return false;
166
                } elseif (strtoupper(substr($id, -1, 1)) != $checksum) {
167
                    return false;
168
                }
169
                break;
170
        }
171
        return true;
172
    }
173
174
    /**
175
     * Get checksum for given digits.
176
     *
177
     * @access private
178
     *
179
     * @static
180
     *
181
     * @param string $digits
182
     *
183
     * @return int
184
     */
185
    private static function getChecksum(string $digits): int
186
    {
187
        $checksum = 0;
188
        for ($i = 0, $j = strlen($digits); $i < $j; $i++) {
189
            $checksum += (9 - $i) * (int) substr($digits, $i, 1);
190
        }
191
        return (11 - ($checksum % 11)) % 11;
192
    }
193
194
    /**
195
     * Decrypt encrypted value with given control hash
196
     *
197
     * @access public
198
     *
199
     * @static
200
     *
201
     * @param string $encrypted The encrypted value to decrypt
202
     *
203
     * @return mixed The decrypted value or false on error
204
     */
205
    public static function decrypt(string $encrypted)
206
    {
207
        if (
208
            !in_array(self::$cipherAlgorithm, openssl_get_cipher_methods(true))
209
            || !in_array(self::$hashAlgorithm, openssl_get_md_methods(true))
210
        ) {
211
            self::log('OpenSSL library doesn\'t support cipher and/or hash algorithm', LOG_SEVERITY_ERROR);
212
            return false;
213
        }
214
        if (empty(self::getEncryptionKey())) {
215
            self::log('No encryption key set in TYPO3 configuration', LOG_SEVERITY_ERROR);
216
            return false;
217
        }
218
        if (
219
            empty($encrypted)
220
            || strlen($encrypted) < openssl_cipher_iv_length(self::$cipherAlgorithm)
221
        ) {
222
            self::log('Invalid parameters given for decryption', LOG_SEVERITY_ERROR);
223
            return false;
224
        }
225
        // Split initialisation vector and encrypted data.
226
        $binary = base64_decode($encrypted);
227
        $iv = substr($binary, 0, openssl_cipher_iv_length(self::$cipherAlgorithm));
228
        $data = substr($binary, openssl_cipher_iv_length(self::$cipherAlgorithm));
229
        $key = openssl_digest(self::getEncryptionKey(), self::$hashAlgorithm, true);
0 ignored issues
show
Bug introduced by
It seems like self::getEncryptionKey() can also be of type null; however, parameter $data of openssl_digest() 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

229
        $key = openssl_digest(/** @scrutinizer ignore-type */ self::getEncryptionKey(), self::$hashAlgorithm, true);
Loading history...
230
        // Decrypt data.
231
        return openssl_decrypt($data, self::$cipherAlgorithm, $key, OPENSSL_RAW_DATA, $iv);
232
    }
233
234
    /**
235
     * Try to parse $content into a `SimpleXmlElement`. If $content is not a
236
     * string or does not contain valid XML, `false` is returned.
237
     *
238
     * @access public
239
     *
240
     * @static
241
     *
242
     * @param mixed $content content of file to read
243
     *
244
     * @return \SimpleXMLElement|false
245
     */
246
    public static function getXmlFileAsString($content)
247
    {
248
        // Don't make simplexml_load_string throw (when $content is an array
249
        // or object)
250
        if (!is_string($content)) {
251
            return false;
252
        }
253
254
        // Turn off libxml's error logging.
255
        $libxmlErrors = libxml_use_internal_errors(true);
256
257
        if (\PHP_VERSION_ID < 80000) {
258
            // Disables the functionality to allow external entities to be loaded when parsing the XML, must be kept
259
            $previousValueOfEntityLoader = libxml_disable_entity_loader(true);
260
        }
261
262
        // Try to load XML from file.
263
        $xml = simplexml_load_string($content);
264
265
        if (\PHP_VERSION_ID < 80000) {
266
            // reset entity loader setting
267
            libxml_disable_entity_loader($previousValueOfEntityLoader);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $previousValueOfEntityLoader does not seem to be defined for all execution paths leading up to this point.
Loading history...
268
        }
269
        // Reset libxml's error logging.
270
        libxml_use_internal_errors($libxmlErrors);
271
        return $xml;
272
    }
273
274
    /**
275
     * Add a message to the TYPO3 log
276
     *
277
     * @access public
278
     *
279
     * @static
280
     *
281
     * @param string $message The message to log
282
     * @param int $severity The severity of the message 0 is info, 1 is notice, 2 is warning, 3 is fatal error, -1 is "OK" message
283
     *
284
     * @return void
285
     */
286
    public static function log(string $message, int $severity = 0): void
287
    {
288
        $logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(get_called_class());
289
290
        switch ($severity) {
291
            case 0:
292
                $logger->info($message);
293
                break;
294
            case 1:
295
                $logger->notice($message);
296
                break;
297
            case 2:
298
                $logger->warning($message);
299
                break;
300
            case 3:
301
                $logger->error($message);
302
                break;
303
            default:
304
                break;
305
        }
306
    }
307
308
    /**
309
     * Digest the given string
310
     *
311
     * @access public
312
     *
313
     * @static
314
     *
315
     * @param string $string The string to encrypt
316
     *
317
     * @return mixed Hashed string or false on error
318
     */
319
    public static function digest(string $string)
320
    {
321
        if (!in_array(self::$hashAlgorithm, openssl_get_md_methods(true))) {
322
            self::log('OpenSSL library doesn\'t support hash algorithm', LOG_SEVERITY_ERROR);
323
            return false;
324
        }
325
        // Hash string.
326
        return openssl_digest($string, self::$hashAlgorithm);
327
    }
328
329
    /**
330
     * Encrypt the given string
331
     *
332
     * @access public
333
     *
334
     * @static
335
     *
336
     * @param string $string The string to encrypt
337
     *
338
     * @return mixed Encrypted string or false on error
339
     */
340
    public static function encrypt(string $string)
341
    {
342
        if (
343
            !in_array(self::$cipherAlgorithm, openssl_get_cipher_methods(true))
344
            || !in_array(self::$hashAlgorithm, openssl_get_md_methods(true))
345
        ) {
346
            self::log('OpenSSL library doesn\'t support cipher and/or hash algorithm', LOG_SEVERITY_ERROR);
347
            return false;
348
        }
349
        if (empty(self::getEncryptionKey())) {
350
            self::log('No encryption key set in TYPO3 configuration', LOG_SEVERITY_ERROR);
351
            return false;
352
        }
353
        // Generate random initialization vector.
354
        $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length(self::$cipherAlgorithm));
355
        $key = openssl_digest(self::getEncryptionKey(), self::$hashAlgorithm, true);
0 ignored issues
show
Bug introduced by
It seems like self::getEncryptionKey() can also be of type null; however, parameter $data of openssl_digest() 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

355
        $key = openssl_digest(/** @scrutinizer ignore-type */ self::getEncryptionKey(), self::$hashAlgorithm, true);
Loading history...
356
        // Encrypt data.
357
        $encrypted = openssl_encrypt($string, self::$cipherAlgorithm, $key, OPENSSL_RAW_DATA, $iv);
358
        // Merge initialization vector and encrypted data.
359
        if ($encrypted !== false) {
360
            $encrypted = base64_encode($iv . $encrypted);
361
        }
362
        return $encrypted;
363
    }
364
365
    /**
366
     * Clean up a string to use in an URL.
367
     *
368
     * @access public
369
     *
370
     * @static
371
     *
372
     * @param string $string The string to clean up
373
     *
374
     * @return string The cleaned up string
375
     */
376
    public static function getCleanString(string $string): string
377
    {
378
        // Convert to lowercase.
379
        $string = strtolower($string);
380
        // Remove non-alphanumeric characters.
381
        $string = preg_replace('/[^a-z\d_\s-]/', '', $string);
382
        // Remove multiple dashes or whitespaces.
383
        $string = preg_replace('/[\s-]+/', ' ', $string);
384
        // Convert whitespaces and underscore to dash.
385
        return preg_replace('/[\s_]/', '-', $string);
386
    }
387
388
    /**
389
     * Get the registered hook objects for a class
390
     *
391
     * @access public
392
     *
393
     * @static
394
     *
395
     * @param string $scriptRelPath The path to the class file
396
     *
397
     * @return array Array of hook objects for the class
398
     */
399
    public static function getHookObjects(string $scriptRelPath): array
400
    {
401
        $hookObjects = [];
402
        if (is_array(self::getOptions()[self::$extKey . '/' . $scriptRelPath]['hookClass'])) {
403
            foreach (self::getOptions()[self::$extKey . '/' . $scriptRelPath]['hookClass'] as $classRef) {
404
                $hookObjects[] = GeneralUtility::makeInstance($classRef);
405
            }
406
        }
407
        return $hookObjects;
408
    }
409
410
    /**
411
     * Get the "index_name" for an UID
412
     *
413
     * @access public
414
     *
415
     * @static
416
     *
417
     * @param int $uid The UID of the record
418
     * @param string $table Get the "index_name" from this table
419
     * @param int $pid Get the "index_name" from this page
420
     *
421
     * @return string "index_name" for the given UID
422
     */
423
    public static function getIndexNameFromUid(int $uid, string $table, int $pid = -1): string
424
    {
425
        // Sanitize input.
426
        $uid = max($uid, 0);
427
        if (
428
            !$uid
429
            // NOTE: Only use tables that don't have too many entries!
430
            || !in_array($table, ['tx_dlf_collections', 'tx_dlf_libraries', 'tx_dlf_metadata', 'tx_dlf_metadatasubentries', 'tx_dlf_structures', 'tx_dlf_solrcores'])
431
        ) {
432
            self::log('Invalid UID "' . $uid . '" or table "' . $table . '"', LOG_SEVERITY_ERROR);
433
            return '';
434
        }
435
436
        $makeCacheKey = function ($pid, $uid) {
437
            return $pid . '.' . $uid;
438
        };
439
440
        static $cache = [];
441
        if (!isset($cache[$table])) {
442
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
443
                ->getQueryBuilderForTable($table);
444
445
            $result = $queryBuilder
446
                ->select(
447
                    $table . '.index_name AS index_name',
448
                    $table . '.uid AS uid',
449
                    $table . '.pid AS pid',
450
                )
451
                ->from($table)
452
                ->execute();
453
454
            $cache[$table] = [];
455
456
            while ($row = $result->fetchAssociative()) {
457
                $cache[$table][$makeCacheKey($row['pid'], $row['uid'])]
458
                    = $cache[$table][$makeCacheKey(-1, $row['uid'])]
459
                    = $row['index_name'];
460
            }
461
        }
462
463
        $cacheKey = $makeCacheKey($pid, $uid);
464
        $result = $cache[$table][$cacheKey] ?? '';
465
466
        if ($result === '') {
467
            self::log('No "index_name" with UID ' . $uid . ' and PID ' . $pid . ' found in table "' . $table . '"', LOG_SEVERITY_WARNING);
468
        }
469
470
        return $result;
471
    }
472
473
    /**
474
     * Get language name from ISO code
475
     *
476
     * @access public
477
     *
478
     * @static
479
     *
480
     * @param string $code ISO 639-1 or ISO 639-2/B language code
481
     *
482
     * @return string Localized full name of language or unchanged input
483
     */
484
    public static function getLanguageName(string $code): string
485
    {
486
        // Analyze code and set appropriate ISO table.
487
        $isoCode = strtolower(trim($code));
488
        if (preg_match('/^[a-z]{3}$/', $isoCode)) {
489
            $file = 'EXT:dlf/Resources/Private/Data/iso-639-2b.xlf';
490
        } elseif (preg_match('/^[a-z]{2}$/', $isoCode)) {
491
            $file = 'EXT:dlf/Resources/Private/Data/iso-639-1.xlf';
492
        } else {
493
            // No ISO code, return unchanged.
494
            return $code;
495
        }
496
        $lang = LocalizationUtility::translate('LLL:' . $file . ':' . $code);
497
        if (!empty($lang)) {
498
            return $lang;
499
        } else {
500
            self::log('Language code "' . $code . '" not found in ISO-639 table', LOG_SEVERITY_NOTICE);
501
            return $code;
502
        }
503
    }
504
505
    /**
506
     * Get all document structures as array
507
     *
508
     * @access public
509
     *
510
     * @static
511
     *
512
     * @param int $pid Get the "index_name" from this page only
513
     *
514
     * @return array
515
     */
516
    public static function getDocumentStructures(int $pid = -1): array
517
    {
518
        // TODO: Against redundancy with getIndexNameFromUid
519
520
        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
521
        $queryBuilder = $connectionPool->getQueryBuilderForTable('tx_dlf_structures');
522
523
        $where = '';
524
        // Should we check for a specific PID, too?
525
        if ($pid !== -1) {
526
            $pid = max($pid, 0);
527
            $where = $queryBuilder->expr()->eq('tx_dlf_structures.pid', $pid);
528
        }
529
530
        // Fetch document info for UIDs in $documentSet from DB
531
        $kitodoStructures = $queryBuilder
532
            ->select(
533
                'tx_dlf_structures.uid AS uid',
534
                'tx_dlf_structures.index_name AS indexName'
535
            )
536
            ->from('tx_dlf_structures')
537
            ->where($where)
538
            ->execute();
539
540
        $allStructures = $kitodoStructures->fetchAllAssociative();
541
542
        // make lookup-table indexName -> uid
543
        return array_column($allStructures, 'indexName', 'uid');
544
    }
545
546
    /**
547
     * Determine whether or not $url is a valid URL using HTTP or HTTPS scheme.
548
     *
549
     * @access public
550
     *
551
     * @static
552
     *
553
     * @param string $url
554
     *
555
     * @return bool
556
     */
557
    public static function isValidHttpUrl(string $url): bool
558
    {
559
        if (!GeneralUtility::isValidUrl($url)) {
560
            return false;
561
        }
562
563
        try {
564
            $uri = new Uri($url);
565
            return !empty($uri->getScheme());
566
        } catch (\InvalidArgumentException $e) {
567
            self::log($e->getMessage(), LOG_SEVERITY_ERROR);
568
            return false;
569
        }
570
    }
571
572
    /**
573
     * Process a data and/or command map with TYPO3 core engine as admin.
574
     *
575
     * @access public
576
     *
577
     * @param array $data Data map
578
     * @param array $cmd Command map
579
     * @param bool $reverseOrder Should the data map be reversed?
580
     * @param bool $cmdFirst Should the command map be processed first?
581
     *
582
     * @return array Array of substituted "NEW..." identifiers and their actual UIDs.
583
     */
584
    public static function processDatabaseAsAdmin(array $data = [], array $cmd = [], $reverseOrder = false, $cmdFirst = false)
585
    {
586
        $context = GeneralUtility::makeInstance(Context::class);
587
588
        if (
589
            ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isBackend()
590
            && $context->getPropertyFromAspect('backend.user', 'isAdmin')
591
        ) {
592
            // Instantiate TYPO3 core engine.
593
            $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
594
            // We do not use workspaces and have to bypass restrictions in DataHandler.
595
            $dataHandler->bypassWorkspaceRestrictions = true;
596
            // Load data and command arrays.
597
            $dataHandler->start($data, $cmd);
598
            // Process command map first if default order is reversed.
599
            if (
600
                !empty($cmd)
601
                && $cmdFirst
602
            ) {
603
                $dataHandler->process_cmdmap();
604
            }
605
            // Process data map.
606
            if (!empty($data)) {
607
                $dataHandler->reverseOrder = $reverseOrder;
608
                $dataHandler->process_datamap();
609
            }
610
            // Process command map if processing order is not reversed.
611
            if (
612
                !empty($cmd)
613
                && !$cmdFirst
614
            ) {
615
                $dataHandler->process_cmdmap();
616
            }
617
            return $dataHandler->substNEWwithIDs;
618
        } else {
619
            self::log('Current backend user has no admin privileges', LOG_SEVERITY_ERROR);
620
            return [];
621
        }
622
    }
623
624
    /**
625
     * Fetches and renders all available flash messages from the queue.
626
     *
627
     * @access public
628
     *
629
     * @static
630
     *
631
     * @param string $queue The queue's unique identifier
632
     *
633
     * @return string All flash messages in the queue rendered as HTML.
634
     */
635
    public static function renderFlashMessages(string $queue = 'kitodo.default.flashMessages'): string
636
    {
637
        $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
638
        $flashMessageQueue = $flashMessageService->getMessageQueueByIdentifier($queue);
639
        $flashMessages = $flashMessageQueue->getAllMessagesAndFlush();
640
        return GeneralUtility::makeInstance(KitodoFlashMessageRenderer::class)
641
            ->render($flashMessages);
642
    }
643
644
    /**
645
     * This translates an internal "index_name"
646
     *
647
     * @access public
648
     *
649
     * @static
650
     *
651
     * @param string $indexName The internal "index_name" to translate
652
     * @param string $table Get the translation from this table
653
     * @param string $pid Get the translation from this page
654
     *
655
     * @return string Localized label for $indexName
656
     */
657
    public static function translate(string $indexName, string $table, string $pid): string
658
    {
659
        // Load labels into static variable for future use.
660
        static $labels = [];
661
        // Sanitize input.
662
        $pid = max((int) $pid, 0);
663
        if (!$pid) {
664
            self::log('Invalid PID ' . $pid . ' for translation', LOG_SEVERITY_WARNING);
665
            return $indexName;
666
        }
667
        /** @var PageRepository $pageRepository */
668
        $pageRepository = GeneralUtility::makeInstance(PageRepository::class);
669
670
        $languageAspect = GeneralUtility::makeInstance(Context::class)->getAspect('language');
671
        $languageContentId = $languageAspect->getContentId();
672
673
        // Check if "index_name" is an UID.
674
        if (MathUtility::canBeInterpretedAsInteger($indexName)) {
675
            $indexName = self::getIndexNameFromUid((int) $indexName, $table, $pid);
676
        }
677
        /* $labels already contains the translated content element, but with the index_name of the translated content element itself
678
         * and not with the $indexName of the original that we receive here. So we have to determine the index_name of the
679
         * associated translated content element. E.g. $labels['title0'] != $indexName = title. */
680
681
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
682
            ->getQueryBuilderForTable($table);
683
684
        // First fetch the uid of the received index_name
685
        $result = $queryBuilder
686
            ->select(
687
                $table . '.uid AS uid',
688
                $table . '.l18n_parent AS l18n_parent'
689
            )
690
            ->from($table)
691
            ->where(
692
                $queryBuilder->expr()->eq($table . '.pid', $pid),
693
                $queryBuilder->expr()->eq($table . '.index_name', $queryBuilder->expr()->literal($indexName)),
694
                self::whereExpression($table, true)
695
            )
696
            ->setMaxResults(1)
697
            ->execute();
698
699
        $row = $result->fetchAssociative();
700
701
        if ($row) {
702
            // Now we use the uid of the l18_parent to fetch the index_name of the translated content element.
703
            $result = $queryBuilder
704
                ->select($table . '.index_name AS index_name')
705
                ->from($table)
706
                ->where(
707
                    $queryBuilder->expr()->eq($table . '.pid', $pid),
708
                    $queryBuilder->expr()->eq($table . '.uid', $row['l18n_parent']),
709
                    $queryBuilder->expr()->eq($table . '.sys_language_uid', (int) $languageContentId),
710
                    self::whereExpression($table, true)
711
                )
712
                ->setMaxResults(1)
713
                ->execute();
714
715
            $row = $result->fetchAssociative();
716
717
            if ($row) {
718
                // If there is an translated content element, overwrite the received $indexName.
719
                $indexName = $row['index_name'];
720
            }
721
        }
722
723
        // Check if we already got a translation.
724
        if (empty($labels[$table][$pid][$languageContentId][$indexName])) {
725
            // Check if this table is allowed for translation.
726
            if (in_array($table, ['tx_dlf_collections', 'tx_dlf_libraries', 'tx_dlf_metadata', 'tx_dlf_metadatasubentries', 'tx_dlf_structures'])) {
727
                $additionalWhere = $queryBuilder->expr()->in($table . '.sys_language_uid', [-1, 0]);
728
                if ($languageContentId > 0) {
729
                    $additionalWhere = $queryBuilder->expr()->andX(
730
                        $queryBuilder->expr()->orX(
731
                            $queryBuilder->expr()->in($table . '.sys_language_uid', [-1, 0]),
732
                            $queryBuilder->expr()->eq($table . '.sys_language_uid', (int) $languageContentId)
733
                        ),
734
                        $queryBuilder->expr()->eq($table . '.l18n_parent', 0)
735
                    );
736
                }
737
738
                // Get labels from database.
739
                $result = $queryBuilder
740
                    ->select('*')
741
                    ->from($table)
742
                    ->where(
743
                        $queryBuilder->expr()->eq($table . '.pid', $pid),
744
                        $additionalWhere,
745
                        self::whereExpression($table, true)
746
                    )
747
                    ->setMaxResults(10000)
748
                    ->execute();
749
750
                if ($result->rowCount() > 0) {
751
                    while ($resArray = $result->fetchAssociative()) {
752
                        // Overlay localized labels if available.
753
                        if ($languageContentId > 0) {
754
                            $resArray = $pageRepository->getRecordOverlay($table, $resArray, $languageContentId, $languageAspect->getLegacyOverlayType());
755
                        }
756
                        if ($resArray) {
757
                            $labels[$table][$pid][$languageContentId][$resArray['index_name']] = $resArray['label'];
758
                        }
759
                    }
760
                } else {
761
                    self::log('No translation with PID ' . $pid . ' available in table "' . $table . '" or translation not accessible', LOG_SEVERITY_NOTICE);
762
                }
763
            } else {
764
                self::log('No translations available for table "' . $table . '"', LOG_SEVERITY_WARNING);
765
            }
766
        }
767
768
        if (!empty($labels[$table][$pid][$languageContentId][$indexName])) {
769
            return $labels[$table][$pid][$languageContentId][$indexName];
770
        } else {
771
            return $indexName;
772
        }
773
    }
774
775
    /**
776
     * This returns the additional WHERE expression of a table based on its TCA configuration
777
     *
778
     * @access public
779
     *
780
     * @static
781
     *
782
     * @param string $table Table name as defined in TCA
783
     * @param bool $showHidden Ignore the hidden flag?
784
     *
785
     * @return string Additional WHERE expression
786
     */
787
    public static function whereExpression(string $table, bool $showHidden = false): string
788
    {
789
        if (!Environment::isCli() && ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend()) {
790
            // Should we ignore the record's hidden flag?
791
            $ignoreHide = 0;
792
            if ($showHidden) {
793
                $ignoreHide = 1;
794
            }
795
            /** @var PageRepository $pageRepository */
796
            $pageRepository = GeneralUtility::makeInstance(PageRepository::class);
797
798
            $expression = $pageRepository->enableFields($table, $ignoreHide);
799
            if (!empty($expression)) {
800
                return substr($expression, 5);
801
            } else {
802
                return '';
803
            }
804
        } elseif (Environment::isCli() || ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isBackend()) {
805
            return GeneralUtility::makeInstance(ConnectionPool::class)
806
                ->getQueryBuilderForTable($table)
807
                ->expr()
808
                ->eq($table . '.' . $GLOBALS['TCA'][$table]['ctrl']['delete'], 0);
809
        } else {
810
            self::log('Unexpected application type (neither frontend or backend)', LOG_SEVERITY_ERROR);
811
            return '1=-1';
812
        }
813
    }
814
815
    /**
816
     * Prevent instantiation by hiding the constructor
817
     *
818
     * @access private
819
     *
820
     * @return void
821
     */
822
    private function __construct()
823
    {
824
        // This is a static class, thus no instances should be created.
825
    }
826
827
    /**
828
     * Returns the LanguageService
829
     *
830
     * @access public
831
     *
832
     * @static
833
     *
834
     * @return LanguageService
835
     */
836
    public static function getLanguageService(): LanguageService
837
    {
838
        return $GLOBALS['LANG'];
839
    }
840
841
    /**
842
     * Replacement for the TYPO3 GeneralUtility::getUrl().
843
     *
844
     * This method respects the User Agent settings from extConf
845
     *
846
     * @access public
847
     *
848
     * @static
849
     *
850
     * @param string $url
851
     *
852
     * @return string|bool
853
     */
854
    public static function getUrl(string $url)
855
    {
856
        if (!Helper::isValidHttpUrl($url)) {
857
            return false;
858
        }
859
860
        // Get extension configuration.
861
        $extConf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('dlf', 'general');
862
863
        /** @var RequestFactory $requestFactory */
864
        $requestFactory = GeneralUtility::makeInstance(RequestFactory::class);
865
        $configuration = [
866
            'timeout' => 30,
867
            'headers' => [
868
                'User-Agent' => $extConf['userAgent'] ?? 'Kitodo.Presentation Proxy',
869
            ],
870
        ];
871
        try {
872
            $response = $requestFactory->request($url, 'GET', $configuration);
873
        } catch (\Exception $e) {
874
            self::log('Could not fetch data from URL "' . $url . '". Error: ' . $e->getMessage() . '.', LOG_SEVERITY_WARNING);
875
            return false;
876
        }
877
        return $response->getBody()->getContents();
878
    }
879
880
    /**
881
     * Check if given value is a valid XML ID.
882
     * @see https://www.w3.org/TR/xmlschema-2/#ID
883
     *
884
     * @access public
885
     *
886
     * @static
887
     *
888
     * @param mixed $id The ID value to check
889
     *
890
     * @return bool TRUE if $id is valid XML ID, FALSE otherwise
891
     */
892
    public static function isValidXmlId($id): bool
893
    {
894
        return preg_match('/^[_a-z][_a-z0-9-.]*$/i', $id) === 1;
895
    }
896
897
    /**
898
     * Get options from local configuration.
899
     *
900
     * @access private
901
     *
902
     * @static
903
     *
904
     * @return array
905
     */
906
    private static function getOptions(): array
907
    {
908
        return self::getLocalConfigurationByPath('SC_OPTIONS');
909
    }
910
911
    /**
912
     * Get encryption key from local configuration.
913
     *
914
     * @access private
915
     *
916
     * @static
917
     *
918
     * @return string|null
919
     */
920
    private static function getEncryptionKey(): ?string
921
    {
922
        return self::getLocalConfigurationByPath('SYS/encryptionKey');
923
    }
924
925
    /**
926
     * Get local configuration for given path.
927
     *
928
     * @access private
929
     *
930
     * @static
931
     *
932
     * @param string $path
933
     *
934
     * @return mixed
935
     */
936
    private static function getLocalConfigurationByPath(string $path)
937
    {
938
        $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
939
940
        if (array_key_exists(strtok($path, '/'), $configurationManager->getLocalConfiguration())) {
941
            return $configurationManager->getLocalConfigurationValueByPath($path);
942
        }
943
944
        return ArrayUtility::getValueByPath($GLOBALS['TYPO3_CONF_VARS'], $path);
945
    }
946
947
    /**
948
     * Filters a file based on its mimetype categories.
949
     *
950
     * This method checks if the provided file array contains a specified mimetype key and
951
     * verifies if the mimetype belongs to any of the specified categories or matches any of the additional custom mimetypes.
952
     *
953
     * @param mixed $file The file array to filter
954
     * @param array $categories The MIME type categories to filter by (e.g., ['audio'], ['video'] or ['image', 'application'])
955
     * @param array $dlfMimeTypes The custom DLF mimetype keys IIIF, IIP or ZOOMIFY to check against (default is an empty array)
956
     * @param string $mimeTypeKey The key used to access the mimetype in the file array (default is 'mimetype')
957
     *
958
     * @return bool True if the file mimetype belongs to any of the specified categories or matches any custom mimetypes, false otherwise
959
     */
960
    public static function filterFilesByMimeType($file, array $categories, array $dlfMimeTypes = [], string $mimeTypeKey = 'mimetype'): bool
961
    {
962
        // Retrieves MIME types from the TYPO3 Core MimeTypeCollection
963
        $mimeTypeCollection = GeneralUtility::makeInstance(MimeTypeCollection::class);
964
        $mimeTypes = array_filter(
965
            $mimeTypeCollection->getMimeTypes(),
966
            function ($mimeType) use ($categories) {
967
                foreach ($categories as $category) {
968
                    if (strpos($mimeType, $category . '/') === 0) {
969
                        return true;
970
                    }
971
                }
972
                return false;
973
            }
974
        );
975
976
        // Custom dlf MIME types
977
        $dlfMimeTypeArray = [
978
            'IIIF' => 'application/vnd.kitodo.iiif',
979
            'IIP' => 'application/vnd.netfpx',
980
            'ZOOMIFY' => 'application/vnd.kitodo.zoomify'
981
        ];
982
983
        // Filter custom MIME types based on provided keys
984
        $filteredDlfMimeTypes = array_intersect_key($dlfMimeTypeArray, array_flip($dlfMimeTypes));
985
986
        if (is_array($file) && isset($file[$mimeTypeKey])) {
987
            return in_array($file[$mimeTypeKey], $mimeTypes) || in_array($file[$mimeTypeKey], $filteredDlfMimeTypes);
988
        }
989
        return false;
990
    }
991
}
992