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 ( 613ade...e3d09d )
by
unknown
03:49
created

Solr::escapeQueryKeepField()   B

Complexity

Conditions 6
Paths 3

Size

Total Lines 42
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 28
nc 3
nop 2
dl 0
loc 42
rs 8.8497
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 TYPO3\CMS\Core\Cache\CacheManager;
19
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
20
use TYPO3\CMS\Core\Database\ConnectionPool;
21
use TYPO3\CMS\Core\Utility\GeneralUtility;
22
use TYPO3\CMS\Core\Utility\MathUtility;
23
24
/**
25
 * Solr class for the 'dlf' extension
26
 *
27
 * @author Sebastian Meyer <[email protected]>
28
 * @author Henrik Lochmann <[email protected]>
29
 * @package TYPO3
30
 * @subpackage dlf
31
 * @access public
32
 * @property-read string|null $core This holds the core name for the current instance
33
 * @property-write int $cPid This holds the PID for the configuration
34
 * @property int $limit This holds the max results
35
 * @property-read int $numberOfHits This holds the number of hits for last search
36
 * @property-write array $params This holds the additional query parameters
37
 * @property-read bool $ready Is the Solr service instantiated successfully?
38
 * @property-read \Solarium\Client $service This holds the Solr service object
39
 */
40
class Solr implements LoggerAwareInterface
41
{
42
    use LoggerAwareTrait;
43
44
    /**
45
     * This holds the Solr configuration
46
     *
47
     * @var array
48
     * @access protected
49
     */
50
    protected $config = [];
51
52
    /**
53
     * This holds the core name
54
     *
55
     * @var string|null
56
     * @access protected
57
     */
58
    protected $core = null;
59
60
    /**
61
     * This holds the PID for the configuration
62
     *
63
     * @var int
64
     * @access protected
65
     */
66
    protected $cPid = 0;
67
68
    /**
69
     * The extension key
70
     *
71
     * @var string
72
     * @access public
73
     */
74
    public static $extKey = 'dlf';
75
76
    /**
77
     * The fields for SOLR index
78
     *
79
     * @var array
80
     * @access public
81
     */
82
    public static $fields = [];
83
84
    /**
85
     * This holds the max results
86
     *
87
     * @var int
88
     * @access protected
89
     */
90
    protected $limit = 50000;
91
92
    /**
93
     * This holds the number of hits for last search
94
     *
95
     * @var int
96
     * @access protected
97
     */
98
    protected $numberOfHits = 0;
99
100
    /**
101
     * This holds the additional query parameters
102
     *
103
     * @var array
104
     * @access protected
105
     */
106
    protected $params = [];
107
108
    /**
109
     * Is the search instantiated successfully?
110
     *
111
     * @var bool
112
     * @access protected
113
     */
114
    protected $ready = false;
115
116
    /**
117
     * This holds the singleton search objects with their core as array key
118
     *
119
     * @var array (\Kitodo\Dlf\Common\Solr\Solr)
120
     * @access protected
121
     */
122
    protected static $registry = [];
123
124
    /**
125
     * This holds the Solr service object
126
     *
127
     * @var \Solarium\Client
128
     * @access protected
129
     */
130
    protected $service;
131
132
    /**
133
     * Add a new core to Apache Solr
134
     *
135
     * @access public
136
     *
137
     * @param string $core: The name of the new core. If empty, the next available core name is used.
138
     *
139
     * @return string The name of the new core
140
     */
141
    public static function createCore($core = '')
142
    {
143
        // Get next available core name if none given.
144
        if (empty($core)) {
145
            $core = 'dlfCore' . self::getNextCoreNumber();
146
        }
147
        // Get Solr service instance.
148
        $solr = self::getInstance($core);
149
        // Create new core if core with given name doesn't exist.
150
        if ($solr->ready) {
151
            // Core already exists.
152
            return $core;
153
        } else {
154
            // Core doesn't exist yet.
155
            $solrAdmin = self::getInstance();
156
            if ($solrAdmin->ready) {
157
                $query = $solrAdmin->service->createCoreAdmin();
158
                $action = $query->createCreate();
159
                $action->setConfigSet('dlf');
160
                $action->setCore($core);
161
                $action->setDataDir('data');
162
                $action->setInstanceDir($core);
163
                $query->setAction($action);
164
                try {
165
                    $response = $solrAdmin->service->coreAdmin($query);
166
                    if ($response->getWasSuccessful()) {
167
                        // Core successfully created.
168
                        return $core;
169
                    }
170
                } catch (\Exception $e) {
171
                    // Nothing to do here.
172
                }
173
            } else {
174
                Helper::log('Apache Solr not available', LOG_SEVERITY_ERROR);
175
            }
176
        }
177
        return '';
178
    }
179
180
    /**
181
     * Escape special characters in a query string
182
     *
183
     * @access public
184
     *
185
     * @param string $query: The query string
186
     *
187
     * @return string The escaped query string
188
     */
189
    public static function escapeQuery($query)
190
    {
191
        // Escape query by disallowing range and field operators
192
        // Permit operators: wildcard, boolean, fuzzy, proximity, boost, grouping
193
        // https://solr.apache.org/guide/solr/latest/query-guide/standard-query-parser.html
194
        return preg_replace('/(\{|}|\[|]|:|\/|\\\)/', '\\\$1', $query);
195
    }
196
197
    /**
198
     * Escape all special characters in a query string while retaining valid field queries
199
     *
200
     * @access public
201
     *
202
     * @param string $query: The query string
203
     * @param int $pid: The PID for the field configuration
204
     *
205
     * @return string The escaped query string
206
     */
207
    public static function escapeQueryKeepField($query, $pid)
208
    {
209
        // Is there a field query?
210
        if (preg_match('/^[[:alnum:]]+_[tu][su]i:\(?.*\)?$/', $query)) {
211
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
212
                ->getQueryBuilderForTable('tx_dlf_metadata');
213
214
            // Get all indexed fields.
215
            $fields = [];
216
            $result = $queryBuilder
217
                ->select(
218
                    'tx_dlf_metadata.index_name AS index_name',
219
                    'tx_dlf_metadata.index_tokenized AS index_tokenized',
220
                    'tx_dlf_metadata.index_stored AS index_stored'
221
                )
222
                ->from('tx_dlf_metadata')
223
                ->where(
224
                    $queryBuilder->expr()->eq('tx_dlf_metadata.index_indexed', 1),
225
                    $queryBuilder->expr()->eq('tx_dlf_metadata.pid', intval($pid)),
226
                    $queryBuilder->expr()->orX(
227
                        $queryBuilder->expr()->in('tx_dlf_metadata.sys_language_uid', [-1, 0]),
228
                        $queryBuilder->expr()->eq('tx_dlf_metadata.l18n_parent', 0)
229
                    ),
230
                    Helper::whereExpression('tx_dlf_metadata')
231
                )
232
                ->execute();
233
234
            while ($resArray = $result->fetchAssociative()) {
235
                $fields[] = $resArray['index_name'] . '_' . ($resArray['index_tokenized'] ? 't' : 'u') . ($resArray['index_stored'] ? 's' : 'u') . 'i';
236
            }
237
238
            // Check if queried field is valid.
239
            $splitQuery = explode(':', $query, 2);
240
            if (in_array($splitQuery[0], $fields)) {
241
                $query = $splitQuery[0] . ':(' . self::escapeQuery(trim($splitQuery[1], '()')) . ')';
242
            } else {
243
                $query = self::escapeQuery($query);
244
            }
245
        } else {
246
            $query = self::escapeQuery($query);
247
        }
248
        return $query;
249
    }
250
251
    /**
252
     * Get fields for index.
253
     *
254
     * @access public
255
     *
256
     * @return array fields
257
     */
258
    public static function getFields()
259
    {
260
        if (empty(self::$fields)) {
261
            $conf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get(self::$extKey);
262
263
            self::$fields['id'] = $conf['solrFieldId'];
264
            self::$fields['uid'] = $conf['solrFieldUid'];
265
            self::$fields['pid'] = $conf['solrFieldPid'];
266
            self::$fields['page'] = $conf['solrFieldPage'];
267
            self::$fields['partof'] = $conf['solrFieldPartof'];
268
            self::$fields['root'] = $conf['solrFieldRoot'];
269
            self::$fields['sid'] = $conf['solrFieldSid'];
270
            self::$fields['toplevel'] = $conf['solrFieldToplevel'];
271
            self::$fields['type'] = $conf['solrFieldType'];
272
            self::$fields['title'] = $conf['solrFieldTitle'];
273
            self::$fields['volume'] = $conf['solrFieldVolume'];
274
            self::$fields['date'] = $conf['solrFieldDate'];
275
            self::$fields['thumbnail'] = $conf['solrFieldThumbnail'];
276
            self::$fields['default'] = $conf['solrFieldDefault'];
277
            self::$fields['timestamp'] = $conf['solrFieldTimestamp'];
278
            self::$fields['autocomplete'] = $conf['solrFieldAutocomplete'];
279
            self::$fields['fulltext'] = $conf['solrFieldFulltext'];
280
            self::$fields['record_id'] = $conf['solrFieldRecordId'];
281
            self::$fields['purl'] = $conf['solrFieldPurl'];
282
            self::$fields['urn'] = $conf['solrFieldUrn'];
283
            self::$fields['location'] = $conf['solrFieldLocation'];
284
            self::$fields['collection'] = $conf['solrFieldCollection'];
285
            self::$fields['license'] = $conf['solrFieldLicense'];
286
            self::$fields['terms'] = $conf['solrFieldTerms'];
287
            self::$fields['restrictions'] = $conf['solrFieldRestrictions'];
288
            self::$fields['geom'] = $conf['solrFieldGeom'];
289
        }
290
291
        return self::$fields;
292
    }
293
294
    /**
295
     * This is a singleton class, thus instances must be created by this method
296
     *
297
     * @access public
298
     *
299
     * @param mixed $core: Name or UID of the core to load or null to get core admin endpoint
300
     *
301
     * @return \Kitodo\Dlf\Common\Solr\Solr Instance of this class
302
     */
303
    public static function getInstance($core = null)
304
    {
305
        // Get core name if UID is given.
306
        if (MathUtility::canBeInterpretedAsInteger($core)) {
307
            $core = Helper::getIndexNameFromUid($core, 'tx_dlf_solrcores');
308
        }
309
        // Check if core is set or null.
310
        if (
311
            empty($core)
312
            && $core !== null
313
        ) {
314
            Helper::log('Invalid core UID or name given for Apache Solr', LOG_SEVERITY_ERROR);
315
        }
316
        if (!empty($core)) {
317
            // Check if there is an instance in the registry already.
318
            if (
319
                is_object(self::$registry[$core])
320
                && self::$registry[$core] instanceof self
321
            ) {
322
                // Return singleton instance if available.
323
                return self::$registry[$core];
324
            }
325
        }
326
        // Create new instance...
327
        $instance = new self($core);
328
        // ...and save it to registry.
329
        if (!empty($instance->core)) {
330
            self::$registry[$instance->core] = $instance;
331
        }
332
        return $instance;
333
    }
334
335
    /**
336
     * Get next unused Solr core number
337
     *
338
     * @access public
339
     *
340
     * @param int $number: Number to start with
341
     *
342
     * @return int First unused core number found
343
     */
344
    public static function getNextCoreNumber($number = 0)
345
    {
346
        $number = max(intval($number), 0);
347
        // Check if core already exists.
348
        $solr = self::getInstance('dlfCore' . $number);
349
        if (!$solr->ready) {
350
            return $number;
351
        } else {
352
            return self::getNextCoreNumber($number + 1);
353
        }
354
    }
355
356
    /**
357
     * Sets the connection information for Solr
358
     *
359
     * @access protected
360
     *
361
     * @return void
362
     */
363
    protected function loadSolrConnectionInfo()
364
    {
365
        if (empty($this->config)) {
366
            $config = [];
367
            // Extract extension configuration.
368
            $conf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get(self::$extKey);
369
            // Derive Solr scheme
370
            $config['scheme'] = empty($conf['solrHttps']) ? 'http' : 'https';
371
            // Derive Solr host name.
372
            $config['host'] = ($conf['solrHost'] ? $conf['solrHost'] : '127.0.0.1');
373
            // Set username and password.
374
            $config['username'] = $conf['solrUser'];
375
            $config['password'] = $conf['solrPass'];
376
            // Set port if not set.
377
            $config['port'] = MathUtility::forceIntegerInRange($conf['solrPort'], 1, 65535, 8983);
378
            // Trim path of slashes and (re-)add trailing slash if path not empty.
379
            $config['path'] = trim($conf['solrPath'], '/');
380
            if (!empty($config['path'])) {
381
                $config['path'] .= '/';
382
            }
383
            // Add "/solr" API endpoint when using Solarium <5.x
384
                // Todo: Remove when dropping support for Solarium 4.x
385
            if (!\Solarium\Client::checkMinimal('5.0.0')) {
386
                $config['path'] .= 'solr/';
387
            }
388
            // Set connection timeout lower than PHP's max_execution_time.
389
            $max_execution_time = intval(ini_get('max_execution_time')) ? : 30;
390
            $config['timeout'] = MathUtility::forceIntegerInRange($conf['solrTimeout'], 1, $max_execution_time, 10);
391
            $this->config = $config;
392
        }
393
    }
394
395
    /**
396
     * Processes a search request and returns the raw Apache Solr Documents.
397
     *
398
     * @access public
399
     *
400
     * @param array $parameters: Additional search parameters
401
     *
402
     * @return array The Apache Solr Documents that were fetched
403
     */
404
    public function search_raw($parameters = [])
405
    {
406
        // Set additional query parameters.
407
        $parameters['start'] = 0;
408
        $parameters['rows'] = $this->limit;
409
        // Calculate cache identifier.
410
        $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

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