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.

Issues (188)

Classes/Common/Solr/Solr.php (7 issues)

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\Solr;
14
15
use Kitodo\Dlf\Common\Helper;
16
use Psr\Log\LoggerAwareInterface;
17
use Psr\Log\LoggerAwareTrait;
18
use Solarium\Client;
19
use Solarium\Core\Client\Adapter\Http;
20
use Solarium\QueryType\Server\CoreAdmin\Result\StatusResult;
21
use TYPO3\CMS\Core\Cache\CacheManager;
22
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
23
use TYPO3\CMS\Core\Database\ConnectionPool;
24
use TYPO3\CMS\Core\EventDispatcher\EventDispatcher;
25
use TYPO3\CMS\Core\Utility\GeneralUtility;
26
use TYPO3\CMS\Core\Utility\MathUtility;
27
28
/**
29
 * Solr class for the 'dlf' extension
30
 *
31
 * @package TYPO3
32
 * @subpackage dlf
33
 *
34
 * @access public
35
 *
36
 * @property array $config this holds the Solr configuration
37
 * @property-read string|null $core this holds the core name for the current instance
38
 * @property-write int $configPid this holds the PID for the configuration
39
 * @property int $limit this holds the max results
40
 * @property-read int $numberOfHits this holds the number of hits for last search
41
 * @property-write array $params this holds the additional query parameters
42
 * @property-read bool $ready flag if the Solr service is instantiated successfully
43
 * @property-read Client $service this holds the Solr service object
44
 */
45
class Solr implements LoggerAwareInterface
46
{
47
    use LoggerAwareTrait;
48
49
    /**
50
     * @access protected
51
     * @var array This holds the Solr configuration
52
     */
53
    protected array $config = [];
54
55
    /**
56
     * @access protected
57
     * @var string|null This holds the core name
58
     */
59
    protected ?string $core = null;
60
61
    /**
62
     * @access protected
63
     * @var int This holds the PID for the configuration
64
     */
65
    protected int $configPid = 0;
66
67
    /**
68
     * @access public
69
     * @static
70
     * @var string The extension key
71
     */
72
    public static string $extKey = 'dlf';
73
74
    /**
75
     * @access public
76
     * @static
77
     * @var array The fields for SOLR index
78
     */
79
    public static array $fields = [];
80
81
    /**
82
     * @access protected
83
     * @var int This holds the max results
84
     */
85
    protected int $limit = 50000;
86
87
    /**
88
     * @access protected
89
     * @var int This holds the number of hits for last search
90
     */
91
    protected int $numberOfHits = 0;
92
93
    /**
94
     * @access protected
95
     * @var array This holds the additional query parameters
96
     */
97
    protected array $params = [];
98
99
    /**
100
     * @access protected
101
     * @var bool Is the search instantiated successfully?
102
     */
103
    protected bool $ready = false;
104
105
    /**
106
     * @access protected
107
     * @var array(Solr) This holds the singleton search objects with their core as array key
108
     */
109
    protected static array $registry = [];
110
111
    /**
112
     * @access protected
113
     * @var Client This holds the Solr service object
114
     */
115
    protected Client $service;
116
117
    /**
118
     * Add a new core to Apache Solr
119
     *
120
     * @access public
121
     *
122
     * @param string $core The name of the new core. If empty, the next available core name is used.
123
     *
124
     * @return string The name of the new core
125
     */
126
    public static function createCore($core = ''): string
127
    {
128
        // Get next available core name if none given.
129
        if (empty($core)) {
130
            $core = 'dlfCore' . self::getNextCoreNumber();
131
        }
132
        // Get Solr service instance.
133
        $solr = self::getInstance($core);
134
        // Create new core if core with given name doesn't exist.
135
        if ($solr->ready) {
136
            // Core already exists.
137
            return $core;
138
        } else {
139
            // Core doesn't exist yet.
140
            $solrAdmin = self::getInstance();
141
            if ($solrAdmin->ready) {
142
                $query = $solrAdmin->service->createCoreAdmin();
143
                $action = $query->createCreate();
144
                $action->setConfigSet('dlf');
145
                $action->setCore($core);
146
                $action->setDataDir('data');
147
                $action->setInstanceDir($core);
148
                $query->setAction($action);
149
                try {
150
                    $response = $solrAdmin->service->coreAdmin($query);
151
                    if ($response->getWasSuccessful()) {
152
                        // Core successfully created.
153
                        return $core;
154
                    }
155
                } catch (\Exception $e) {
156
                    // Nothing to do here.
157
                }
158
            } else {
159
                Helper::log('Apache Solr not available', LOG_SEVERITY_ERROR);
160
            }
161
        }
162
        return '';
163
    }
164
165
    /**
166
     * Escape special characters in a query string
167
     *
168
     * @access public
169
     *
170
     * @param string $query The query string
171
     *
172
     * @return string The escaped query string
173
     */
174
    public static function escapeQuery(string $query): string
175
    {
176
        // Escape query by disallowing range and field operators
177
        // Permit operators: wildcard, boolean, fuzzy, proximity, boost, grouping
178
        // https://solr.apache.org/guide/solr/latest/query-guide/standard-query-parser.html
179
        return preg_replace('/(\{|}|\[|]|:|\/|\\\)/', '\\\$1', $query);
180
    }
181
182
    /**
183
     * Escape all special characters in a query string while retaining valid field queries
184
     *
185
     * @access public
186
     *
187
     * @param string $query The query string
188
     * @param int $pid The PID for the field configuration
189
     *
190
     * @return string The escaped query string
191
     */
192
    public static function escapeQueryKeepField(string $query, int $pid): string
193
    {
194
        // Is there a field query?
195
        if (preg_match('/^[[:alnum:]]+_[tu][su]i:\(?.*\)?$/', $query)) {
196
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
197
                ->getQueryBuilderForTable('tx_dlf_metadata');
198
199
            // Get all indexed fields.
200
            $fields = [];
201
            $result = $queryBuilder
202
                ->select(
203
                    'tx_dlf_metadata.index_name AS index_name',
204
                    'tx_dlf_metadata.index_tokenized AS index_tokenized',
205
                    'tx_dlf_metadata.index_stored AS index_stored'
206
                )
207
                ->from('tx_dlf_metadata')
208
                ->where(
209
                    $queryBuilder->expr()->eq('tx_dlf_metadata.index_indexed', 1),
210
                    $queryBuilder->expr()->eq('tx_dlf_metadata.pid', (int) $pid),
211
                    $queryBuilder->expr()->orX(
212
                        $queryBuilder->expr()->in('tx_dlf_metadata.sys_language_uid', [-1, 0]),
213
                        $queryBuilder->expr()->eq('tx_dlf_metadata.l18n_parent', 0)
214
                    ),
215
                    Helper::whereExpression('tx_dlf_metadata')
216
                )
217
                ->execute();
218
219
            while ($resArray = $result->fetchAssociative()) {
220
                $fields[] = $resArray['index_name'] . '_' . ($resArray['index_tokenized'] ? 't' : 'u') . ($resArray['index_stored'] ? 's' : 'u') . 'i';
221
            }
222
223
            // Check if queried field is valid.
224
            $splitQuery = explode(':', $query, 2);
225
            if (in_array($splitQuery[0], $fields)) {
226
                $query = $splitQuery[0] . ':(' . self::escapeQuery(trim($splitQuery[1], '()')) . ')';
227
            } else {
228
                $query = self::escapeQuery($query);
229
            }
230
        } else {
231
            $query = self::escapeQuery($query);
232
        }
233
        return $query;
234
    }
235
236
    /**
237
     * Get fields for index.
238
     *
239
     * @access public
240
     *
241
     * @return array fields
242
     */
243
    public static function getFields(): array
244
    {
245
        if (empty(self::$fields)) {
246
            $conf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get(self::$extKey);
247
248
            self::$fields['id'] = $conf['solrFieldId'];
249
            self::$fields['uid'] = $conf['solrFieldUid'];
250
            self::$fields['pid'] = $conf['solrFieldPid'];
251
            self::$fields['page'] = $conf['solrFieldPage'];
252
            self::$fields['partof'] = $conf['solrFieldPartof'];
253
            self::$fields['root'] = $conf['solrFieldRoot'];
254
            self::$fields['sid'] = $conf['solrFieldSid'];
255
            self::$fields['toplevel'] = $conf['solrFieldToplevel'];
256
            self::$fields['type'] = $conf['solrFieldType'];
257
            self::$fields['title'] = $conf['solrFieldTitle'];
258
            self::$fields['volume'] = $conf['solrFieldVolume'];
259
            self::$fields['date'] = $conf['solrFieldDate'];
260
            self::$fields['thumbnail'] = $conf['solrFieldThumbnail'];
261
            self::$fields['default'] = $conf['solrFieldDefault'];
262
            self::$fields['timestamp'] = $conf['solrFieldTimestamp'];
263
            self::$fields['autocomplete'] = $conf['solrFieldAutocomplete'];
264
            self::$fields['fulltext'] = $conf['solrFieldFulltext'];
265
            self::$fields['record_id'] = $conf['solrFieldRecordId'];
266
            self::$fields['purl'] = $conf['solrFieldPurl'];
267
            self::$fields['urn'] = $conf['solrFieldUrn'];
268
            self::$fields['location'] = $conf['solrFieldLocation'];
269
            self::$fields['collection'] = $conf['solrFieldCollection'];
270
            self::$fields['license'] = $conf['solrFieldLicense'];
271
            self::$fields['terms'] = $conf['solrFieldTerms'];
272
            self::$fields['restrictions'] = $conf['solrFieldRestrictions'];
273
            self::$fields['geom'] = $conf['solrFieldGeom'];
274
        }
275
276
        return self::$fields;
277
    }
278
279
    /**
280
     * This is a singleton class, thus instances must be created by this method
281
     *
282
     * @access public
283
     *
284
     * @param mixed $core Name or UID of the core to load or null to get core admin endpoint
285
     *
286
     * @return Solr Instance of this class
287
     */
288
    public static function getInstance($core = null): Solr
289
    {
290
        // Get core name if UID is given.
291
        if (MathUtility::canBeInterpretedAsInteger($core)) {
292
            $core = Helper::getIndexNameFromUid($core, 'tx_dlf_solrcores');
0 ignored issues
show
It seems like $core can also be of type null; however, parameter $uid of Kitodo\Dlf\Common\Helper::getIndexNameFromUid() does only seem to accept integer, 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

292
            $core = Helper::getIndexNameFromUid(/** @scrutinizer ignore-type */ $core, 'tx_dlf_solrcores');
Loading history...
293
        }
294
        // Check if core is set or null.
295
        if (
296
            empty($core)
297
            && $core !== null
0 ignored issues
show
The condition $core !== null is always false.
Loading history...
298
        ) {
299
            Helper::log('Invalid core UID or name given for Apache Solr', LOG_SEVERITY_ERROR);
300
        }
301
        if (!empty($core)) {
302
            // Check if there is an instance in the registry already.
303
            if (
304
                is_object(self::$registry[$core])
305
                && self::$registry[$core] instanceof self
306
            ) {
307
                // Return singleton instance if available.
308
                return self::$registry[$core];
309
            }
310
        }
311
        // Create new instance...
312
        $instance = new self($core);
313
        // ...and save it to registry.
314
        if (!empty($instance->core)) {
315
            self::$registry[$instance->core] = $instance;
316
        }
317
        return $instance;
318
    }
319
320
    /**
321
     * Get next unused Solr core number
322
     *
323
     * @access public
324
     *
325
     * @param int $number Number to start with
326
     *
327
     * @return int First unused core number found
328
     */
329
    public static function getNextCoreNumber(int $number = 0): int
330
    {
331
        $number = max($number, 0);
332
        // Check if core already exists.
333
        $solr = self::getInstance('dlfCore' . $number);
334
        if (!$solr->ready) {
335
            return $number;
336
        } else {
337
            return self::getNextCoreNumber($number + 1);
338
        }
339
    }
340
341
    /**
342
     * Sets the connection information for Solr
343
     *
344
     * @access protected
345
     *
346
     * @return void
347
     */
348
    protected function loadSolrConnectionInfo(): void
349
    {
350
        if (empty($this->config)) {
351
            $config = [];
352
            // Extract extension configuration.
353
            $conf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get(self::$extKey);
354
            // Derive Solr scheme
355
            $config['scheme'] = empty($conf['solrHttps']) ? 'http' : 'https';
356
            // Derive Solr host name.
357
            $config['host'] = ($conf['solrHost'] ? $conf['solrHost'] : '127.0.0.1');
358
            // Set username and password.
359
            $config['username'] = $conf['solrUser'];
360
            $config['password'] = $conf['solrPass'];
361
            // Set port if not set.
362
            $config['port'] = MathUtility::forceIntegerInRange($conf['solrPort'], 1, 65535, 8983);
363
            // Trim path of slashes and (re-)add trailing slash if path not empty.
364
            $config['path'] = trim($conf['solrPath'], '/');
365
            if (!empty($config['path'])) {
366
                $config['path'] .= '/';
367
            }
368
369
            // Set connection timeout lower than PHP's max_execution_time.
370
            $maxExecutionTime = (int) ini_get('max_execution_time') ? : 30;
371
            $config['timeout'] = MathUtility::forceIntegerInRange($conf['solrTimeout'], 1, $maxExecutionTime, 10);
372
            $this->config = $config;
373
        }
374
    }
375
376
    /**
377
     * Processes a search request and returns the raw Apache Solr Documents.
378
     *
379
     * @access public
380
     *
381
     * @param array $parameters Additional search parameters
382
     *
383
     * @return array The Apache Solr Documents that were fetched
384
     */
385
    public function searchRaw(array $parameters = []): array
386
    {
387
        // Set additional query parameters.
388
        $parameters['start'] = 0;
389
        $parameters['rows'] = $this->limit;
390
        // Calculate cache identifier.
391
        $cacheIdentifier = Helper::digest($this->core . print_r(array_merge($this->params, $parameters), true));
0 ignored issues
show
The property params is declared write-only in Kitodo\Dlf\Common\Solr\Solr.
Loading history...
Are you sure print_r(array_merge($thi...ms, $parameters), true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

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

391
        $cacheIdentifier = Helper::digest($this->core . /** @scrutinizer ignore-type */ print_r(array_merge($this->params, $parameters), true));
Loading history...
392
        $cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('tx_dlf_solr');
393
        $resultSet = [];
394
        $entry = $cache->get($cacheIdentifier);
395
        if ($entry === false) {
396
            $selectQuery = $this->service->createSelect(array_merge($this->params, $parameters));
397
            $result = $this->service->select($selectQuery);
398
            foreach ($result as $doc) {
399
                $resultSet[] = $doc;
400
            }
401
            // Save value in cache.
402
            $cache->set($cacheIdentifier, $resultSet);
403
        } else {
404
            // Return cache hit.
405
            $resultSet = $entry;
406
        }
407
        return $resultSet;
408
    }
409
410
    /**
411
     * This returns $this->core via __get()
412
     *
413
     * @access protected
414
     *
415
     * @return string|null The core name of the current query endpoint or null if core admin endpoint
416
     */
417
    protected function magicGetCore(): ?string
418
    {
419
        return $this->core;
420
    }
421
422
    /**
423
     * This returns $this->limit via __get()
424
     *
425
     * @access protected
426
     *
427
     * @return int The max number of results
428
     */
429
    protected function magicGetLimit(): int
430
    {
431
        return $this->limit;
432
    }
433
434
    /**
435
     * This returns $this->numberOfHits via __get()
436
     *
437
     * @access protected
438
     *
439
     * @return int Total number of hits for last search
440
     */
441
    protected function magicGetNumberOfHits(): int
442
    {
443
        return $this->numberOfHits;
444
    }
445
446
    /**
447
     * This returns $this->ready via __get()
448
     *
449
     * @access protected
450
     *
451
     * @return bool Is the search instantiated successfully?
452
     */
453
    protected function magicGetReady(): bool
454
    {
455
        return $this->ready;
456
    }
457
458
    /**
459
     * This returns $this->service via __get()
460
     *
461
     * @access protected
462
     *
463
     * @return Client Apache Solr service object
464
     */
465
    protected function magicGetService(): Client
466
    {
467
        return $this->service;
468
    }
469
470
    /**
471
     * This sets $this->configPid via __set()
472
     *
473
     * @access protected
474
     *
475
     * @param int $value The new PID for the metadata definitions
476
     *
477
     * @return void
478
     */
479
    protected function magicSetConfigPid(int $value): void
480
    {
481
        $this->configPid = max($value, 0);
482
    }
483
484
    /**
485
     * This sets $this->limit via __set()
486
     *
487
     * @access protected
488
     *
489
     * @param int $value The max number of results
490
     *
491
     * @return void
492
     */
493
    protected function magicSetLimit(int $value): void
494
    {
495
        $this->limit = max($value, 0);
496
    }
497
498
    /**
499
     * This sets $this->params via __set()
500
     *
501
     * @access protected
502
     *
503
     * @param array $value The query parameters
504
     *
505
     * @return void
506
     */
507
    protected function magicSetParams(array $value): void
508
    {
509
        $this->params = $value;
510
    }
511
512
    /**
513
     * This magic method is called each time an invisible property is referenced from the object
514
     *
515
     * @access public
516
     *
517
     * @param string $var Name of variable to get
518
     *
519
     * @return mixed Value of $this->$var
520
     */
521
    public function __get(string $var)
522
    {
523
        $method = 'magicGet' . ucfirst($var);
524
        if (
525
            !property_exists($this, $var)
526
            || !method_exists($this, $method)
527
        ) {
528
            $this->logger->warning('There is no getter function for property "' . $var . '"');
529
            return null;
530
        } else {
531
            return $this->$method();
532
        }
533
    }
534
535
    /**
536
     * This magic method is called each time an invisible property is checked for isset() or empty()
537
     *
538
     * @access public
539
     *
540
     * @param string $var Name of variable to check
541
     *
542
     * @return bool true if variable is set and not empty, false otherwise
543
     */
544
    public function __isset(string $var): bool
545
    {
546
        return !empty($this->__get($var));
547
    }
548
549
    /**
550
     * This magic method is called each time an invisible property is referenced from the object
551
     *
552
     * @access public
553
     *
554
     * @param string $var Name of variable to set
555
     * @param mixed $value New value of variable
556
     *
557
     * @return void
558
     */
559
    public function __set(string $var, $value): void
560
    {
561
        $method = 'magicSet' . ucfirst($var);
562
        if (
563
            !property_exists($this, $var)
564
            || !method_exists($this, $method)
565
        ) {
566
            $this->logger->warning('There is no setter function for property "' . $var . '"');
567
        } else {
568
            $this->$method($value);
569
        }
570
    }
571
572
    /**
573
     * This is a singleton class, thus the constructor should be private/protected
574
     *
575
     * @access protected
576
     *
577
     * @param string|null $core The name of the core to use or null for core admin endpoint
578
     *
579
     * @return void
580
     */
581
    protected function __construct(?string $core)
582
    {
583
        // Solarium requires different code for version 5 and 6.
584
        $isSolarium5 = Client::checkExact('5');
585
        // Get Solr connection parameters from configuration.
586
        $this->loadSolrConnectionInfo();
587
        // Configure connection adapter.
588
        $adapter = GeneralUtility::makeInstance(Http::class);
589
        $adapter->setTimeout($this->config['timeout']);
590
        // Configure event dispatcher.
591
        if ($isSolarium5) {
592
            $eventDispatcher = null;
593
        } else {
594
            // When updating to TYPO3 >=10.x and Solarium >=6.x
595
            // we have to provide an PSR-14 Event Dispatcher instead of
596
            // "null".
597
            $eventDispatcher = GeneralUtility::makeInstance(EventDispatcher::class);
598
        }
599
        // Configure endpoint.
600
        $config = [
601
            'endpoint' => [
602
                'default' => [
603
                    'scheme' => $this->config['scheme'],
604
                    'host' => $this->config['host'],
605
                    'port' => $this->config['port'],
606
                    'path' => '/' . $this->config['path'],
607
                    'core' => $core,
608
                    'username' => $this->config['username'],
609
                    'password' => $this->config['password']
610
                ]
611
            ]
612
        ];
613
        // Instantiate Solarium\Client class.
614
        if ($isSolarium5) {
615
            $this->service = GeneralUtility::makeInstance(Client::class, $config);
0 ignored issues
show
The property service is declared read-only in Kitodo\Dlf\Common\Solr\Solr.
Loading history...
616
        } else {
617
            // When updating to TYPO3 >=10.x and Solarium >=6.x
618
            // $adapter and $eventDispatcher are mandatory arguments
619
            // of the \Solarium\Client constructor.
620
            $this->service = GeneralUtility::makeInstance(Client::class, $adapter, $eventDispatcher, $config);
621
        }
622
        $this->service->setAdapter($adapter);
623
        // Check if connection is established.
624
        $query = $this->service->createCoreAdmin();
625
        $action = $query->createStatus();
626
        if ($core !== null) {
627
            $action->setCore($core);
628
        }
629
        $query->setAction($action);
630
        try {
631
            $response = $this->service->coreAdmin($query);
632
            if ($response->getWasSuccessful()) {
633
                // Solr is reachable, but is the core as well?
634
                if ($core !== null) {
635
                    $result = $response->getStatusResult();
636
                    if (
637
                        $result instanceof StatusResult
638
                        && $result->getUptime() > 0
639
                    ) {
640
                        // Set core name.
641
                        $this->core = $core;
0 ignored issues
show
The property core is declared read-only in Kitodo\Dlf\Common\Solr\Solr.
Loading history...
642
                    } else {
643
                        // Core not available.
644
                        return;
645
                    }
646
                }
647
                // Instantiation successful!
648
                $this->ready = true;
0 ignored issues
show
The property ready is declared read-only in Kitodo\Dlf\Common\Solr\Solr.
Loading history...
649
            }
650
        } catch (\Exception $e) {
651
            // Nothing to do here.
652
        }
653
    }
654
}
655