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.

OaiPmhController::getDateFromTimestamp()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 8
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 11
rs 10
1
<?php
2
/**
3
 * (c) Kitodo. Key to digital objects e.V. <[email protected]>
4
 *
5
 * This file is part of the Kitodo and TYPO3 projects.
6
 *
7
 * @license GNU General Public License version 3 or later.
8
 * For the full copyright and license information, please read the
9
 * LICENSE.txt file that was distributed with this source code.
10
 */
11
12
namespace Kitodo\Dlf\Controller;
13
14
use TYPO3\CMS\Core\Utility\GeneralUtility;
15
use Kitodo\Dlf\Common\Solr\Solr;
16
use Kitodo\Dlf\Domain\Model\Token;
17
use Kitodo\Dlf\Domain\Repository\CollectionRepository;
18
use Kitodo\Dlf\Domain\Repository\LibraryRepository;
19
use Kitodo\Dlf\Domain\Repository\TokenRepository;
20
use Psr\Http\Message\ResponseInterface;
21
22
/**
23
 * Controller class for the plugin 'OAI-PMH Interface'.
24
 *
25
 * @package TYPO3
26
 * @subpackage dlf
27
 *
28
 * @access public
29
 */
30
class OaiPmhController extends AbstractController
31
{
32
    /**
33
     * @access protected
34
     * @var TokenRepository
35
     */
36
    protected $tokenRepository;
37
38
    /**
39
     * @access public
40
     *
41
     * @param TokenRepository $tokenRepository
42
     *
43
     * @return void
44
     */
45
    public function injectTokenRepository(TokenRepository $tokenRepository)
46
    {
47
        $this->tokenRepository = $tokenRepository;
48
    }
49
50
    /**
51
     * @access protected
52
     * @var CollectionRepository
53
     */
54
    protected $collectionRepository;
55
56
    /**
57
     * @access public
58
     *
59
     * @param CollectionRepository $collectionRepository
60
     *
61
     * @return void
62
     */
63
    public function injectCollectionRepository(CollectionRepository $collectionRepository)
64
    {
65
        $this->collectionRepository = $collectionRepository;
66
    }
67
68
    /**
69
     * @access protected
70
     * @var LibraryRepository
71
     */
72
    protected $libraryRepository;
73
74
    /**
75
     * @access public
76
     *
77
     * @param LibraryRepository $libraryRepository
78
     *
79
     * @return void
80
     */
81
    public function injectLibraryRepository(LibraryRepository $libraryRepository)
82
    {
83
        $this->libraryRepository = $libraryRepository;
84
    }
85
86
    /**
87
     * Initializes the current action
88
     *
89
     * @access public
90
     *
91
     * @return void
92
     */
93
    public function initializeAction()
94
    {
95
        $this->request = $this->request->withFormat("xml");
96
    }
97
98
    /**
99
     * @access protected
100
     * @var string Did an error occur?
101
     */
102
    protected $error;
103
104
    /**
105
     * @access protected
106
     * @var array This holds the configuration for all supported metadata prefixes
107
     */
108
    protected $formats = [
109
        'oai_dc' => [
110
            'schema' => 'http://www.openarchives.org/OAI/2.0/oai_dc.xsd',
111
            'namespace' => 'http://www.openarchives.org/OAI/2.0/oai_dc/',
112
            'requiredFields' => ['record_id'],
113
        ],
114
        'epicur' => [
115
            'schema' => 'http://www.persistent-identifier.de/xepicur/version1.0/xepicur.xsd',
116
            'namespace' => 'urn:nbn:de:1111-2004033116',
117
            'requiredFields' => ['purl', 'urn'],
118
        ],
119
        'mets' => [
120
            'schema' => 'http://www.loc.gov/standards/mets/version17/mets.v1-7.xsd',
121
            'namespace' => 'http://www.loc.gov/METS/',
122
            'requiredFields' => ['location'],
123
        ]
124
    ];
125
126
    /**
127
     * @access protected
128
     * @var array
129
     */
130
    protected $parameters = [];
131
132
    /**
133
     * Delete expired resumption tokens
134
     *
135
     * @access protected
136
     *
137
     * @return void
138
     */
139
    protected function deleteExpiredTokens()
140
    {
141
        // Delete expired resumption tokens.
142
        $this->tokenRepository->deleteExpiredTokens($this->settings['expired']);
143
    }
144
145
    /**
146
     * Load URL parameters
147
     *
148
     * @access protected
149
     *
150
     * @return void
151
     */
152
    protected function getUrlParams()
153
    {
154
        $allowedParams = [
155
            'verb',
156
            'identifier',
157
            'metadataPrefix',
158
            'from',
159
            'until',
160
            'set',
161
            'resumptionToken'
162
        ];
163
        // Clear plugin variables.
164
        $this->parameters = [];
165
        // Set only allowed parameters.
166
        foreach ($allowedParams as $param) {
167
            // replace with $this->request->getQueryParams() when dropping support for Typo3 v11, see Deprecation-100596
168
            if (GeneralUtility::_GP($param)) {
169
                $this->parameters[$param] = GeneralUtility::_GP($param);
170
            }
171
        }
172
    }
173
174
    /**
175
     * Get unqualified Dublin Core data.
176
     * @see http://www.openarchives.org/OAI/openarchivesprotocol.html#dublincore
177
     *
178
     * @access private
179
     *
180
     * @param array $record The full record array
181
     *
182
     * @return array The mapped metadata array
183
     */
184
    private function getDublinCoreData(array $record)
185
    {
186
        $metadata = [];
187
188
        $metadata[] = ['dc:identifier' => $record['record_id']];
189
190
        $this->addDublinCoreData($metadata, 'dc:identifier', $record['purl']);
191
        $this->addDublinCoreData($metadata, 'dc:identifier', $record['prod_id']);
192
        $this->addDublinCoreData($metadata, 'dc:identifier', $record['urn']);
193
        $this->addDublinCoreData($metadata, 'dc:title', $record['title']);
194
        $this->addDublinCoreData($metadata, 'dc:creator', $record['author']);
195
        $this->addDublinCoreData($metadata, 'dc:date', $record['year']);
196
        $this->addDublinCoreData($metadata, 'dc:coverage', $record['place']);
197
198
        $record[] = ['dc:format' => $record['application/mets+xml']];
199
        $record[] = ['dc:type' => $record['Text']];
200
        if (!empty($record['partof'])) {
201
            $document = $this->documentRepository->findOneByPartof($metadata['partof']);
0 ignored issues
show
Bug introduced by
The method findOneByPartof() does not exist on Kitodo\Dlf\Domain\Repository\DocumentRepository. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

201
            /** @scrutinizer ignore-call */ 
202
            $document = $this->documentRepository->findOneByPartof($metadata['partof']);
Loading history...
202
203
            if ($document) {
204
                $metadata[] = ['dc:relation' => $document->getRecordId()];
0 ignored issues
show
Bug introduced by
The method getRecordId() does not exist on TYPO3\CMS\Extbase\Persistence\QueryResultInterface. ( Ignorable by Annotation )

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

204
                $metadata[] = ['dc:relation' => $document->/** @scrutinizer ignore-call */ getRecordId()];

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
205
            }
206
        }
207
        $this->addDublinCoreData($metadata, 'dc:rights', $record['license']);
0 ignored issues
show
Bug introduced by
$record['license'] of type array|array<string,array> is incompatible with the type string expected by parameter $value of Kitodo\Dlf\Controller\Oa...er::addDublinCoreData(). ( Ignorable by Annotation )

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

207
        $this->addDublinCoreData($metadata, 'dc:rights', /** @scrutinizer ignore-type */ $record['license']);
Loading history...
208
        $this->addDublinCoreData($metadata, 'dc:rights', $record['terms']);
209
        $this->addDublinCoreData($metadata, 'dc:rights', $record['restrictions']);
210
        $this->addDublinCoreData($metadata, 'dc:rights', $record['out_of_print']);
211
        $this->addDublinCoreData($metadata, 'dc:rights', $record['rights_info']);
212
213
        return $metadata;
214
    }
215
216
    /**
217
     * Add Dublin Core data.
218
     *
219
     * @access private
220
     *
221
     * @param array $metadata The mapped metadata array passed as reference
222
     * @param string $key The key to which record value should be assigned
223
     * @param string $value The key from record array
224
     *
225
     * @return void
226
     */
227
    private function addDublinCoreData(&$metadata, $key, $value)
228
    {
229
        if (!empty($value)) {
230
            $metadata[] = [$key => $value];
231
        }
232
    }
233
234
    /**
235
     * Get METS data.
236
     * @see http://www.loc.gov/standards/mets/docs/mets.v1-7.html
237
     *
238
     * @access protected
239
     *
240
     * @param array $record The full record array
241
     *
242
     * @return string The fetched METS XML
243
     */
244
    protected function getMetsData(array $record)
245
    {
246
        $mets = null;
247
        // Load METS file.
248
        $xml = new \DOMDocument();
249
        if ($xml->load($record['location'])) {
250
            // Get root element.
251
            $root = $xml->getElementsByTagNameNS($this->formats['mets']['namespace'], 'mets');
252
            if ($root->item(0) instanceof \DOMNode) {
253
                $mets = $xml->saveXML($root->item(0));
254
            } else {
255
                $this->logger->error('No METS part found in document with location "' . $record['location'] . '"');
256
            }
257
        } else {
258
            $this->logger->error('Could not load XML file from "' . $record['location'] . '"');
259
        }
260
        return $mets;
261
    }
262
263
    /**
264
     * The main method of the plugin
265
     *
266
     * @access public
267
     *
268
     * @return ResponseInterface
269
     */
270
    public function mainAction(): ResponseInterface
271
    {
272
        // Get allowed GET and POST variables.
273
        $this->getUrlParams();
274
275
        // Delete expired resumption tokens.
276
        $this->deleteExpiredTokens();
277
278
        switch ($this->parameters['verb'] ?? null) {
279
            case 'GetRecord':
280
                $this->verbGetRecord();
281
                break;
282
            case 'Identify':
283
                $this->verbIdentify();
284
                break;
285
            case 'ListIdentifiers':
286
                $this->verbListIdentifiers();
287
                break;
288
            case 'ListMetadataFormats':
289
                $this->verbListMetadataFormats();
290
                break;
291
            case 'ListRecords':
292
                $this->verbListRecords();
293
                break;
294
            case 'ListSets':
295
                $this->verbListSets();
296
                break;
297
            default:
298
                $this->error = 'badVerb';
299
                break;
300
        }
301
302
        $this->view->assign('parameters', $this->parameters);
303
        $this->view->assign('error', $this->error);
304
305
        return $this->htmlResponse();
306
    }
307
308
    /**
309
     * Continue with resumption token
310
     *
311
     * @access protected
312
     *
313
     * @return array|null list of uids
314
     */
315
    protected function resume(): ?array
316
    {
317
        $token = $this->tokenRepository->findOneByToken($this->parameters['resumptionToken']);
0 ignored issues
show
Bug introduced by
The method findOneByToken() does not exist on Kitodo\Dlf\Domain\Repository\TokenRepository. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

317
        /** @scrutinizer ignore-call */ 
318
        $token = $this->tokenRepository->findOneByToken($this->parameters['resumptionToken']);
Loading history...
318
319
        if ($token) {
320
            $options = $token->getOptions();
0 ignored issues
show
Bug introduced by
The method getOptions() does not exist on TYPO3\CMS\Extbase\Persistence\QueryResultInterface. ( Ignorable by Annotation )

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

320
            /** @scrutinizer ignore-call */ 
321
            $options = $token->getOptions();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
321
        }
322
        if (is_array($options)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $options does not seem to be defined for all execution paths leading up to this point.
Loading history...
323
            return $options;
324
        } else {
325
            // No resumption token found or resumption token expired.
326
            $this->error = 'badResumptionToken';
327
            return null;
328
        }
329
    }
330
331
    /**
332
     * Process verb "GetRecord"
333
     *
334
     * @access protected
335
     *
336
     * @return void
337
     */
338
    protected function verbGetRecord()
339
    {
340
        if (count($this->parameters) !== 3 || empty($this->parameters['metadataPrefix']) || empty($this->parameters['identifier'])) {
341
            $this->error = 'badArgument';
342
            return;
343
        }
344
345
        if (!array_key_exists($this->parameters['metadataPrefix'], $this->formats)) {
346
            $this->error = 'cannotDisseminateFormat';
347
            return;
348
        }
349
350
        $document = $this->documentRepository->getOaiRecord($this->settings, $this->parameters);
351
352
        if (!$document['uid']) {
353
            $this->error = 'idDoesNotExist';
354
            return;
355
        }
356
357
        // Check for required fields.
358
        foreach ($this->formats[$this->parameters['metadataPrefix']]['requiredFields'] as $required) {
359
            if (empty($document[$required])) {
360
                $this->error = 'cannotDisseminateFormat';
361
                return;
362
            }
363
        }
364
365
        // we need the collections as array later
366
        $document['collections'] = explode(' ', $document['collections']);
367
368
        // Add metadata
369
        switch ($this->parameters['metadataPrefix']) {
370
            case 'oai_dc':
371
                $document['metadata'] = $this->getDublinCoreData($document);
372
                break;
373
            case 'epicur':
374
                $document['metadata'] = $document;
375
                break;
376
            case 'mets':
377
                $document['metadata'] = $this->getMetsData($document);
378
                break;
379
        }
380
381
        $this->view->assign('record', $document);
382
    }
383
384
    /**
385
     * Process verb "Identify"
386
     *
387
     * @access protected
388
     *
389
     * @return void
390
     */
391
    protected function verbIdentify()
392
    {
393
        $library = $this->libraryRepository->findByUid($this->settings['library']);
394
395
        $oaiIdentifyInfo = [];
396
397
        if (!$oaiIdentifyInfo) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $oaiIdentifyInfo of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
introduced by
$oaiIdentifyInfo is an empty array, thus ! $oaiIdentifyInfo is always true.
Loading history...
398
            $this->logger->notice('Incomplete plugin configuration');
399
        }
400
401
        $oaiIdentifyInfo['oai_label'] = $library ? $library->getOaiLabel() : '';
402
        // Use default values for an installation with incomplete plugin configuration.
403
        if (empty($oaiIdentifyInfo['oai_label'])) {
404
            $oaiIdentifyInfo['oai_label'] = 'Kitodo.Presentation OAI-PMH Interface (default configuration)';
405
            $this->logger->notice('Incomplete plugin configuration (oai_label is missing)');
406
        }
407
408
        $oaiIdentifyInfo['contact'] = $library ? $library->getContact() : '';
409
        if (empty($oaiIdentifyInfo['contact'])) {
410
            $oaiIdentifyInfo['contact'] = '[email protected]';
411
            $this->logger->notice('Incomplete plugin configuration (contact is missing)');
412
        }
413
414
        $document = $this->documentRepository->findOldestDocument();
415
416
        if ($document) {
417
            $oaiIdentifyInfo['earliestDatestamp'] = gmdate('Y-m-d\TH:i:s\Z', $document->getTstamp()->getTimestamp());
418
        } else {
419
            // Provide a fallback timestamp if no document is found
420
            $oaiIdentifyInfo['earliestDatestamp'] = '0000-00-00T00:00:00Z';
421
422
            // access storagePid from TypoScript
423
            $pageSettings = $this->configurationManager->getConfiguration($this->configurationManager::CONFIGURATION_TYPE_FULL_TYPOSCRIPT);
424
            $storagePid = $pageSettings["plugin."]["tx_dlf."]["persistence."]["storagePid"];
425
            if ($storagePid > 0) {
426
                $this->logger->notice('No records found with PID ' . $storagePid);
427
            } else {
428
                $this->logger->notice('No records found');
429
            }
430
        }
431
        $this->view->assign('oaiIdentifyInfo', $oaiIdentifyInfo);
432
    }
433
434
    /**
435
     * Process verb "ListIdentifiers"
436
     *
437
     * @access protected
438
     *
439
     * @return void
440
     */
441
    protected function verbListIdentifiers()
442
    {
443
        // If we have a resumption token we can continue our work
444
        if (!empty($this->parameters['resumptionToken'])) {
445
            // "resumptionToken" is an exclusive argument.
446
            if (count($this->parameters) > 2) {
447
                $this->error = 'badArgument';
448
                return;
449
            } else {
450
                // return next chunk of documents
451
                $resultSet = $this->resume();
452
                if (is_array($resultSet)) {
453
                    $listIdentifiers = $this->generateOutputForDocumentList($resultSet);
454
                    $this->view->assign('listIdentifiers', $listIdentifiers);
455
                }
456
                return;
457
            }
458
        }
459
        // "metadataPrefix" is required and "identifier" is not allowed.
460
        if (empty($this->parameters['metadataPrefix']) || !empty($this->parameters['identifier'])) {
461
            $this->error = 'badArgument';
462
            return;
463
        }
464
        if (!in_array($this->parameters['metadataPrefix'], array_keys($this->formats))) {
465
            $this->error = 'cannotDisseminateFormat';
466
            return;
467
        }
468
        try {
469
            $documentSet = $this->fetchDocumentSet();
470
        } catch (\Exception $exception) {
471
            $this->error = 'idDoesNotExist';
472
            return;
473
        }
474
        // create new and empty document list
475
        $resultSet = [];
476
        if (is_array($documentSet)) {
0 ignored issues
show
introduced by
The condition is_array($documentSet) is always true.
Loading history...
477
            $resultSet['elements'] = $documentSet;
478
            $resultSet['metadata'] = [
479
                'cursor' => 0,
480
                'completeListSize' => count($documentSet),
481
                'metadataPrefix' => $this->parameters['metadataPrefix'],
482
            ];
483
        }
484
485
        $listIdentifiers = $this->generateOutputForDocumentList($resultSet);
486
        $this->view->assign('listIdentifiers', $listIdentifiers);
487
    }
488
489
    /**
490
     * Process verb "ListMetadataFormats"
491
     *
492
     * @access protected
493
     *
494
     * @return void
495
     */
496
    protected function verbListMetadataFormats()
497
    {
498
        $resArray = [];
499
        // check for the optional "identifier" parameter
500
        if (isset($this->parameters['identifier'])) {
501
            $resArray = $this->documentRepository->findOneByRecordId($this->parameters['identifier']);
0 ignored issues
show
Bug introduced by
The method findOneByRecordId() does not exist on Kitodo\Dlf\Domain\Repository\DocumentRepository. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

501
            /** @scrutinizer ignore-call */ 
502
            $resArray = $this->documentRepository->findOneByRecordId($this->parameters['identifier']);
Loading history...
502
        }
503
504
        $resultSet = [];
505
        foreach ($this->formats as $prefix => $details) {
506
            if (!empty($resArray)) {
507
                // check, if all required fields are available for a given identifier
508
                foreach ($details['requiredFields'] as $required) {
509
                    $methodName = 'get' . GeneralUtility::underscoredToUpperCamelCase($required);
510
                    if (empty($resArray->$methodName())) {
511
                        // Skip metadata formats whose requirements are not met.
512
                        continue 2;
513
                    }
514
                }
515
            }
516
            $details['prefix'] = $prefix;
517
            $resultSet[] = $details;
518
        }
519
        $this->view->assign('metadataFormats', $resultSet);
520
    }
521
522
    /**
523
     * Process verb "ListRecords"
524
     *
525
     * @access protected
526
     *
527
     * @return void
528
     */
529
    protected function verbListRecords()
530
    {
531
        // Check for invalid arguments.
532
        if (!empty($this->parameters['resumptionToken'])) {
533
            // "resumptionToken" is an exclusive argument.
534
            if (count($this->parameters) > 2) {
535
                $this->error = 'badArgument';
536
                return;
537
            } else {
538
                // return next chunk of documents
539
                $resultSet = $this->resume();
540
                if (is_array($resultSet)) {
541
                    $listRecords = $this->generateOutputForDocumentList($resultSet);
542
                    $this->parameters['metadataPrefix'] = $resultSet['metadata']['metadataPrefix'];
543
                    $this->view->assign('listRecords', $listRecords);
544
                }
545
                return;
546
            }
547
        }
548
        if (empty($this->parameters['metadataPrefix']) || !empty($this->parameters['identifier'])) {
549
            // "metadataPrefix" is required and "identifier" is not allowed.
550
            $this->error = 'badArgument';
551
            return;
552
        }
553
        // Check "metadataPrefix" for valid value.
554
        if (!in_array($this->parameters['metadataPrefix'], array_keys($this->formats))) {
555
            $this->error = 'cannotDisseminateFormat';
556
            return;
557
        }
558
        try {
559
            $documentSet = $this->fetchDocumentSet();
560
        } catch (\Exception $exception) {
561
            $this->error = 'idDoesNotExist';
562
            return;
563
        }
564
        $resultSet = [];
565
        if (count($documentSet) > 0) {
566
            $resultSet['elements'] = $documentSet;
567
            $resultSet['metadata'] = [
568
                'cursor' => 0,
569
                'completeListSize' => count($documentSet),
570
                'metadataPrefix' => $this->parameters['metadataPrefix'],
571
            ];
572
        }
573
574
        $resultSet = $this->generateOutputForDocumentList($resultSet);
575
        $this->view->assign('listRecords', $resultSet);
576
    }
577
578
    /**
579
     * Process verb "ListSets"
580
     *
581
     * @access protected
582
     *
583
     * @return void
584
     */
585
    protected function verbListSets()
586
    {
587
        // It is required to set a oai_name inside the collection record to be shown in oai-pmh plugin.
588
        $this->settings['hideEmptyOaiNames'] = true;
589
590
        $oaiSets = $this->collectionRepository->findCollectionsBySettings($this->settings);
591
592
        $this->view->assign('oaiSets', $oaiSets);
593
    }
594
595
    /**
596
     * Fetch records
597
     *
598
     * @access protected
599
     *
600
     * @return array matching records or empty array if there were some errors
601
     */
602
    protected function fetchDocumentSet(): array
603
    {
604
        $documentSet = [];
605
        $solrQuery = '';
606
        // Check "set" for valid value.
607
        if (!empty($this->parameters['set'])) {
608
            // For SOLR we need the index_name of the collection,
609
            // For DB Query we need the UID of the collection
610
611
            $result = $this->collectionRepository->getIndexNameForSolr($this->settings, $this->parameters['set']);
612
            $resArray = $result->fetchAssociative();
613
            if ($resArray) {
614
                if ($resArray['index_query'] != "") {
615
                    $solrQuery .= '(' . $resArray['index_query'] . ')';
616
                } else {
617
                    $solrQuery .= 'collection:' . '"' . $resArray['index_name'] . '"';
618
                }
619
            } else {
620
                $this->error = 'noSetHierarchy';
621
                return $documentSet;
622
            }
623
        } else {
624
            // If no set is specified we have to query for all collections
625
            $solrQuery .= 'collection:* NOT collection:""';
626
        }
627
        // Check for required fields.
628
        foreach ($this->formats[$this->parameters['metadataPrefix']]['requiredFields'] as $required) {
629
            $solrQuery .= ' NOT ' . $required . ':""';
630
        }
631
        // toplevel="true" is always required
632
        $solrQuery .= ' AND toplevel:true';
633
634
        $from = $this->getFrom();
635
        $until = $this->getUntil($from);
636
637
        $this->checkGranularity();
638
639
        if ($this->error === 'badArgument') {
640
            return $documentSet;
641
        }
642
643
        $solrQuery .= ' AND timestamp:[' . $from . ' TO ' . $until . ']';
644
645
        $solr = Solr::getInstance($this->settings['solrcore']);
646
        if (!$solr->ready) {
647
            $this->logger->error('Apache Solr not available');
648
            return $documentSet;
649
        }
650
        if ($this->settings['solr_limit'] > 0) {
651
            $solr->limit = $this->settings['solr_limit'];
652
        }
653
        // We only care about the UID in the results and want them sorted
654
        $parameters = [
655
            "fields" => "uid",
656
            "sort" => [
657
                "uid" => "asc"
658
            ]
659
        ];
660
        $parameters['query'] = $solrQuery;
661
        $result = $solr->searchRaw($parameters);
662
        if (empty($result)) {
663
            $this->error = 'noRecordsMatch';
664
            return $documentSet;
665
        }
666
        foreach ($result as $doc) {
667
            $documentSet[] = $doc->uid;
668
        }
669
        return $documentSet;
670
    }
671
672
    /**
673
     * Get 'from' query parameter.
674
     *
675
     * @access private
676
     *
677
     * @return string
678
     */
679
    private function getFrom(): string
680
    {
681
        $from = "*";
682
        // Check "from" for valid value.
683
        if (!empty($this->parameters['from'])) {
684
            // Is valid format?
685
            $date = $this->getDate('from');
686
            if (is_array($date)) {
0 ignored issues
show
introduced by
The condition is_array($date) is always true.
Loading history...
687
                $from = $this->getDateFromTimestamp($date, '.000Z');
688
            } else {
689
                $this->error = 'badArgument';
690
            }
691
        }
692
        return $from;
693
    }
694
695
    /**
696
     * Get 'until' query parameter.
697
     *
698
     * @access private
699
     *
700
     * @param string $from start date
701
     *
702
     * @return string
703
     */
704
    private function getUntil(string $from): string
705
    {
706
        $until = "*";
707
        // Check "until" for valid value.
708
        if (!empty($this->parameters['until'])) {
709
            // Is valid format?
710
            $date = $this->getDate('until');
711
            if (is_array($date)) {
0 ignored issues
show
introduced by
The condition is_array($date) is always true.
Loading history...
712
                $until = $this->getDateFromTimestamp($date, '.999Z');
713
                if ($from != "*" && $from > $until) {
714
                    $this->error = 'badArgument';
715
                }
716
            } else {
717
                $this->error = 'badArgument';
718
            }
719
        }
720
        return $until;
721
    }
722
723
    /**
724
     * Get date from parameter string.
725
     *
726
     * @access private
727
     *
728
     * @param string $dateType
729
     *
730
     * @return array|false
731
     */
732
    private function getDate(string $dateType)
733
    {
734
        return strptime($this->parameters[$dateType], '%Y-%m-%dT%H:%M:%SZ') ?: strptime($this->parameters[$dateType], '%Y-%m-%d');
735
    }
736
737
    /**
738
     * Get date from timestamp.
739
     *
740
     * @access private
741
     *
742
     * @param array $date
743
     * @param string $end
744
     *
745
     * @return string
746
     */
747
    private function getDateFromTimestamp(array $date, string $end): string
748
    {
749
        $timestamp = gmmktime(
750
            $date['tm_hour'],
751
            $date['tm_min'],
752
            $date['tm_sec'],
753
            $date['tm_mon'] + 1,
754
            $date['tm_mday'],
755
            $date['tm_year'] + 1900
756
        );
757
        return date("Y-m-d", $timestamp) . 'T' . date("H:i:s", $timestamp) . $end;
758
    }
759
760
    /**
761
     * Check "from" and "until" for same granularity.
762
     *
763
     * @access private
764
     *
765
     * @return void
766
     */
767
    private function checkGranularity(): void
768
    {
769
        if (
770
            !empty($this->parameters['from'])
771
            && !empty($this->parameters['until'])
772
        ) {
773
            if (strlen($this->parameters['from']) != strlen($this->parameters['until'])) {
774
                $this->error = 'badArgument';
775
            }
776
        }
777
    }
778
779
    /**
780
     * Fetch more information for document list
781
     *
782
     * @access protected
783
     *
784
     * @param array $documentListSet
785
     *
786
     * @return array of enriched records
787
     */
788
    protected function generateOutputForDocumentList(array $documentListSet)
789
    {
790
        // check whether any result elements are available
791
        if (empty($documentListSet) || empty($documentListSet['elements'])) {
792
            $this->error = 'noRecordsMatch';
793
            return [];
794
        }
795
        // consume result elements from list to implement pagination logic of resumptionToken
796
        $documentsToProcess = array_splice($documentListSet['elements'], 0, $this->settings['limit']);
797
        $verb = $this->parameters['verb'];
798
799
        $documents = $this->documentRepository->getOaiDocumentList($documentsToProcess);
800
801
        $records = [];
802
        while ($resArray = $documents->fetchAssociative()) {
803
            // we need the collections as array later
804
            $resArray['collections'] = explode(' ', $resArray['collections']);
805
806
            if ($verb === 'ListRecords') {
807
                // Add metadata node.
808
                $metadataPrefix = $this->parameters['metadataPrefix'];
809
                if (!$metadataPrefix) {
810
                    // If we resume an action the metadataPrefix is stored with the documentSet
811
                    $metadataPrefix = $documentListSet['metadata']['metadataPrefix'];
812
                }
813
                switch ($metadataPrefix) {
814
                    case 'oai_dc':
815
                        $resArray['metadata'] = $this->getDublinCoreData($resArray);
816
                        break;
817
                    case 'epicur':
818
                        $resArray['metadata'] = $resArray;
819
                        break;
820
                    case 'mets':
821
                        $resArray['metadata'] = $this->getMetsData($resArray);
822
                        break;
823
                }
824
            }
825
826
            $records[] = $resArray;
827
        }
828
829
        $this->generateResumptionTokenForDocumentListSet($documentListSet, count($documentsToProcess));
830
831
        return $records;
832
    }
833
834
    /**
835
     * Generate resumption token
836
     *
837
     * @access protected
838
     *
839
     * @param array $documentListSet
840
     * @param int $numShownDocuments
841
     *
842
     * @return void
843
     */
844
    protected function generateResumptionTokenForDocumentListSet(array $documentListSet, int $numShownDocuments)
845
    {
846
        // The cursor specifies how many elements have already been returned in previous requests
847
        // See http://www.openarchives.org/OAI/openarchivesprotocol.html#FlowControl
848
        $currentCursor = $documentListSet['metadata']['cursor'];
849
850
        if (count($documentListSet['elements']) !== 0) {
851
            $resumptionToken = uniqid('', false);
852
853
            $documentListSet['metadata']['cursor'] += $numShownDocuments;
854
855
            // create new token
856
            $newToken = GeneralUtility::makeInstance(Token::class);
857
            $newToken->setToken($resumptionToken);
858
            $newToken->setOptions($documentListSet);
859
860
            // add to tokenRepository
861
            $this->tokenRepository->add($newToken);
862
        } else {
863
            // Result set complete. We don't need a token.
864
            $resumptionToken = '';
865
        }
866
867
        $resumptionTokenInfo = [];
868
        $resumptionTokenInfo['token'] = $resumptionToken;
869
        $resumptionTokenInfo['cursor'] = $currentCursor;
870
        $resumptionTokenInfo['completeListSize'] = $documentListSet['metadata']['completeListSize'];
871
        $expireDateTime = new \DateTime();
872
        $expireDateTime->add(new \DateInterval('PT' . $this->settings['expired'] . 'S'));
873
        $resumptionTokenInfo['expired'] = $expireDateTime;
874
875
        $omitResumptionToken = $currentCursor === 0 && $numShownDocuments >= $documentListSet['metadata']['completeListSize'];
876
        if (!$omitResumptionToken) {
877
            $this->view->assign('resumptionToken', $resumptionTokenInfo);
878
        }
879
    }
880
}
881