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.
Passed
Push — master ( 1f4a32...bc6252 )
by
unknown
03:38
created

Solr::searchRaw()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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