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.
Completed
Push — master ( 90c912...99a750 )
by Sebastian
14s queued 11s
created

Solr::createCore()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 37
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 23
c 1
b 0
f 0
dl 0
loc 37
rs 8.9297
cc 6
nc 12
nop 1
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\Cache\CacheManager;
16
use TYPO3\CMS\Core\Database\ConnectionPool;
17
use TYPO3\CMS\Core\Utility\GeneralUtility;
18
use TYPO3\CMS\Core\Utility\MathUtility;
19
20
/**
21
 * Solr class for the 'dlf' extension
22
 *
23
 * @author Sebastian Meyer <[email protected]>
24
 * @author Henrik Lochmann <[email protected]>
25
 * @package TYPO3
26
 * @subpackage dlf
27
 * @access public
28
 * @property-read string|null $core This holds the core name for the current instance
29
 * @property-write int $cPid This holds the PID for the configuration
30
 * @property int $limit This holds the max results
31
 * @property-read int $numberOfHits This holds the number of hits for last search
32
 * @property-write array $params This holds the additional query parameters
33
 * @property-read bool $ready Is the Solr service instantiated successfully?
34
 * @property-read \Solarium\Client $service This holds the Solr service object
35
 */
36
class Solr
37
{
38
    /**
39
     * This holds the Solr configuration
40
     *
41
     * @var array
42
     * @access protected
43
     */
44
    protected $config = [];
45
46
    /**
47
     * This holds the core name
48
     *
49
     * @var string|null
50
     * @access protected
51
     */
52
    protected $core = null;
53
54
    /**
55
     * This holds the PID for the configuration
56
     *
57
     * @var int
58
     * @access protected
59
     */
60
    protected $cPid = 0;
61
62
    /**
63
     * The extension key
64
     *
65
     * @var string
66
     * @access public
67
     */
68
    public static $extKey = 'dlf';
69
70
    /**
71
     * This holds the max results
72
     *
73
     * @var int
74
     * @access protected
75
     */
76
    protected $limit = 50000;
77
78
    /**
79
     * This holds the number of hits for last search
80
     *
81
     * @var int
82
     * @access protected
83
     */
84
    protected $numberOfHits = 0;
85
86
    /**
87
     * This holds the additional query parameters
88
     *
89
     * @var array
90
     * @access protected
91
     */
92
    protected $params = [];
93
94
    /**
95
     * Is the search instantiated successfully?
96
     *
97
     * @var bool
98
     * @access protected
99
     */
100
    protected $ready = false;
101
102
    /**
103
     * This holds the singleton search objects with their core as array key
104
     *
105
     * @var array (\Kitodo\Dlf\Common\Solr)
106
     * @access protected
107
     */
108
    protected static $registry = [];
109
110
    /**
111
     * This holds the Solr service object
112
     *
113
     * @var \Solarium\Client
114
     * @access protected
115
     */
116
    protected $service;
117
118
    /**
119
     * Add a new core to Apache Solr
120
     *
121
     * @access public
122
     *
123
     * @param string $core: The name of the new core. If empty, the next available core name is used.
124
     *
125
     * @return string The name of the new core
126
     */
127
    public static function createCore($core = '')
128
    {
129
        // Get next available core name if none given.
130
        if (empty($core)) {
131
            $core = 'dlfCore' . self::getNextCoreNumber();
132
        }
133
        // Get Solr service instance.
134
        $solr = self::getInstance($core);
135
        // Create new core if core with given name doesn't exist.
136
        if ($solr->ready) {
137
            // Core already exists.
138
            return $core;
139
        } else {
140
            // Core doesn't exist yet.
141
            $solrAdmin = self::getInstance();
142
            if ($solrAdmin->ready) {
143
                $query = $solrAdmin->service->createCoreAdmin();
144
                $action = $query->createCreate();
145
                $action->setConfigSet('dlf');
146
                $action->setCore($core);
147
                $action->setDataDir('data');
148
                $action->setInstanceDir($core);
149
                $query->setAction($action);
150
                try {
151
                    $response = $solrAdmin->service->coreAdmin($query);
152
                    if ($response->getWasSuccessful()) {
153
                        // Core successfully created.
154
                        return $core;
155
                    }
156
                } catch (\Exception $e) {
157
                    // Nothing to do here.
158
                }
159
            } else {
160
                Helper::devLog('Apache Solr not available', DEVLOG_SEVERITY_ERROR);
161
            }
162
        }
163
        return '';
164
    }
165
166
    /**
167
     * Escape all special characters in a query string
168
     *
169
     * @access public
170
     *
171
     * @param string $query: The query string
172
     *
173
     * @return string The escaped query string
174
     */
175
    public static function escapeQuery($query)
176
    {
177
        $helper = GeneralUtility::makeInstance(\Solarium\Core\Query\Helper::class);
178
        // Escape query phrase or term.
179
        if (preg_match('/^".*"$/', $query)) {
180
            return $helper->escapePhrase(trim($query, '"'));
181
        } else {
182
            // Using a modified escape function here to retain whitespace, '*' and '?' for search truncation.
183
            // @see https://github.com/solariumphp/solarium/blob/5.x/src/Core/Query/Helper.php#L70 for reference
184
            /* return $helper->escapeTerm($query); */
185
            return preg_replace('/(\+|-|&&|\|\||!|\(|\)|\{|}|\[|]|\^|"|~|:|\/|\\\)/', '\\\$1', $query);
186
        }
187
    }
188
189
    /**
190
     * Escape all special characters in a query string while retaining valid field queries
191
     *
192
     * @access public
193
     *
194
     * @param string $query: The query string
195
     * @param int $pid: The PID for the field configuration
196
     *
197
     * @return string The escaped query string
198
     */
199
    public static function escapeQueryKeepField($query, $pid)
200
    {
201
        // Is there a field query?
202
        if (preg_match('/^[[:alnum:]]+_[tu][su]i:\(?.*\)?$/', $query)) {
203
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
204
                ->getQueryBuilderForTable('tx_dlf_metadata');
205
206
            // Get all indexed fields.
207
            $fields = [];
208
            $result = $queryBuilder
209
                ->select(
210
                    'tx_dlf_metadata.index_name AS index_name',
211
                    'tx_dlf_metadata.index_tokenized AS index_tokenized',
212
                    'tx_dlf_metadata.index_stored AS index_stored'
213
                )
214
                ->from('tx_dlf_metadata')
215
                ->where(
216
                    $queryBuilder->expr()->eq('tx_dlf_metadata.index_indexed', 1),
217
                    $queryBuilder->expr()->eq('tx_dlf_metadata.pid', intval($pid)),
218
                    $queryBuilder->expr()->orX(
219
                        $queryBuilder->expr()->in('tx_dlf_metadata.sys_language_uid', [-1, 0]),
220
                        $queryBuilder->expr()->eq('tx_dlf_metadata.l18n_parent', 0)
221
                    ),
222
                    Helper::whereExpression('tx_dlf_metadata')
223
                )
224
                ->execute();
225
226
            while ($resArray = $result->fetch()) {
227
                $fields[] = $resArray['index_name'] . '_' . ($resArray['index_tokenized'] ? 't' : 'u') . ($resArray['index_stored'] ? 's' : 'u') . 'i';
228
            }
229
230
            // Check if queried field is valid.
231
            $splitQuery = explode(':', $query, 2);
232
            if (in_array($splitQuery[0], $fields)) {
233
                $query = $splitQuery[0] . ':(' . self::escapeQuery(trim($splitQuery[1], '()')) . ')';
234
            } else {
235
                $query = self::escapeQuery($query);
236
            }
237
        } else {
238
            $query = self::escapeQuery($query);
239
        }
240
        return $query;
241
    }
242
243
    /**
244
     * This is a singleton class, thus instances must be created by this method
245
     *
246
     * @access public
247
     *
248
     * @param mixed $core: Name or UID of the core to load or null to get core admin endpoint
249
     *
250
     * @return \Kitodo\Dlf\Common\Solr Instance of this class
251
     */
252
    public static function getInstance($core = null)
253
    {
254
        // Get core name if UID is given.
255
        if (MathUtility::canBeInterpretedAsInteger($core)) {
256
            $core = Helper::getIndexNameFromUid($core, 'tx_dlf_solrcores');
257
        }
258
        // Check if core is set or null.
259
        if (
260
            empty($core)
261
            && $core !== null
262
        ) {
263
            Helper::devLog('Invalid core UID or name given for Apache Solr', DEVLOG_SEVERITY_ERROR);
264
        }
265
        if (!empty($core)) {
266
            // Check if there is an instance in the registry already.
267
            if (
268
                is_object(self::$registry[$core])
269
                && self::$registry[$core] instanceof self
270
            ) {
271
                // Return singleton instance if available.
272
                return self::$registry[$core];
273
            }
274
        }
275
        // Create new instance...
276
        $instance = new self($core);
277
        // ...and save it to registry.
278
        if (!empty($instance->core)) {
279
            self::$registry[$instance->core] = $instance;
280
        }
281
        return $instance;
282
    }
283
284
    /**
285
     * Get next unused Solr core number
286
     *
287
     * @access public
288
     *
289
     * @param int $number: Number to start with
290
     *
291
     * @return int First unused core number found
292
     */
293
    public static function getNextCoreNumber($number = 0)
294
    {
295
        $number = max(intval($number), 0);
296
        // Check if core already exists.
297
        $solr = self::getInstance('dlfCore' . $number);
298
        if (!$solr->ready) {
299
            return $number;
300
        } else {
301
            return self::getNextCoreNumber($number + 1);
302
        }
303
    }
304
305
    /**
306
     * Sets the connection information for Solr
307
     *
308
     * @access protected
309
     *
310
     * @return void
311
     */
312
    protected function loadSolrConnectionInfo()
313
    {
314
        if (empty($this->config)) {
315
            $config = [];
316
            // Extract extension configuration.
317
            $conf = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf'][self::$extKey]);
318
            // Derive Solr scheme
319
            $config['scheme'] = empty($conf['solrHttps']) ? 'http' : 'https';
320
            // Derive Solr host name.
321
            $config['host'] = ($conf['solrHost'] ? $conf['solrHost'] : '127.0.0.1');
322
            // Set username and password.
323
            $config['username'] = $conf['solrUser'];
324
            $config['password'] = $conf['solrPass'];
325
            // Set port if not set.
326
            $config['port'] = MathUtility::forceIntegerInRange($conf['solrPort'], 1, 65535, 8983);
327
            // Trim path of slashes and (re-)add trailing slash if path not empty.
328
            $config['path'] = trim($conf['solrPath'], '/');
329
            if (!empty($config['path'])) {
330
                $config['path'] .= '/';
331
            }
332
            // Set connection timeout lower than PHP's max_execution_time.
333
            $max_execution_time = intval(ini_get('max_execution_time')) ?: 30;
334
            $config['timeout'] = MathUtility::forceIntegerInRange($conf['solrTimeout'], 1, $max_execution_time, 10);
335
            $this->config = $config;
336
        }
337
    }
338
339
    /**
340
     * Processes a search request.
341
     *
342
     * @access public
343
     *
344
     * @return \Kitodo\Dlf\Common\DocumentList The result list
345
     */
346
    public function search()
347
    {
348
        $toplevel = [];
349
        // Take over query parameters.
350
        $params = $this->params;
351
        $params['filterquery'] = isset($params['filterquery']) ? $params['filterquery'] : [];
352
        // Set some query parameters.
353
        $params['start'] = 0;
354
        $params['rows'] = 0;
355
        // Perform search to determine the total number of hits without fetching them.
356
        $selectQuery = $this->service->createSelect($params);
357
        $results = $this->service->select($selectQuery);
358
        $this->numberOfHits = $results->getNumFound();
359
        // Restore query parameters
360
        $params = $this->params;
361
        $params['filterquery'] = isset($params['filterquery']) ? $params['filterquery'] : [];
362
        // Restrict the fields to the required ones.
363
        $params['fields'] = 'uid,id';
364
        // Extend filter query to get all documents with the same uids.
365
        foreach ($params['filterquery'] as $key => $value) {
366
            if (isset($value['query'])) {
367
                $params['filterquery'][$key]['query'] = '{!join from=uid to=uid}' . $value['query'];
368
            }
369
        }
370
        // Set filter query to just get toplevel documents.
371
        $params['filterquery'][] = ['query' => 'toplevel:true'];
372
        // Set join query to get all documents with the same uids.
373
        $params['query'] = '{!join from=uid to=uid}' . $params['query'];
374
        // Perform search to determine the total number of toplevel hits and fetch the required rows.
375
        $selectQuery = $this->service->createSelect($params);
376
        $results = $this->service->select($selectQuery);
377
        $numberOfToplevelHits = $results->getNumFound();
378
        // Process results.
379
        foreach ($results as $doc) {
380
            $toplevel[$doc->id] = [
381
                'u' => $doc->uid,
382
                'h' => '',
383
                's' => '',
384
                'p' => []
385
            ];
386
        }
387
        // Save list of documents.
388
        $list = GeneralUtility::makeInstance(DocumentList::class);
389
        $list->reset();
390
        $list->add(array_values($toplevel));
391
        // Set metadata for search.
392
        $list->metadata = [
393
            'label' => '',
394
            'description' => '',
395
            'options' => [
396
                'source' => 'search',
397
                'engine' => 'solr',
398
                'select' => $this->params['query'],
399
                'userid' => 0,
400
                'params' => $this->params,
401
                'core' => $this->core,
402
                'pid' => $this->cPid,
403
                'order' => 'score',
404
                'order.asc' => true,
405
                'numberOfHits' => $this->numberOfHits,
406
                'numberOfToplevelHits' => $numberOfToplevelHits
407
            ]
408
        ];
409
        return $list;
410
    }
411
412
    /**
413
     * Processes a search request and returns the raw Apache Solr Documents.
414
     *
415
     * @access public
416
     *
417
     * @param string $query: The search query
418
     * @param array $parameters: Additional search parameters
419
     *
420
     * @return array The Apache Solr Documents that were fetched
421
     */
422
    public function search_raw($query = '', $parameters = [])
423
    {
424
        // Set additional query parameters.
425
        $parameters['start'] = 0;
426
        $parameters['rows'] = $this->limit;
427
        // Set query.
428
        $parameters['query'] = $query;
429
430
        // calculate cache identifier
431
        $cacheIdentifier = hash('md5', print_r(array_merge($this->params, $parameters), 1));
432
        $cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('tx_dlf_solr');
433
434
        $resultSet = [];
435
        if (($entry = $cache->get($cacheIdentifier)) === false) {
436
            $selectQuery = $this->service->createSelect(array_merge($this->params, $parameters));
437
            $result = $this->service->select($selectQuery);
438
            foreach ($result as $doc) {
439
                $resultSet[] = $doc;
440
            }
441
            // Save value in cache
442
            $cache->set($cacheIdentifier, $resultSet);
443
        } else {
444
            // return cache hit
445
            $resultSet = $entry;
446
        }
447
        return $resultSet;
448
    }
449
450
    /**
451
     * This returns $this->core via __get()
452
     *
453
     * @access protected
454
     *
455
     * @return string|null The core name of the current query endpoint or null if core admin endpoint
456
     */
457
    protected function _getCore()
458
    {
459
        return $this->core;
460
    }
461
462
    /**
463
     * This returns $this->limit via __get()
464
     *
465
     * @access protected
466
     *
467
     * @return int The max number of results
468
     */
469
    protected function _getLimit()
470
    {
471
        return $this->limit;
472
    }
473
474
    /**
475
     * This returns $this->numberOfHits via __get()
476
     *
477
     * @access protected
478
     *
479
     * @return int Total number of hits for last search
480
     */
481
    protected function _getNumberOfHits()
482
    {
483
        return $this->numberOfHits;
484
    }
485
486
    /**
487
     * This returns $this->ready via __get()
488
     *
489
     * @access protected
490
     *
491
     * @return bool Is the search instantiated successfully?
492
     */
493
    protected function _getReady()
494
    {
495
        return $this->ready;
496
    }
497
498
    /**
499
     * This returns $this->service via __get()
500
     *
501
     * @access protected
502
     *
503
     * @return \Solarium\Client Apache Solr service object
504
     */
505
    protected function _getService()
506
    {
507
        return $this->service;
508
    }
509
510
    /**
511
     * This sets $this->cPid via __set()
512
     *
513
     * @access protected
514
     *
515
     * @param int $value: The new PID for the metadata definitions
516
     *
517
     * @return void
518
     */
519
    protected function _setCPid($value)
520
    {
521
        $this->cPid = max(intval($value), 0);
522
    }
523
524
    /**
525
     * This sets $this->limit via __set()
526
     *
527
     * @access protected
528
     *
529
     * @param int $value: The max number of results
530
     *
531
     * @return void
532
     */
533
    protected function _setLimit($value)
534
    {
535
        $this->limit = max(intval($value), 0);
536
    }
537
538
    /**
539
     * This sets $this->params via __set()
540
     *
541
     * @access protected
542
     *
543
     * @param array $value: The query parameters
544
     *
545
     * @return void
546
     */
547
    protected function _setParams(array $value)
548
    {
549
        $this->params = $value;
550
    }
551
552
    /**
553
     * This magic method is called each time an invisible property is referenced from the object
554
     *
555
     * @access public
556
     *
557
     * @param string $var: Name of variable to get
558
     *
559
     * @return mixed Value of $this->$var
560
     */
561
    public function __get($var)
562
    {
563
        $method = '_get' . ucfirst($var);
564
        if (
565
            !property_exists($this, $var)
566
            || !method_exists($this, $method)
567
        ) {
568
            Helper::devLog('There is no getter function for property "' . $var . '"', DEVLOG_SEVERITY_WARNING);
569
            return;
570
        } else {
571
            return $this->$method();
572
        }
573
    }
574
575
    /**
576
     * This magic method is called each time an invisible property is checked for isset() or empty()
577
     *
578
     * @access public
579
     *
580
     * @param string $var: Name of variable to check
581
     *
582
     * @return bool true if variable is set and not empty, false otherwise
583
     */
584
    public function __isset($var)
585
    {
586
        return !empty($this->__get($var));
587
    }
588
589
    /**
590
     * This magic method is called each time an invisible property is referenced from the object
591
     *
592
     * @access public
593
     *
594
     * @param string $var: Name of variable to set
595
     * @param mixed $value: New value of variable
596
     *
597
     * @return void
598
     */
599
    public function __set($var, $value)
600
    {
601
        $method = '_set' . ucfirst($var);
602
        if (
603
            !property_exists($this, $var)
604
            || !method_exists($this, $method)
605
        ) {
606
            Helper::devLog('There is no setter function for property "' . $var . '"', DEVLOG_SEVERITY_WARNING);
607
        } else {
608
            $this->$method($value);
609
        }
610
    }
611
612
    /**
613
     * This is a singleton class, thus the constructor should be private/protected
614
     *
615
     * @access protected
616
     *
617
     * @param string|null $core: The name of the core to use or null for core admin endpoint
618
     *
619
     * @return void
620
     */
621
    protected function __construct($core)
622
    {
623
        // Get Solr connection parameters from configuration.
624
        $this->loadSolrConnectionInfo();
625
        // Configure connection adapter.
626
        $adapter = GeneralUtility::makeInstance(\Solarium\Core\Client\Adapter\Http::class);
627
        $adapter->setTimeout($this->config['timeout']);
628
        // Configure event dispatcher.
629
            // Todo: When updating to TYPO3 >=10.x and Solarium >=6.x
630
            // we have to provide an PSR-14 Event Dispatcher instead of
631
            // "null".
632
            // $eventDispatcher = GeneralUtility::makeInstance(\TYPO3\CMS\Core\EventDispatcher\EventDispatcher::class);
633
        $eventDispatcher = null;
634
        // Configure endpoint.
635
        $config = [
636
            'endpoint' => [
637
                'default' => [
638
                    'scheme' => $this->config['scheme'],
639
                    'host' => $this->config['host'],
640
                    'port' => $this->config['port'],
641
                    'path' => '/' . $this->config['path'],
642
                    'core' => $core,
643
                    'username' => $this->config['username'],
644
                    'password' => $this->config['password']
645
                ]
646
            ]
647
        ];
648
        // Instantiate Solarium\Client class.
649
        $this->service = GeneralUtility::makeInstance(\Solarium\Client::class, $adapter, $eventDispatcher, $config);
650
        // Check if connection is established.
651
        $query = $this->service->createCoreAdmin();
652
        $action = $query->createStatus();
653
        if ($core !== null) {
654
            $action->setCore($core);
655
        }
656
        $query->setAction($action);
657
        try {
658
            $response = $this->service->coreAdmin($query);
659
            if ($response->getWasSuccessful()) {
660
                // Solr is reachable, but is the core as well?
661
                if ($core !== null) {
662
                    $result = $response->getStatusResult();
663
                    if (
664
                        $result instanceof \Solarium\QueryType\Server\CoreAdmin\Result\StatusResult
665
                        && $result->getUptime() > 0
666
                    ) {
667
                        // Set core name.
668
                        $this->core = $core;
0 ignored issues
show
Bug introduced by
The property core is declared read-only in Kitodo\Dlf\Common\Solr.
Loading history...
669
                    } else {
670
                        // Core not available.
671
                        return;
672
                    }
673
                }
674
                // Instantiation successful!
675
                $this->ready = true;
676
            }
677
        } catch (\Exception $e) {
678
            // Nothing to do here.
679
        }
680
    }
681
}
682