Passed
Pull Request — master (#123)
by
unknown
05:29
created

OaiPmhController   F

Complexity

Total Complexity 108

Size/Duplication

Total Lines 848
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 1
Metric Value
eloc 352
c 3
b 0
f 1
dl 0
loc 848
rs 2
wmc 108

25 Methods

Rating   Name   Duplication   Size   Complexity  
A checkGranularity() 0 8 4
A resume() 0 13 3
A getDate() 0 3 2
B mainAction() 0 36 7
A getMetsData() 0 17 3
A getDateFromTimestamp() 0 11 1
A getDublinCoreData() 0 30 3
A verbListMetadataFormats() 0 24 6
A injectLibraryRepository() 0 3 1
A addDublinCoreData() 0 4 2
B generateOutputForDocumentList() 0 44 9
B fetchDocumentSet() 0 68 10
A deleteExpiredTokens() 0 4 1
A initializeAction() 0 3 1
A injectTokenRepository() 0 3 1
A getFrom() 0 14 3
B verbListIdentifiers() 0 46 9
A getUntil() 0 17 5
A getUrlParams() 0 18 3
A verbListSets() 0 8 1
B verbListRecords() 0 47 9
A generateResumptionTokenForDocumentListSet() 0 34 4
A injectCollectionRepository() 0 3 1
B verbIdentify() 0 41 8
B verbGetRecord() 0 44 11

How to fix   Complexity   

Complex Class

Complex classes like OaiPmhController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use OaiPmhController, and based on these observations, apply Extract Interface, too.

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'] . '"');
0 ignored issues
show
Bug introduced by
The method error() does not exist on null. ( Ignorable by Annotation )

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

255
                $this->logger->/** @scrutinizer ignore-call */ 
256
                               error('No METS part found in document with location "' . $record['location'] . '"');

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...
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