Completed
Push — master ( 5b15d1...83bd13 )
by
unknown
18s queued 14s
created

Helper::mergeRecursiveWithOverrule()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 4
rs 10
cc 1
nc 1
nop 5
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\Database\ConnectionPool;
16
use TYPO3\CMS\Core\Log\LogManager;
17
use TYPO3\CMS\Core\Utility\GeneralUtility;
18
19
/**
20
 * Helper class for the 'dlf' extension
21
 *
22
 * @author Sebastian Meyer <[email protected]>
23
 * @author Henrik Lochmann <[email protected]>
24
 * @package TYPO3
25
 * @subpackage dlf
26
 * @access public
27
 */
28
class Helper
29
{
30
    /**
31
     * The extension key
32
     *
33
     * @var string
34
     * @access public
35
     */
36
    public static $extKey = 'dlf';
37
38
    /**
39
     * This holds the cipher algorithm
40
     * @see openssl_get_cipher_methods() for options
41
     *
42
     * @var string
43
     * @access protected
44
     */
45
    protected static $cipherAlgorithm = 'aes-256-ctr';
46
47
    /**
48
     * This holds the hash algorithm
49
     * @see openssl_get_md_methods() for options
50
     *
51
     * @var string
52
     * @access protected
53
     */
54
    protected static $hashAlgorithm = 'sha256';
55
56
    /**
57
     * The locallang array for flash messages
58
     *
59
     * @var array
60
     * @access protected
61
     */
62
    protected static $messages = [];
63
64
    /**
65
     * Generates a flash message and adds it to a message queue.
66
     *
67
     * @access public
68
     *
69
     * @param string $message: The body of the message
70
     * @param string $title: The title of the message
71
     * @param int $severity: The message's severity
72
     * @param bool $session: Should the message be saved in the user's session?
73
     * @param string $queue: The queue's unique identifier
74
     *
75
     * @return \TYPO3\CMS\Core\Messaging\FlashMessageQueue The queue the message was added to
76
     */
77
    public static function addMessage($message, $title, $severity, $session = false, $queue = 'kitodo.default.flashMessages')
78
    {
79
        $flashMessageService = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Messaging\FlashMessageService::class);
80
        $flashMessageQueue = $flashMessageService->getMessageQueueByIdentifier($queue);
81
        $flashMessage = GeneralUtility::makeInstance(
82
            \TYPO3\CMS\Core\Messaging\FlashMessage::class,
83
            $message,
84
            $title,
85
            $severity,
86
            $session
87
        );
88
        $flashMessageQueue->enqueue($flashMessage);
89
        return $flashMessageQueue;
90
    }
91
92
    /**
93
     * Check if given identifier is a valid identifier of the German National Library
94
     *
95
     * @access public
96
     *
97
     * @param string $id: The identifier to check
98
     * @param string $type: What type is the identifier supposed to be?
99
     *                      Possible values: PPN, IDN, PND, ZDB, SWD, GKD
100
     *
101
     * @return bool Is $id a valid GNL identifier of the given $type?
102
     */
103
    public static function checkIdentifier($id, $type)
104
    {
105
        $digits = substr($id, 0, 8);
106
        $checksum = 0;
107
        for ($i = 0, $j = strlen($digits); $i < $j; $i++) {
108
            $checksum += (9 - $i) * intval(substr($digits, $i, 1));
109
        }
110
        $checksum = (11 - ($checksum % 11)) % 11;
111
        switch (strtoupper($type)) {
112
            case 'PPN':
113
            case 'IDN':
114
            case 'PND':
115
                if ($checksum == 10) {
116
                    $checksum = 'X';
117
                }
118
                if (!preg_match('/[0-9]{8}[0-9X]{1}/i', $id)) {
119
                    return false;
120
                } elseif (strtoupper(substr($id, -1, 1)) != $checksum) {
121
                    return false;
122
                }
123
                break;
124
            case 'ZDB':
125
                if ($checksum == 10) {
126
                    $checksum = 'X';
127
                }
128
                if (!preg_match('/[0-9]{8}-[0-9X]{1}/i', $id)) {
129
                    return false;
130
                } elseif (strtoupper(substr($id, -1, 1)) != $checksum) {
131
                    return false;
132
                }
133
                break;
134
            case 'SWD':
135
                $checksum = 11 - $checksum;
136
                if (!preg_match('/[0-9]{8}-[0-9]{1}/i', $id)) {
137
                    return false;
138
                } elseif ($checksum == 10) {
139
                    return self::checkIdentifier(($digits + 1) . substr($id, -2, 2), 'SWD');
140
                } elseif (substr($id, -1, 1) != $checksum) {
141
                    return false;
142
                }
143
                break;
144
            case 'GKD':
145
                $checksum = 11 - $checksum;
146
                if ($checksum == 10) {
147
                    $checksum = 'X';
148
                }
149
                if (!preg_match('/[0-9]{8}-[0-9X]{1}/i', $id)) {
150
                    return false;
151
                } elseif (strtoupper(substr($id, -1, 1)) != $checksum) {
152
                    return false;
153
                }
154
                break;
155
        }
156
        return true;
157
    }
158
159
    /**
160
     * Decrypt encrypted value with given control hash
161
     *
162
     * @access public
163
     *
164
     * @param string $encrypted: The encrypted value to decrypt
165
     *
166
     * @return mixed The decrypted value or false on error
167
     */
168
    public static function decrypt($encrypted)
169
    {
170
        if (
171
            !in_array(self::$cipherAlgorithm, openssl_get_cipher_methods(true))
172
            || !in_array(self::$hashAlgorithm, openssl_get_md_methods(true))
173
        ) {
174
            self::log('OpenSSL library doesn\'t support cipher and/or hash algorithm', LOG_SEVERITY_ERROR);
175
            return false;
176
        }
177
        if (empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'])) {
178
            self::log('No encryption key set in TYPO3 configuration', LOG_SEVERITY_ERROR);
179
            return false;
180
        }
181
        if (
182
            empty($encrypted)
183
            || strlen($encrypted) < openssl_cipher_iv_length(self::$cipherAlgorithm)
184
        ) {
185
            self::log('Invalid parameters given for decryption', LOG_SEVERITY_ERROR);
186
            return false;
187
        }
188
        // Split initialisation vector and encrypted data.
189
        $binary = base64_decode($encrypted);
190
        $iv = substr($binary, 0, openssl_cipher_iv_length(self::$cipherAlgorithm));
191
        $data = substr($binary, openssl_cipher_iv_length(self::$cipherAlgorithm));
192
        $key = openssl_digest($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'], self::$hashAlgorithm, true);
193
        // Decrypt data.
194
        $decrypted = openssl_decrypt($data, self::$cipherAlgorithm, $key, OPENSSL_RAW_DATA, $iv);
195
        return $decrypted;
196
    }
197
198
    /**
199
     * Get content of XML file as string or false if there is nothing.
200
     *
201
     * @access public
202
     *
203
     * @param string $content: content of file to read
204
     *
205
     * @return mixed
206
     */
207
    public static function getXmlFileAsString($content)
208
    {
209
        // Turn off libxml's error logging.
210
        $libxmlErrors = libxml_use_internal_errors(true);
211
        // Disables the functionality to allow external entities to be loaded when parsing the XML, must be kept
212
        $previousValueOfEntityLoader = libxml_disable_entity_loader(true);
213
        // Try to load XML from file.
214
        $xml = simplexml_load_string($content);
215
        // reset entity loader setting
216
        libxml_disable_entity_loader($previousValueOfEntityLoader);
217
        // Reset libxml's error logging.
218
        libxml_use_internal_errors($libxmlErrors);
219
        return $xml;
220
    }
221
222
    /**
223
     * Add a message to the TYPO3 log
224
     *
225
     * @access public
226
     *
227
     * @param string $message: The message to log
228
     * @param int $severity: The severity of the message
229
     *                       0 is info, 1 is notice, 2 is warning, 3 is fatal error, -1 is "OK" message
230
     *
231
     * @return void
232
     */
233
    public static function log($message, $severity = 0)
234
    {
235
        $logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(get_called_class());
236
237
        switch ($severity) {
238
            case 0:
239
                $logger->info($message);
240
                break;
241
            case 1:
242
                $logger->notice($message);
243
                break;
244
            case 2:
245
                $logger->warning($message);
246
                break;
247
            case 3:
248
                $logger->error($message);
249
                break;
250
            default:
251
                break;
252
        }
253
    }
254
255
    /**
256
     * Digest the given string
257
     *
258
     * @access public
259
     *
260
     * @param string $string: The string to encrypt
261
     *
262
     * @return mixed Hashed string or false on error
263
     */
264
    public static function digest($string)
265
    {
266
        if (!in_array(self::$hashAlgorithm, openssl_get_md_methods(true))) {
267
            self::log('OpenSSL library doesn\'t support hash algorithm', LOG_SEVERITY_ERROR);
268
            return false;
269
        }
270
        // Hash string.
271
        $hashed = openssl_digest($string, self::$hashAlgorithm);
272
        return $hashed;
273
    }
274
275
    /**
276
     * Encrypt the given string
277
     *
278
     * @access public
279
     *
280
     * @param string $string: The string to encrypt
281
     *
282
     * @return mixed Encrypted string or false on error
283
     */
284
    public static function encrypt($string)
285
    {
286
        if (
287
            !in_array(self::$cipherAlgorithm, openssl_get_cipher_methods(true))
288
            || !in_array(self::$hashAlgorithm, openssl_get_md_methods(true))
289
        ) {
290
            self::log('OpenSSL library doesn\'t support cipher and/or hash algorithm', LOG_SEVERITY_ERROR);
291
            return false;
292
        }
293
        if (empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'])) {
294
            self::log('No encryption key set in TYPO3 configuration', LOG_SEVERITY_ERROR);
295
            return false;
296
        }
297
        // Generate random initialisation vector.
298
        $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length(self::$cipherAlgorithm));
299
        $key = openssl_digest($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'], self::$hashAlgorithm, true);
300
        // Encrypt data.
301
        $encrypted = openssl_encrypt($string, self::$cipherAlgorithm, $key, OPENSSL_RAW_DATA, $iv);
302
        // Merge initialisation vector and encrypted data.
303
        if ($encrypted !== false) {
304
            $encrypted = base64_encode($iv . $encrypted);
305
        }
306
        return $encrypted;
307
    }
308
309
    /**
310
     * Get the unqualified name of a class
311
     *
312
     * @access public
313
     *
314
     * @param string $qualifiedClassname: The qualified class name from get_class()
315
     *
316
     * @return string The unqualified class name
317
     */
318
    public static function getUnqualifiedClassName($qualifiedClassname)
319
    {
320
        $nameParts = explode('\\', $qualifiedClassname);
321
        return end($nameParts);
322
    }
323
324
    /**
325
     * Clean up a string to use in an URL.
326
     *
327
     * @access public
328
     *
329
     * @param string $string: The string to clean up
330
     *
331
     * @return string The cleaned up string
332
     */
333
    public static function getCleanString($string)
334
    {
335
        // Convert to lowercase.
336
        $string = strtolower($string);
337
        // Remove non-alphanumeric characters.
338
        $string = preg_replace('/[^a-z0-9_\s-]/', '', $string);
339
        // Remove multiple dashes or whitespaces.
340
        $string = preg_replace('/[\s-]+/', ' ', $string);
341
        // Convert whitespaces and underscore to dash.
342
        $string = preg_replace('/[\s_]/', '-', $string);
343
        return $string;
344
    }
345
346
    /**
347
     * Get the registered hook objects for a class
348
     *
349
     * @access public
350
     *
351
     * @param string $scriptRelPath: The path to the class file
352
     *
353
     * @return array Array of hook objects for the class
354
     */
355
    public static function getHookObjects($scriptRelPath)
356
    {
357
        $hookObjects = [];
358
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::$extKey . '/' . $scriptRelPath]['hookClass'])) {
359
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::$extKey . '/' . $scriptRelPath]['hookClass'] as $classRef) {
360
                $hookObjects[] = GeneralUtility::makeInstance($classRef);
361
            }
362
        }
363
        return $hookObjects;
364
    }
365
366
    /**
367
     * Get the "index_name" for an UID
368
     *
369
     * @access public
370
     *
371
     * @param int $uid: The UID of the record
372
     * @param string $table: Get the "index_name" from this table
373
     * @param int $pid: Get the "index_name" from this page
374
     *
375
     * @return string "index_name" for the given UID
376
     */
377
    public static function getIndexNameFromUid($uid, $table, $pid = -1)
378
    {
379
        // Sanitize input.
380
        $uid = max(intval($uid), 0);
381
        if (
382
            !$uid
383
            || !in_array($table, ['tx_dlf_collections', 'tx_dlf_libraries', 'tx_dlf_metadata', 'tx_dlf_structures', 'tx_dlf_solrcores'])
384
        ) {
385
            self::log('Invalid UID "' . $uid . '" or table "' . $table . '"', LOG_SEVERITY_ERROR);
386
            return '';
387
        }
388
389
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
390
            ->getQueryBuilderForTable($table);
391
392
        $where = '';
393
        // Should we check for a specific PID, too?
394
        if ($pid !== -1) {
395
            $pid = max(intval($pid), 0);
396
            $where = $queryBuilder->expr()->eq($table . '.pid', $pid);
397
        }
398
399
        // Get index_name from database.
400
        $result = $queryBuilder
401
            ->select($table . '.index_name AS index_name')
402
            ->from($table)
403
            ->where(
404
                $queryBuilder->expr()->eq($table . '.uid', $uid),
405
                $where,
406
                self::whereExpression($table)
407
            )
408
            ->setMaxResults(1)
409
            ->execute();
410
411
        if ($resArray = $result->fetch()) {
412
            return $resArray['index_name'];
413
        } else {
414
            self::log('No "index_name" with UID ' . $uid . ' and PID ' . $pid . ' found in table "' . $table . '"', LOG_SEVERITY_WARNING);
415
            return '';
416
        }
417
    }
418
419
    /**
420
     * Get the "label" for an UID
421
     *
422
     * @access public
423
     *
424
     * @param int $uid: The UID of the record
425
     * @param string $table: Get the "label" from this table
426
     * @param int $pid: Get the "label" from this page
427
     *
428
     * @return string "label" for the given UID
429
     */
430
    public static function getLabelFromUid($uid, $table, $pid = -1)
431
    {
432
        // Sanitize input.
433
        $uid = max(intval($uid), 0);
434
        if (
435
            !$uid
436
            || !in_array($table, ['tx_dlf_collections', 'tx_dlf_libraries', 'tx_dlf_metadata', 'tx_dlf_structures', 'tx_dlf_solrcores'])
437
        ) {
438
            self::log('Invalid UID "' . $uid . '" or table "' . $table . '"', LOG_SEVERITY_ERROR);
439
            return '';
440
        }
441
442
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
443
            ->getQueryBuilderForTable($table);
444
445
        $where = '';
446
        // Should we check for a specific PID, too?
447
        if ($pid !== -1) {
448
            $pid = max(intval($pid), 0);
449
            $where = $queryBuilder->expr()->eq($table . '.pid', $pid);
450
        }
451
452
        // Get label from database.
453
        $result = $queryBuilder
454
            ->select($table . '.label AS label')
455
            ->from($table)
456
            ->where(
457
                $queryBuilder->expr()->eq($table . '.uid', $uid),
458
                $where,
459
                self::whereExpression($table)
460
            )
461
            ->setMaxResults(1)
462
            ->execute();
463
464
        if ($resArray = $result->fetch()) {
465
            return $resArray['label'];
466
        } else {
467
            self::log('No "label" with UID ' . $uid . ' and PID ' . $pid . ' found in table "' . $table . '"', LOG_SEVERITY_WARNING);
468
            return '';
469
        }
470
    }
471
472
    /**
473
     * Get language name from ISO code
474
     *
475
     * @access public
476
     *
477
     * @param string $code: ISO 639-1 or ISO 639-2/B language code
478
     *
479
     * @return string Localized full name of language or unchanged input
480
     */
481
    public static function getLanguageName($code)
482
    {
483
        // Analyze code and set appropriate ISO table.
484
        $isoCode = strtolower(trim($code));
485
        if (preg_match('/^[a-z]{3}$/', $isoCode)) {
486
            $file = \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath(self::$extKey) . 'Resources/Private/Data/iso-639-2b.xml';
487
        } elseif (preg_match('/^[a-z]{2}$/', $isoCode)) {
488
            $file = \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath(self::$extKey) . 'Resources/Private/Data/iso-639-1.xml';
489
        } else {
490
            // No ISO code, return unchanged.
491
            return $code;
492
        }
493
        // Load ISO table and get localized full name of language.
494
        if (\TYPO3_MODE === 'FE') {
495
            $iso639 = $GLOBALS['TSFE']->readLLfile($file);
496
            if (!empty($iso639['default'][$isoCode])) {
497
                $lang = $GLOBALS['TSFE']->getLLL($isoCode, $iso639);
498
            }
499
        } elseif (\TYPO3_MODE === 'BE') {
500
            $iso639 = $GLOBALS['LANG']->includeLLFile($file, false, true);
501
            if (!empty($iso639['default'][$isoCode])) {
502
                $lang = $GLOBALS['LANG']->getLLL($isoCode, $iso639);
503
            }
504
        } else {
505
            self::log('Unexpected TYPO3_MODE "' . \TYPO3_MODE . '"', LOG_SEVERITY_ERROR);
506
            return $code;
507
        }
508
        if (!empty($lang)) {
509
            return $lang;
510
        } else {
511
            self::log('Language code "' . $code . '" not found in ISO-639 table', LOG_SEVERITY_NOTICE);
512
            return $code;
513
        }
514
    }
515
516
    /**
517
     * Wrapper function for getting localized messages in frontend and backend
518
     *
519
     * @access public
520
     *
521
     * @param string $key: The locallang key to translate
522
     * @param bool $hsc: Should the result be htmlspecialchar()'ed?
523
     * @param string $default: Default return value if no translation is available
524
     *
525
     * @return string The translated string or the given key on failure
526
     */
527
    public static function getMessage($key, $hsc = false, $default = '')
528
    {
529
        // Set initial output to default value.
530
        $translated = (string) $default;
531
        // Load common messages file.
532
        if (empty(self::$messages)) {
533
            $file = \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath(self::$extKey, 'Resources/Private/Language/FlashMessages.xml');
534
            if (\TYPO3_MODE === 'FE') {
535
                self::$messages = $GLOBALS['TSFE']->readLLfile($file);
536
            } elseif (\TYPO3_MODE === 'BE') {
537
                self::$messages = $GLOBALS['LANG']->includeLLFile($file, false, true);
538
            } else {
539
                self::log('Unexpected TYPO3_MODE "' . \TYPO3_MODE . '"', LOG_SEVERITY_ERROR);
540
            }
541
        }
542
        // Get translation.
543
        if (!empty(self::$messages['default'][$key])) {
544
            if (\TYPO3_MODE === 'FE') {
545
                $translated = $GLOBALS['TSFE']->getLLL($key, self::$messages);
546
            } elseif (\TYPO3_MODE === 'BE') {
547
                $translated = $GLOBALS['LANG']->getLLL($key, self::$messages);
548
            } else {
549
                self::log('Unexpected TYPO3_MODE "' . \TYPO3_MODE . '"', LOG_SEVERITY_ERROR);
550
            }
551
        }
552
        // Escape HTML characters if applicable.
553
        if ($hsc) {
554
            $translated = htmlspecialchars($translated);
555
        }
556
        return $translated;
557
    }
558
559
    /**
560
     * Get the UID for a given "index_name"
561
     *
562
     * @access public
563
     *
564
     * @param int $index_name: The index_name of the record
565
     * @param string $table: Get the "index_name" from this table
566
     * @param int $pid: Get the "index_name" from this page
567
     *
568
     * @return string "uid" for the given index_name
569
     */
570
    public static function getUidFromIndexName($index_name, $table, $pid = -1)
571
    {
572
        if (
573
            !$index_name
574
            || !in_array($table, ['tx_dlf_collections', 'tx_dlf_libraries', 'tx_dlf_metadata', 'tx_dlf_structures', 'tx_dlf_solrcores'])
575
        ) {
576
            self::log('Invalid UID ' . $index_name . ' or table "' . $table . '"', LOG_SEVERITY_ERROR);
577
            return '';
578
        }
579
580
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
581
            ->getQueryBuilderForTable($table);
582
583
        $where = '';
584
        // Should we check for a specific PID, too?
585
        if ($pid !== -1) {
586
            $pid = max(intval($pid), 0);
587
            $where = $queryBuilder->expr()->eq($table . '.pid', $pid);
588
        }
589
        // Get index_name from database.
590
        $result = $queryBuilder
591
            ->select($table . '.uid AS uid')
592
            ->from($table)
593
            ->where(
594
                $queryBuilder->expr()->eq($table . '.index_name', $queryBuilder->expr()->literal($index_name)),
595
                $where,
596
                self::whereExpression($table)
597
            )
598
            ->setMaxResults(1)
599
            ->execute();
600
601
        $allResults = $result->fetchAll();
602
603
        if (count($allResults) == 1) {
604
            return $allResults[0]['uid'];
605
        } else {
606
            self::log('No UID for given index_name "' . $index_name . '" and PID ' . $pid . ' found in table "' . $table . '"', LOG_SEVERITY_WARNING);
607
            return '';
608
        }
609
    }
610
611
    /**
612
     * Get the URN of an object
613
     * @see http://www.persistent-identifier.de/?link=316
614
     *
615
     * @access public
616
     *
617
     * @param string $base: The namespace and base URN
618
     * @param string $id: The object's identifier
619
     *
620
     * @return string Uniform Resource Name as string
621
     */
622
    public static function getURN($base, $id)
623
    {
624
        $concordance = [
625
            '0' => 1,
626
            '1' => 2,
627
            '2' => 3,
628
            '3' => 4,
629
            '4' => 5,
630
            '5' => 6,
631
            '6' => 7,
632
            '7' => 8,
633
            '8' => 9,
634
            '9' => 41,
635
            'a' => 18,
636
            'b' => 14,
637
            'c' => 19,
638
            'd' => 15,
639
            'e' => 16,
640
            'f' => 21,
641
            'g' => 22,
642
            'h' => 23,
643
            'i' => 24,
644
            'j' => 25,
645
            'k' => 42,
646
            'l' => 26,
647
            'm' => 27,
648
            'n' => 13,
649
            'o' => 28,
650
            'p' => 29,
651
            'q' => 31,
652
            'r' => 12,
653
            's' => 32,
654
            't' => 33,
655
            'u' => 11,
656
            'v' => 34,
657
            'w' => 35,
658
            'x' => 36,
659
            'y' => 37,
660
            'z' => 38,
661
            '-' => 39,
662
            ':' => 17,
663
        ];
664
        $urn = strtolower($base . $id);
665
        if (preg_match('/[^a-z0-9:-]/', $urn)) {
666
            self::log('Invalid chars in given parameters', LOG_SEVERITY_WARNING);
667
            return '';
668
        }
669
        $digits = '';
670
        for ($i = 0, $j = strlen($urn); $i < $j; $i++) {
671
            $digits .= $concordance[substr($urn, $i, 1)];
672
        }
673
        $checksum = 0;
674
        for ($i = 0, $j = strlen($digits); $i < $j; $i++) {
675
            $checksum += ($i + 1) * intval(substr($digits, $i, 1));
676
        }
677
        $checksum = substr(intval($checksum / intval(substr($digits, -1, 1))), -1, 1);
678
        return $base . $id . $checksum;
679
    }
680
681
    /**
682
     * Check if given ID is a valid Pica Production Number (PPN)
683
     *
684
     * @access public
685
     *
686
     * @param string $id: The identifier to check
687
     *
688
     * @return bool Is $id a valid PPN?
689
     */
690
    public static function isPPN($id)
691
    {
692
        return self::checkIdentifier($id, 'PPN');
693
    }
694
695
    /**
696
     * Determine whether or not $url is a valid URL using HTTP or HTTPS scheme.
697
     *
698
     * @param string $url
699
     *
700
     * @return bool
701
     */
702
    public static function isValidHttpUrl($url)
703
    {
704
        if (!GeneralUtility::isValidUrl($url)) {
705
            return false;
706
        }
707
708
        $parsed = parse_url($url);
709
        $scheme = $parsed['scheme'] ?? '';
710
        $schemeNormalized = strtolower($scheme);
711
712
        return $schemeNormalized === 'http' || $schemeNormalized === 'https';
713
    }
714
715
    /**
716
     * Load value from user's session.
717
     *
718
     * @access public
719
     *
720
     * @param string $key: Session data key for retrieval
721
     *
722
     * @return mixed Session value for given key or null on failure
723
     */
724
    public static function loadFromSession($key)
725
    {
726
        // Cast to string for security reasons.
727
        $key = (string) $key;
728
        if (!$key) {
729
            self::log('Invalid key "' . $key . '" for session data retrieval', LOG_SEVERITY_WARNING);
730
            return;
731
        }
732
        // Get the session data.
733
        if (\TYPO3_MODE === 'FE') {
734
            return $GLOBALS['TSFE']->fe_user->getKey('ses', $key);
735
        } elseif (\TYPO3_MODE === 'BE') {
736
            return $GLOBALS['BE_USER']->getSessionData($key);
737
        } else {
738
            self::log('Unexpected TYPO3_MODE "' . \TYPO3_MODE . '"', LOG_SEVERITY_ERROR);
739
            return;
740
        }
741
    }
742
743
    /**
744
     * Merges two arrays recursively and actually returns the modified array.
745
     * @see \TYPO3\CMS\Core\Utility\ArrayUtility::mergeRecursiveWithOverrule()
746
     *
747
     * @access public
748
     *
749
     * @param array $original: Original array
750
     * @param array $overrule: Overrule array, overruling the original array
751
     * @param bool $addKeys: If set to false, keys that are not found in $original will not be set
752
     * @param bool $includeEmptyValues: If set, values from $overrule will overrule if they are empty
753
     * @param bool $enableUnsetFeature: If set, special value "__UNSET" can be used in the overrule array to unset keys in the original array
754
     *
755
     * @return array Merged array
756
     */
757
    public static function mergeRecursiveWithOverrule(array $original, array $overrule, $addKeys = true, $includeEmptyValues = true, $enableUnsetFeature = true)
758
    {
759
        \TYPO3\CMS\Core\Utility\ArrayUtility::mergeRecursiveWithOverrule($original, $overrule, $addKeys, $includeEmptyValues, $enableUnsetFeature);
760
        return $original;
761
    }
762
763
    /**
764
     * Process a data and/or command map with TYPO3 core engine as admin.
765
     *
766
     * @access public
767
     *
768
     * @param array $data: Data map
769
     * @param array $cmd: Command map
770
     * @param bool $reverseOrder: Should the data map be reversed?
771
     * @param bool $cmdFirst: Should the command map be processed first?
772
     *
773
     * @return array Array of substituted "NEW..." identifiers and their actual UIDs.
774
     */
775
    public static function processDBasAdmin(array $data = [], array $cmd = [], $reverseOrder = false, $cmdFirst = false)
776
    {
777
        if (
778
            \TYPO3_MODE === 'BE'
779
            && $GLOBALS['BE_USER']->isAdmin()
780
        ) {
781
            // Instantiate TYPO3 core engine.
782
            $dataHandler = GeneralUtility::makeInstance(\TYPO3\CMS\Core\DataHandling\DataHandler::class);
783
            // We do not use workspaces and have to bypass restrictions in DataHandler.
784
            $dataHandler->bypassWorkspaceRestrictions = true;
785
            // Load data and command arrays.
786
            $dataHandler->start($data, $cmd);
787
            // Process command map first if default order is reversed.
788
            if (
789
                !empty($cmd)
790
                && $cmdFirst
791
            ) {
792
                $dataHandler->process_cmdmap();
793
            }
794
            // Process data map.
795
            if (!empty($data)) {
796
                $dataHandler->reverseOrder = $reverseOrder;
797
                $dataHandler->process_datamap();
798
            }
799
            // Process command map if processing order is not reversed.
800
            if (
801
                !empty($cmd)
802
                && !$cmdFirst
803
            ) {
804
                $dataHandler->process_cmdmap();
805
            }
806
            return $dataHandler->substNEWwithIDs;
807
        } else {
808
            self::log('Current backend user has no admin privileges', LOG_SEVERITY_ERROR);
809
            return [];
810
        }
811
    }
812
813
    /**
814
     * Fetches and renders all available flash messages from the queue.
815
     *
816
     * @access public
817
     *
818
     * @param string $queue: The queue's unique identifier
819
     *
820
     * @return string All flash messages in the queue rendered as HTML.
821
     */
822
    public static function renderFlashMessages($queue = 'kitodo.default.flashMessages')
823
    {
824
        $flashMessageService = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Messaging\FlashMessageService::class);
825
        $flashMessageQueue = $flashMessageService->getMessageQueueByIdentifier($queue);
826
        $flashMessages = $flashMessageQueue->getAllMessagesAndFlush();
827
        $content = GeneralUtility::makeInstance(\Kitodo\Dlf\Common\KitodoFlashMessageRenderer::class)
828
            ->render($flashMessages);
829
        return $content;
830
    }
831
832
    /**
833
     * Save given value to user's session.
834
     *
835
     * @access public
836
     *
837
     * @param mixed $value: Value to save
838
     * @param string $key: Session data key for saving
839
     *
840
     * @return bool true on success, false on failure
841
     */
842
    public static function saveToSession($value, $key)
843
    {
844
        // Cast to string for security reasons.
845
        $key = (string) $key;
846
        if (!$key) {
847
            self::log('Invalid key "' . $key . '" for session data saving', LOG_SEVERITY_WARNING);
848
            return false;
849
        }
850
        // Save value in session data.
851
        if (\TYPO3_MODE === 'FE') {
852
            $GLOBALS['TSFE']->fe_user->setKey('ses', $key, $value);
853
            $GLOBALS['TSFE']->fe_user->storeSessionData();
854
            return true;
855
        } elseif (\TYPO3_MODE === 'BE') {
856
            $GLOBALS['BE_USER']->setAndSaveSessionData($key, $value);
857
            return true;
858
        } else {
859
            self::log('Unexpected TYPO3_MODE "' . \TYPO3_MODE . '"', LOG_SEVERITY_ERROR);
860
            return false;
861
        }
862
    }
863
864
    /**
865
     * This translates an internal "index_name"
866
     *
867
     * @access public
868
     *
869
     * @param string $index_name: The internal "index_name" to translate
870
     * @param string $table: Get the translation from this table
871
     * @param string $pid: Get the translation from this page
872
     *
873
     * @return string Localized label for $index_name
874
     */
875
    public static function translate($index_name, $table, $pid)
876
    {
877
        // Load labels into static variable for future use.
878
        static $labels = [];
879
        // Sanitize input.
880
        $pid = max(intval($pid), 0);
881
        if (!$pid) {
882
            self::log('Invalid PID ' . $pid . ' for translation', LOG_SEVERITY_WARNING);
883
            return $index_name;
884
        }
885
        // Check if "index_name" is an UID.
886
        if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($index_name)) {
887
            $index_name = self::getIndexNameFromUid($index_name, $table, $pid);
888
        }
889
        /* $labels already contains the translated content element, but with the index_name of the translated content element itself
890
         * and not with the $index_name of the original that we receive here. So we have to determine the index_name of the
891
         * associated translated content element. E.g. $labels['title0'] != $index_name = title. */
892
893
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
894
            ->getQueryBuilderForTable($table);
895
896
        // First fetch the uid of the received index_name
897
        $result = $queryBuilder
898
            ->select(
899
                $table . '.uid AS uid',
900
                $table . '.l18n_parent AS l18n_parent'
901
            )
902
            ->from($table)
903
            ->where(
904
                $queryBuilder->expr()->eq($table . '.pid', $pid),
905
                $queryBuilder->expr()->eq($table . '.index_name', $queryBuilder->expr()->literal($index_name)),
906
                self::whereExpression($table, true)
907
            )
908
            ->setMaxResults(1)
909
            ->execute();
910
911
        $allResults = $result->fetchAll();
912
913
        if (count($allResults) == 1) {
914
            // Now we use the uid of the l18_parent to fetch the index_name of the translated content element.
915
            $resArray = $allResults[0];
916
917
            $result = $queryBuilder
918
                ->select($table . '.index_name AS index_name')
919
                ->from($table)
920
                ->where(
921
                    $queryBuilder->expr()->eq($table . '.pid', $pid),
922
                    $queryBuilder->expr()->eq($table . '.uid', $resArray['l18n_parent']),
923
                    $queryBuilder->expr()->eq($table . '.sys_language_uid', intval($GLOBALS['TSFE']->sys_language_content)),
924
                    self::whereExpression($table, true)
925
                )
926
                ->setMaxResults(1)
927
                ->execute();
928
929
            $allResults = $result->fetchAll();
930
931
            if (count($allResults) == 1) {
932
                // If there is an translated content element, overwrite the received $index_name.
933
                $index_name = $allResults[0]['index_name'];
934
            }
935
        }
936
937
        // Check if we already got a translation.
938
        if (empty($labels[$table][$pid][$GLOBALS['TSFE']->sys_language_content][$index_name])) {
939
            // Check if this table is allowed for translation.
940
            if (in_array($table, ['tx_dlf_collections', 'tx_dlf_libraries', 'tx_dlf_metadata', 'tx_dlf_structures'])) {
941
                $additionalWhere = $queryBuilder->expr()->in($table . '.sys_language_uid', [-1, 0]);
942
                if ($GLOBALS['TSFE']->sys_language_content > 0) {
943
                    $additionalWhere = $queryBuilder->expr()->andX(
944
                        $queryBuilder->expr()->orX(
945
                            $queryBuilder->expr()->in($table . '.sys_language_uid', [-1, 0]),
946
                            $queryBuilder->expr()->eq($table . '.sys_language_uid', intval($GLOBALS['TSFE']->sys_language_content))
947
                        ),
948
                        $queryBuilder->expr()->eq($table . '.l18n_parent', 0)
949
                    );
950
                }
951
952
                // Get labels from database.
953
                $result = $queryBuilder
954
                    ->select('*')
955
                    ->from($table)
956
                    ->where(
957
                        $queryBuilder->expr()->eq($table . '.pid', $pid),
958
                        $additionalWhere,
959
                        self::whereExpression($table, true)
960
                    )
961
                    ->setMaxResults(10000)
962
                    ->execute();
963
964
                if ($result->rowCount() > 0) {
965
                    while ($resArray = $result->fetch()) {
966
                        // Overlay localized labels if available.
967
                        if ($GLOBALS['TSFE']->sys_language_content > 0) {
968
                            $resArray = $GLOBALS['TSFE']->sys_page->getRecordOverlay($table, $resArray, $GLOBALS['TSFE']->sys_language_content, $GLOBALS['TSFE']->sys_language_contentOL);
969
                        }
970
                        if ($resArray) {
971
                            $labels[$table][$pid][$GLOBALS['TSFE']->sys_language_content][$resArray['index_name']] = $resArray['label'];
972
                        }
973
                    }
974
                } else {
975
                    self::log('No translation with PID ' . $pid . ' available in table "' . $table . '" or translation not accessible', LOG_SEVERITY_NOTICE);
976
                }
977
            } else {
978
                self::log('No translations available for table "' . $table . '"', LOG_SEVERITY_WARNING);
979
            }
980
        }
981
982
        if (!empty($labels[$table][$pid][$GLOBALS['TSFE']->sys_language_content][$index_name])) {
983
            return $labels[$table][$pid][$GLOBALS['TSFE']->sys_language_content][$index_name];
984
        } else {
985
            return $index_name;
986
        }
987
    }
988
989
    /**
990
     * This returns the additional WHERE expression of a table based on its TCA configuration
991
     *
992
     * @access public
993
     *
994
     * @param string $table: Table name as defined in TCA
995
     * @param bool $showHidden: Ignore the hidden flag?
996
     *
997
     * @return string Additional WHERE expression
998
     */
999
    public static function whereExpression($table, $showHidden = false)
1000
    {
1001
        if (\TYPO3_MODE === 'FE') {
1002
            // Should we ignore the record's hidden flag?
1003
            $ignoreHide = 0;
1004
            if ($showHidden) {
1005
                $ignoreHide = 1;
1006
            }
1007
            $expression = $GLOBALS['TSFE']->sys_page->enableFields($table, $ignoreHide);
1008
            if (!empty($expression)) {
1009
                return substr($expression, 5);
1010
            } else {
1011
                return '';
1012
            }
1013
        } elseif (\TYPO3_MODE === 'BE') {
1014
            return GeneralUtility::makeInstance(ConnectionPool::class)
1015
                ->getQueryBuilderForTable($table)
1016
                ->expr()
1017
                ->eq($table . '.' . $GLOBALS['TCA'][$table]['ctrl']['delete'], 0);
1018
        } else {
1019
            self::log('Unexpected TYPO3_MODE "' . \TYPO3_MODE . '"', LOG_SEVERITY_ERROR);
1020
            return '1=-1';
1021
        }
1022
    }
1023
1024
    /**
1025
     * Prevent instantiation by hiding the constructor
1026
     *
1027
     * @access private
1028
     */
1029
    private function __construct()
1030
    {
1031
        // This is a static class, thus no instances should be created.
1032
    }
1033
}
1034