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.

Issues (188)

Classes/Common/IiifManifest.php (3 issues)

loose comparison of integers to other values.

Best Practice Bug Major
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 Flow\JSONPath\JSONPath;
16
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
17
use TYPO3\CMS\Core\Database\ConnectionPool;
18
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
19
use TYPO3\CMS\Core\Log\LogManager;
20
use TYPO3\CMS\Core\Utility\GeneralUtility;
21
use Ubl\Iiif\Presentation\Common\Model\Resources\AnnotationContainerInterface;
22
use Ubl\Iiif\Presentation\Common\Model\Resources\AnnotationInterface;
23
use Ubl\Iiif\Presentation\Common\Model\Resources\CanvasInterface;
24
use Ubl\Iiif\Presentation\Common\Model\Resources\ContentResourceInterface;
25
use Ubl\Iiif\Presentation\Common\Model\Resources\IiifResourceInterface;
26
use Ubl\Iiif\Presentation\Common\Model\Resources\ManifestInterface;
27
use Ubl\Iiif\Presentation\Common\Model\Resources\RangeInterface;
28
use Ubl\Iiif\Presentation\Common\Vocabulary\Motivation;
29
use Ubl\Iiif\Presentation\V1\Model\Resources\AbstractIiifResource1;
30
use Ubl\Iiif\Presentation\V2\Model\Resources\AbstractIiifResource2;
31
use Ubl\Iiif\Presentation\V3\Model\Resources\AbstractIiifResource3;
32
use Ubl\Iiif\Services\AbstractImageService;
33
use Ubl\Iiif\Services\Service;
34
use Ubl\Iiif\Tools\IiifHelper;
35
36
/**
37
 * IiifManifest class for the 'dlf' extension.
38
 *
39
 * @package TYPO3
40
 * @subpackage dlf
41
 *
42
 * @access public
43
 *
44
 * @property int $cPid this holds the PID for the configuration
45
 * @property-read array $formats this holds the configuration for all supported metadata encodings
46
 * @property bool $formatsLoaded flag with information if the available metadata formats are loaded
47
 * @property-read bool $hasFulltext flag with information if there are any fulltext files available
48
 * @property array $lastSearchedPhysicalPage the last searched logical and physical page
49
 * @property array $logicalUnits this holds the logical units
50
 * @property-read array $metadataArray this holds the documents' parsed metadata array
51
 * @property bool $metadataArrayLoaded flag with information if the metadata array is loaded
52
 * @property-read int $numPages the holds the total number of pages
53
 * @property-read int $parentId this holds the UID of the parent document or zero if not multi-volumed
54
 * @property-read array $physicalStructure this holds the physical structure
55
 * @property-read array $physicalStructureInfo this holds the physical structure metadata
56
 * @property bool $physicalStructureLoaded flag with information if the physical structure is loaded
57
 * @property-read int $pid this holds the PID of the document or zero if not in database
58
 * @property array $rawTextArray this holds the documents' raw text pages with their corresponding structMap//div's ID (METS) or Range / Manifest / Sequence ID (IIIF) as array key
59
 * @property-read bool $ready Is the document instantiated successfully?
60
 * @property-read string $recordId the METS file's / IIIF manifest's record identifier
61
 * @property-read int $rootId this holds the UID of the root document or zero if not multi-volumed
62
 * @property-read array $smLinks this holds the smLinks between logical and physical structMap
63
 * @property bool $smLinksLoaded flag with information if the smLinks are loaded
64
 * @property-read array $tableOfContents this holds the logical structure
65
 * @property bool $tableOfContentsLoaded flag with information if the table of contents is loaded
66
 * @property-read string $thumbnail this holds the document's thumbnail location
67
 * @property bool $thumbnailLoaded flag with information if the thumbnail is loaded
68
 * @property-read string $toplevelId this holds the toplevel structure's "@ID" (METS) or the manifest's "@id" (IIIF)
69
 * @property \SimpleXMLElement $xml this holds the whole XML file as \SimpleXMLElement object
70
 * @property string $asJson this holds the manifest file as string for serialization purposes
71
 * @property ManifestInterface $iiif a PHP object representation of a IIIF manifest
72
 * @property string $iiifVersion 'IIIF1', 'IIIF2' or 'IIIF3', depending on the API $this->iiif conforms to
73
 * @property bool $hasFulltextSet flag if document has already been analyzed for presence of the fulltext for the Solr index
74
 * @property array $originalMetadataArray this holds the original manifest's parsed metadata array with their corresponding resource (Manifest / Sequence / Range) ID as array key
75
 * @property array $mimeTypes this holds the mime types of linked resources in the manifest (extracted during parsing) for later us
76
 * 
77
 */
78
final class IiifManifest extends AbstractDocument
79
{
80
    /**
81
     * @access protected
82
     * @var string This holds the manifest file as string for serialization purposes
83
     *
84
     * @see __sleep() / __wakeup()
85
     */
86
    protected string $asJson = '';
87
88
    /**
89
     * @access protected
90
     * @var ManifestInterface|null A PHP object representation of a IIIF manifest
91
     */
92
    protected ?ManifestInterface $iiif;
93
94
    /**
95
     * @access protected
96
     * @var string 'IIIF1', 'IIIF2' or 'IIIF3', depending on the API $this->iiif conforms to: IIIF Metadata API 1, IIIF Presentation API 2 or 3
97
     */
98
    protected string $iiifVersion;
99
100
    /**
101
     * @access protected
102
     * @var bool Document has already been analyzed if it contains fulltext for the Solr index
103
     */
104
    protected bool $hasFulltextSet = false;
105
106
    /**
107
     * @access protected
108
     * @var array This holds the original manifest's parsed metadata array with their corresponding resource (Manifest / Sequence / Range) ID as array key
109
     */
110
    protected array $originalMetadataArray = [];
111
112
    /**
113
     * @access protected
114
     * @var array Holds the mime types of linked resources in the manifest (extracted during parsing) for later use
115
     */
116
    protected array $mimeTypes = [];
117
118
    /**
119
     * @see AbstractDocument::establishRecordId()
120
     */
121
    protected function establishRecordId(int $pid): void
122
    {
123
        if ($this->iiif !== null) {
124
            /*
125
             *  FIXME This will not consistently work because we can not be sure to have the pid at hand. It may miss
126
             *  if the plugin that actually loads the manifest allows content from other pages.
127
             *  Up until now the cPid is only set after the document has been initialized. We need it before to
128
             *  check the configuration.
129
             *  TODO Saving / indexing should still work - check!
130
             */
131
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
132
                ->getQueryBuilderForTable('tx_dlf_metadata');
133
            // Get hidden records, too.
134
            $queryBuilder
135
                ->getRestrictions()
136
                ->removeByType(HiddenRestriction::class);
137
            $result = $queryBuilder
138
                ->select('tx_dlf_metadataformat.xpath AS querypath')
139
                ->from('tx_dlf_metadata')
140
                ->from('tx_dlf_metadataformat')
141
                ->from('tx_dlf_formats')
142
                ->where(
143
                    $queryBuilder->expr()->eq('tx_dlf_metadata.pid', (int) $pid),
144
                    $queryBuilder->expr()->eq('tx_dlf_metadataformat.pid', (int) $pid),
145
                    $queryBuilder->expr()->orX(
146
                        $queryBuilder->expr()->andX(
147
                            $queryBuilder->expr()->eq('tx_dlf_metadata.uid', 'tx_dlf_metadataformat.parent_id'),
148
                            $queryBuilder->expr()->eq('tx_dlf_metadataformat.encoded', 'tx_dlf_formats.uid'),
149
                            $queryBuilder->expr()->eq('tx_dlf_metadata.index_name', $queryBuilder->createNamedParameter('record_id')),
150
                            $queryBuilder->expr()->eq('tx_dlf_formats.type', $queryBuilder->createNamedParameter($this->getIiifVersion()))
151
                        ),
152
                        $queryBuilder->expr()->eq('tx_dlf_metadata.format', 0)
153
                    )
154
                )
155
                ->execute();
156
            while ($resArray = $result->fetchAssociative()) {
157
                $recordIdPath = $resArray['querypath'];
158
                if (!empty($recordIdPath)) {
159
                    try {
160
                        $this->recordId = $this->iiif->jsonPath($recordIdPath);
161
                    } catch (\Exception $e) {
162
                        $this->logger->warning('Could not evaluate JSONPath to get IIIF record ID');
163
                    }
164
                }
165
            }
166
            // For now, it's a hardcoded ID, not only as a fallback
167
            if (!isset($this->recordId)) {
168
                $this->recordId = $this->iiif->getId();
169
            }
170
        }
171
    }
172
173
    /**
174
     * @see AbstractDocument::getDocument()
175
     */
176
    protected function getDocument(): IiifResourceInterface
177
    {
178
        return $this->iiif;
179
    }
180
181
    /**
182
     * Returns a string representing the Metadata / Presentation API version which the IIIF resource
183
     * conforms to. This is used for example to extract metadata according to configured patterns.
184
     *
185
     * @access public
186
     *
187
     * @return string 'IIIF1' if the resource is a Metadata API 1 resource, 'IIIF2' / 'IIIF3' if
188
     * the resource is a Presentation API 2 / 3 resource
189
     */
190
    public function getIiifVersion(): string
191
    {
192
        if (!isset($this->iiifVersion)) {
193
            if ($this->iiif instanceof AbstractIiifResource1) {
194
                $this->iiifVersion = 'IIIF1';
195
            } elseif ($this->iiif instanceof AbstractIiifResource2) {
196
                $this->iiifVersion = 'IIIF2';
197
            } elseif ($this->iiif instanceof AbstractIiifResource3) {
198
                $this->iiifVersion = 'IIIF3';
199
            }
200
        }
201
        return $this->iiifVersion;
202
    }
203
204
    /**
205
     * True if getUseGroups() has been called and $this->useGrps is loaded
206
     *
207
     * @var bool
208
     * @access protected
209
     */
210
    protected bool $useGrpsLoaded = false;
211
212
    /**
213
     * Holds the configured useGrps as array.
214
     *
215
     * @var array
216
     * @access protected
217
     */
218
    protected array $useGrps = [];
219
220
    /**
221
     * IiifManifest also populates the physical structure array entries for matching
222
     * 'fileGrp's. To do that, the configuration has to be loaded; afterwards configured
223
     * 'fileGrp's for thumbnails, downloads, audio, fulltext and the 'fileGrp's for images
224
     * can be requested with this method.
225
     *
226
     * @access protected
227
     *
228
     * @param string $use
229
     *
230
     * @return array|string
231
     */
232
    protected function getUseGroups(string $use)
233
    {
234
        if (!$this->useGrpsLoaded) {
235
            // Get configured USE attributes.
236
            $extConf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get(self::$extKey);
237
            if (!empty($extConf['fileGrpImages'])) {
238
                $this->useGrps['fileGrpImages'] = GeneralUtility::trimExplode(',', $extConf['fileGrpImages']);
239
            }
240
            if (!empty($extConf['fileGrpThumbs'])) {
241
                $this->useGrps['fileGrpThumbs'] = GeneralUtility::trimExplode(',', $extConf['fileGrpThumbs']);
242
            }
243
            if (!empty($extConf['fileGrpDownload'])) {
244
                $this->useGrps['fileGrpDownload'] = GeneralUtility::trimExplode(',', $extConf['fileGrpDownload']);
245
            }
246
            if (!empty($extConf['fileGrpFulltext'])) {
247
                $this->useGrps['fileGrpFulltext'] = GeneralUtility::trimExplode(',', $extConf['fileGrpFulltext']);
248
            }
249
            if (!empty($extConf['fileGrpAudio'])) {
250
                $this->useGrps['fileGrpAudio'] = GeneralUtility::trimExplode(',', $extConf['fileGrpAudio']);
251
            }
252
            $this->useGrpsLoaded = true;
253
        }
254
        return array_key_exists($use, $this->useGrps) ? $this->useGrps[$use] : [];
255
    }
256
257
    /**
258
     * @see AbstractDocument::magicGetPhysicalStructure()
259
     */
260
    protected function magicGetPhysicalStructure(): array
261
    {
262
        // Is there no physical structure array yet?
263
        if (!$this->physicalStructureLoaded) {
264
            if ($this->iiif == null || !($this->iiif instanceof ManifestInterface)) {
265
                return [];
266
            }
267
            $extConf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get(self::$extKey);
268
            $iiifId = $this->iiif->getId();
269
            $this->physicalStructureInfo[$iiifId]['id'] = $iiifId;
270
            $this->physicalStructureInfo[$iiifId]['dmdId'] = $iiifId;
271
            $this->physicalStructureInfo[$iiifId]['label'] = $this->iiif->getLabelForDisplay();
272
            $this->physicalStructureInfo[$iiifId]['orderlabel'] = $this->iiif->getLabelForDisplay();
273
            $this->physicalStructureInfo[$iiifId]['type'] = 'physSequence';
274
            $this->physicalStructureInfo[$iiifId]['contentIds'] = null;
275
276
            $this->setFileUseDownload($iiifId, $this->iiif);
277
            $this->setFileUseFulltext($iiifId, $this->iiif);
278
279
            $fileUseThumbs = $this->getUseGroups('fileGrpThumbs');
280
            $fileUses = $this->getUseGroups('fileGrpImages');
281
282
            if (!empty($this->iiif->getDefaultCanvases())) {
283
                // canvases have not order property, but the context defines canveses as @list with a specific order, so we can provide an alternative
284
                $elements = [];
285
                $canvasOrder = 0;
286
                foreach ($this->iiif->getDefaultCanvases() as $canvas) {
287
                    $canvasOrder++;
288
                    $thumbnailUrl = $canvas->getThumbnailUrl();
289
                    // put thumbnails in thumbnail filegroup
290
                    if (
291
                        !empty($thumbnailUrl)
292
                        && empty($this->physicalStructureInfo[$iiifId]['files'][$fileUseThumbs[0]])
293
                    ) {
294
                        $this->physicalStructureInfo[$iiifId]['files'][$fileUseThumbs[0]] = $thumbnailUrl;
295
                    }
296
                    // populate structural metadata info
297
                    $elements[$canvasOrder] = $canvas->getId();
298
                    $this->physicalStructureInfo[$elements[$canvasOrder]]['id'] = $canvas->getId();
299
                    $this->physicalStructureInfo[$elements[$canvasOrder]]['dmdId'] = null;
300
                    $this->physicalStructureInfo[$elements[$canvasOrder]]['label'] = $canvas->getLabelForDisplay();
301
                    $this->physicalStructureInfo[$elements[$canvasOrder]]['orderlabel'] = $canvas->getLabelForDisplay();
302
                    // assume that a canvas always represents a page
303
                    $this->physicalStructureInfo[$elements[$canvasOrder]]['type'] = 'page';
304
                    $this->physicalStructureInfo[$elements[$canvasOrder]]['contentIds'] = null;
305
                    $this->physicalStructureInfo[$elements[$canvasOrder]]['annotationContainers'] = null;
306
                    if (!empty($canvas->getPossibleTextAnnotationContainers(Motivation::PAINTING))) {
307
                        $this->physicalStructureInfo[$elements[$canvasOrder]]['annotationContainers'] = [];
308
                        foreach ($canvas->getPossibleTextAnnotationContainers(Motivation::PAINTING) as $annotationContainer) {
309
                            $this->physicalStructureInfo[$elements[$canvasOrder]]['annotationContainers'][] = $annotationContainer->getId();
310
                            if ($extConf['indexAnnotations']) {
311
                                $this->hasFulltext = true;
312
                                $this->hasFulltextSet = true;
313
                            }
314
                        }
315
                    }
316
317
                    $this->setFileUseFulltext($elements[$canvasOrder], $canvas);
318
319
                    if (!empty($fileUses)) {
320
                        $image = $canvas->getImageAnnotations()[0];
321
                        foreach ($fileUses as $fileUse) {
322
                            if ($image->getBody() !== null && $image->getBody() instanceof ContentResourceInterface) {
323
                                $this->physicalStructureInfo[$elements[$canvasOrder]]['files'][$fileUse] = $image->getBody()->getId();
324
                            }
325
                        }
326
                    }
327
                    if (!empty($thumbnailUrl)) {
328
                        $this->physicalStructureInfo[$elements[$canvasOrder]]['files'][$fileUseThumbs] = $thumbnailUrl;
329
                    }
330
331
                    $this->setFileUseDownload($elements[$canvasOrder], $canvas);
332
                }
333
                $this->numPages = $canvasOrder;
334
                // Merge and re-index the array to get nice numeric indexes.
335
                array_unshift($elements, $iiifId);
336
                $this->physicalStructure = $elements;
337
            }
338
            $this->physicalStructureLoaded = true;
339
        }
340
        return $this->physicalStructure;
341
    }
342
343
    /**
344
     * @see AbstractDocument::getDownloadLocation()
345
     */
346
    public function getDownloadLocation(string $id): string
347
    {
348
        $fileLocation = $this->getFileLocation($id);
349
        $resource = $this->iiif->getContainedResourceById($fileLocation);
350
        if ($resource instanceof AbstractImageService) {
351
            return $resource->getImageUrl();
352
        }
353
        return $fileLocation;
354
    }
355
356
    /**
357
     * @see AbstractDocument::getFileInfo()
358
     */
359
    public function getFileInfo($id): ?array
360
    {
361
        if (empty($this->fileInfos[$id]['location'])) {
362
            $this->fileInfos[$id]['location'] = $this->getFileLocation($id);
363
        }
364
365
        if (empty($this->fileInfos[$id]['mimeType'])) {
366
            $this->fileInfos[$id]['mimeType'] = $this->getFileMimeType($id);
367
        }
368
369
        return $this->fileInfos[$id];
370
    }
371
372
    /**
373
     * @see AbstractDocument::getFileLocation()
374
     */
375
    public function getFileLocation(string $id): string
376
    {
377
        if ($id == null) {
378
            return '';
379
        }
380
        $resource = $this->iiif->getContainedResourceById($id);
381
        if (isset($resource)) {
382
            if ($resource instanceof CanvasInterface) {
383
                // TODO: Cannot call method getSingleService() on array<Ubl\Iiif\Presentation\Common\Model\Resources\AnnotationInterface>.
384
                // @phpstan-ignore-next-line
385
                return (!empty($resource->getImageAnnotations()) && $resource->getImageAnnotations()->getSingleService() != null) ? $resource->getImageAnnotations()[0]->getSingleService()->getId() : $id;
386
            } elseif ($resource instanceof ContentResourceInterface) {
387
                return $resource->getSingleService() instanceof Service ? $resource->getSingleService()->getId() : $id;
388
            } elseif ($resource instanceof AbstractImageService) {
389
                return $resource->getId();
390
            } elseif ($resource instanceof AnnotationContainerInterface) {
391
                return $id;
392
            }
393
        }
394
        return $id;
395
    }
396
397
    /**
398
     * @see AbstractDocument::getFileMimeType()
399
     */
400
    public function getFileMimeType(string $id): string
401
    {
402
        $fileResource = $this->iiif->getContainedResourceById($id);
403
        if ($fileResource instanceof CanvasInterface) {
404
            $format = "application/vnd.kitodo.iiif";
405
        } elseif ($fileResource instanceof AnnotationInterface) {
406
            $format = "application/vnd.kitodo.iiif";
407
        } elseif ($fileResource instanceof ContentResourceInterface) {
408
            if ($fileResource->isText() || $fileResource->isImage() && ($fileResource->getSingleService() == null || !($fileResource->getSingleService() instanceof AbstractImageService))) {
409
                // Support static images without an image service
410
                return $fileResource->getFormat();
411
            }
412
            $format = "application/vnd.kitodo.iiif";
413
        } elseif ($fileResource instanceof AbstractImageService) {
414
            $format = "application/vnd.kitodo.iiif";
415
        } else {
416
            // Assumptions: this can only be the thumbnail and the thumbnail is a jpeg - TODO determine mimetype
417
            $format = "image/jpeg";
418
        }
419
        return $format;
420
    }
421
422
    /**
423
     * @see AbstractDocument::getLogicalStructure()
424
     */
425
    public function getLogicalStructure(string $id, bool $recursive = false): array
426
    {
427
        $details = [];
428
        if (!$recursive && !empty($this->logicalUnits[$id])) {
429
            return $this->logicalUnits[$id];
430
        } elseif (!empty($id)) {
431
            $logUnits[] = $this->iiif->getContainedResourceById($id);
432
        } else {
433
            $logUnits[] = $this->iiif;
434
        }
435
        // TODO: Variable $logUnits in empty() always exists and is not falsy.
436
        // @phpstan-ignore-next-line
437
        if (!empty($logUnits)) {
438
            if (!$recursive) {
439
                $details = $this->getLogicalStructureInfo($logUnits[0]);
440
            } else {
441
                // cache the ranges - they might occur multiple times in the structures "tree" - with full data as well as referenced as id
442
                $processedStructures = [];
443
                foreach ($logUnits as $logUnit) {
444
                    if (array_search($logUnit->getId(), $processedStructures) == false) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing array_search($logUnit->g..., $processedStructures) of type integer|string against false; this is ambiguous if the integer can be zero. Consider using a strict comparison === instead.
Loading history...
445
                        $this->tableOfContents[] = $this->getLogicalStructureInfo($logUnit, true, $processedStructures);
446
                    }
447
                }
448
            }
449
        }
450
        return $details;
451
    }
452
453
    /**
454
     * Get the details about a IIIF resource (manifest or range) in the logical structure
455
     *
456
     * @access protected
457
     *
458
     * @param IiifResourceInterface $resource IIIF resource, either a manifest or range.
459
     * @param bool $recursive Whether to include the child elements
460
     * @param array $processedStructures IIIF resources that already have been processed
461
     *
462
     * @return array Logical structure array
463
     */
464
    protected function getLogicalStructureInfo(IiifResourceInterface $resource, bool $recursive = false, array &$processedStructures = []): array
465
    {
466
        $details = [];
467
        $details['id'] = $resource->getId();
468
        $details['dmdId'] = '';
469
        $details['label'] = $resource->getLabelForDisplay() ?? '';
470
        $details['orderlabel'] = $resource->getLabelForDisplay() ?? '';
471
        $details['contentIds'] = '';
472
        $details['volume'] = '';
473
        $details['pagination'] = '';
474
        $cPid = ($this->cPid ? $this->cPid : $this->pid);
475
        if ($details['id'] == $this->magicGetToplevelId()) {
476
            $metadata = $this->getMetadata($details['id'], $cPid);
477
            if (!empty($metadata['type'][0])) {
478
                $details['type'] = $metadata['type'][0];
479
            }
480
        }
481
        $details['thumbnailId'] = $resource->getThumbnailUrl();
482
        $details['points'] = '';
483
        // Load structural mapping
484
        $this->magicGetSmLinks();
485
        // Load physical structure.
486
        $this->magicGetPhysicalStructure();
487
488
        if ($resource instanceof ManifestInterface || $resource instanceof RangeInterface) {
489
            $startCanvas = $resource->getStartCanvasOrFirstCanvas();
490
        }
491
        if (isset($startCanvas)) {
492
            $details['pagination'] = $startCanvas->getLabel();
493
            $startCanvasIndex = array_search($startCanvas, $this->iiif->getDefaultCanvases());
494
            if ($startCanvasIndex !== false) {
495
                $details['points'] = $startCanvasIndex + 1;
496
            }
497
        }
498
        $useGroups = $this->getUseGroups('fileGrpImages');
499
        if (is_string($useGroups)) {
500
            $useGroups = [$useGroups];
501
        }
502
        // Keep for later usage.
503
        $this->logicalUnits[$details['id']] = $details;
504
        // Walk the structure recursively? And are there any children of the current element?
505
        if ($recursive) {
506
            $processedStructures[] = $resource->getId();
507
            $details['children'] = [];
508
            if ($resource instanceof ManifestInterface && $resource->getRootRanges() != null) {
509
                $rangesToAdd = [];
510
                $rootRanges = [];
511
                if (count($this->iiif->getRootRanges()) == 1 && $this->iiif->getRootRanges()[0]->isTopRange()) {
512
                    $rangesToAdd = $this->iiif->getRootRanges()[0]->getMemberRangesAndRanges();
513
                } else {
514
                    $rangesToAdd = $this->iiif->getRootRanges();
515
                }
516
                foreach ($rangesToAdd as $range) {
517
                    $rootRanges[] = $range;
518
                }
519
                foreach ($rootRanges as $range) {
520
                    if ((array_search($range->getId(), $processedStructures) == false)) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing array_search($range->get..., $processedStructures) of type integer|string against false; this is ambiguous if the integer can be zero. Consider using a strict comparison === instead.
Loading history...
521
                        $details['children'][] = $this->getLogicalStructureInfo($range, true, $processedStructures);
522
                    }
523
                }
524
            } elseif ($resource instanceof RangeInterface) {
525
                if (!empty($resource->getAllRanges())) {
526
                    foreach ($resource->getAllRanges() as $range) {
527
                        if ((array_search($range->getId(), $processedStructures) == false)) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing array_search($range->get..., $processedStructures) of type integer|string against false; this is ambiguous if the integer can be zero. Consider using a strict comparison === instead.
Loading history...
528
                            $details['children'][] = $this->getLogicalStructureInfo($range, true, $processedStructures);
529
                        }
530
                    }
531
                }
532
            }
533
        }
534
        return $details;
535
    }
536
537
    /**
538
     * Returns metadata for IIIF resources with the ID $id in there original form in
539
     * the manifest, but prepared for display to the user.
540
     *
541
     * @access public
542
     *
543
     * @param string $id the ID of the IIIF resource
544
     * @param bool $withDescription add description / summary to the return value
545
     * @param bool $withRights add attribution and license / rights and requiredStatement to the return value
546
     * @param bool $withRelated add related links / homepage to the return value
547
     *
548
     * @return array
549
     *
550
     * @todo This method is still in experimental; the method signature may change.
551
     */
552
    public function getManifestMetadata(string $id, bool $withDescription = true, bool $withRights = true, bool $withRelated = true): array
553
    {
554
        if (!empty($this->originalMetadataArray[$id])) {
555
            return $this->originalMetadataArray[$id];
556
        }
557
        $iiifResource = $this->iiif->getContainedResourceById($id);
558
        $result = [];
559
        if ($iiifResource != null) {
560
            if (!empty($iiifResource->getLabel())) {
561
                $result['label'] = $iiifResource->getLabel();
562
            }
563
            if (!empty($iiifResource->getMetadata())) {
564
                $result['metadata'] = [];
565
                foreach ($iiifResource->getMetadataForDisplay() as $metadata) {
566
                    $result['metadata'][$metadata['label']] = $metadata['value'];
567
                }
568
            }
569
            if ($withDescription && !empty($iiifResource->getSummary())) {
570
                $result["description"] = $iiifResource->getSummaryForDisplay();
571
            }
572
            if ($withRights) {
573
                if (!empty($iiifResource->getRights())) {
574
                    $result["rights"] = $iiifResource->getRights();
575
                }
576
                if (!empty($iiifResource->getRequiredStatement())) {
577
                    $result["requiredStatement"] = $iiifResource->getRequiredStatementForDisplay();
578
                }
579
            }
580
            if ($withRelated && !empty($iiifResource->getWeblinksForDisplay())) {
581
                $result["weblinks"] = [];
582
                foreach ($iiifResource->getWeblinksForDisplay() as $link) {
583
                    $key = array_key_exists("label", $link) ? $link["label"] : $link["@id"];
584
                    $result["weblinks"][$key] = $link["@id"];
585
                }
586
            }
587
        }
588
        return $result;
589
    }
590
591
    /**
592
     * @see AbstractDocument::getMetadata()
593
     */
594
    public function getMetadata(string $id, int $cPid = 0): array
595
    {
596
        if (!empty($this->metadataArray[$id]) && $this->metadataArray[0] == $cPid) {
597
            return $this->metadataArray[$id];
598
        }
599
600
        $metadata = $this->initializeMetadata('IIIF');
601
602
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
603
            ->getQueryBuilderForTable('tx_dlf_metadata');
604
        // Get hidden records, too.
605
        $queryBuilder
606
            ->getRestrictions()
607
            ->removeByType(HiddenRestriction::class);
608
        $result = $queryBuilder
609
            ->select(
610
                'tx_dlf_metadata.index_name AS index_name',
611
                'tx_dlf_metadataformat.xpath AS xpath',
612
                'tx_dlf_metadataformat.xpath_sorting AS xpath_sorting',
613
                'tx_dlf_metadata.is_sortable AS is_sortable',
614
                'tx_dlf_metadata.default_value AS default_value',
615
                'tx_dlf_metadata.format AS format'
616
            )
617
            ->from('tx_dlf_metadata')
618
            ->from('tx_dlf_metadataformat')
619
            ->from('tx_dlf_formats')
620
            ->where(
621
                $queryBuilder->expr()->eq('tx_dlf_metadata.pid', (int) $cPid),
622
                $queryBuilder->expr()->eq('tx_dlf_metadataformat.pid', (int) $cPid),
623
                $queryBuilder->expr()->orX(
624
                    $queryBuilder->expr()->andX(
625
                        $queryBuilder->expr()->eq('tx_dlf_metadata.uid', 'tx_dlf_metadataformat.parent_id'),
626
                        $queryBuilder->expr()->eq('tx_dlf_metadataformat.encoded', 'tx_dlf_formats.uid'),
627
                        $queryBuilder->expr()->eq('tx_dlf_formats.type', $queryBuilder->createNamedParameter($this->getIiifVersion()))
628
                    ),
629
                    $queryBuilder->expr()->eq('tx_dlf_metadata.format', 0)
630
                )
631
            )
632
            ->execute();
633
        $iiifResource = $this->iiif->getContainedResourceById($id);
634
        while ($resArray = $result->fetchAssociative()) {
635
            // Set metadata field's value(s).
636
            if ($resArray['format'] > 0 && !empty($resArray['xpath'])) {
637
                $values = $iiifResource->jsonPath($resArray['xpath']);
638
                if (is_string($values)) {
639
                    $metadata[$resArray['index_name']] = [trim((string) $values)];
640
                } elseif ($values instanceof JSONPath && is_array($values->data()) && count($values->data()) > 1) {
641
                    $metadata[$resArray['index_name']] = [];
642
                    foreach ($values->data() as $value) {
643
                        $metadata[$resArray['index_name']][] = trim((string) $value);
644
                    }
645
                }
646
            }
647
            // Set default value if applicable.
648
            if (empty($metadata[$resArray['index_name']][0]) && strlen($resArray['default_value']) > 0) {
649
                $metadata[$resArray['index_name']] = [$resArray['default_value']];
650
            }
651
            // Set sorting value if applicable.
652
            if (!empty($metadata[$resArray['index_name']]) && $resArray['is_sortable']) {
653
                if ($resArray['format'] > 0 && !empty($resArray['xpath_sorting'])) {
654
                    $values = $iiifResource->jsonPath($resArray['xpath_sorting']);
655
                    if (is_string($values)) {
656
                        $metadata[$resArray['index_name'] . '_sorting'][0] = [trim((string) $values)];
657
                    } elseif ($values instanceof JSONPath && is_array($values->data()) && count($values->data()) > 1) {
658
                        $metadata[$resArray['index_name']] = [];
659
                        foreach ($values->data() as $value) {
660
                            $metadata[$resArray['index_name'] . '_sorting'][0] = trim((string) $value);
661
                        }
662
                    }
663
                }
664
                if (empty($metadata[$resArray['index_name'] . '_sorting'][0])) {
665
                    $metadata[$resArray['index_name'] . '_sorting'][0] = $metadata[$resArray['index_name']][0];
666
                }
667
            }
668
        }
669
        // Set date to empty string if not present.
670
        if (empty($metadata['date'][0])) {
671
            $metadata['date'][0] = '';
672
        }
673
        return $metadata;
674
    }
675
676
    /**
677
     * @see AbstractDocument::magicGetSmLinks()
678
     */
679
    protected function magicGetSmLinks(): array
680
    {
681
        if (!$this->smLinksLoaded && isset($this->iiif) && $this->iiif instanceof ManifestInterface) {
682
            if (!empty($this->iiif->getDefaultCanvases())) {
683
                foreach ($this->iiif->getDefaultCanvases() as $canvas) {
684
                    $this->smLinkCanvasToResource($canvas, $this->iiif);
685
                }
686
            }
687
            if (!empty($this->iiif->getStructures())) {
688
                foreach ($this->iiif->getStructures() as $range) {
689
                    $this->smLinkRangeCanvasesRecursively($range);
690
                }
691
            }
692
            $this->smLinksLoaded = true;
693
        }
694
        return $this->smLinks;
695
    }
696
697
    /**
698
     * Construct a link between a range and it's sub ranges and all contained canvases.
699
     *
700
     * @access private
701
     *
702
     * @param RangeInterface $range Current range whose canvases shall be linked
703
     * 
704
     * @return void
705
     */
706
    private function smLinkRangeCanvasesRecursively(RangeInterface $range): void
707
    {
708
        // map range's canvases including all child ranges' canvases
709
        if (!$range->isTopRange()) {
710
            foreach ($range->getAllCanvasesRecursively() as $canvas) {
711
                $this->smLinkCanvasToResource($canvas, $range);
712
            }
713
        }
714
        // recursive call for all ranges
715
        if (!empty($range->getAllRanges())) {
716
            foreach ($range->getAllRanges() as $childRange) {
717
                $this->smLinkRangeCanvasesRecursively($childRange);
718
            }
719
        }
720
    }
721
722
    /**
723
     * Link a single canvas to a containing range
724
     *
725
     * @access private
726
     *
727
     * @param CanvasInterface $canvas
728
     * @param IiifResourceInterface $resource
729
     * 
730
     * @return void
731
     */
732
    private function smLinkCanvasToResource(CanvasInterface $canvas, IiifResourceInterface $resource): void
733
    {
734
        $this->smLinks['l2p'][$resource->getId()][] = $canvas->getId();
735
        if (!is_array($this->smLinks['p2l'][$canvas->getId()]) || !in_array($resource->getId(), $this->smLinks['p2l'][$canvas->getId()])) {
736
            $this->smLinks['p2l'][$canvas->getId()][] = $resource->getId();
737
        }
738
    }
739
740
    /**
741
     * @see AbstractDocument::getFullText()
742
     */
743
    //TODO: rewrite it to get full OCR
744
    public function getFullText(string $id): string
745
    {
746
        $rawText = '';
747
        // Get text from raw text array if available.
748
        if (!empty($this->rawTextArray[$id])) {
749
            return $this->rawTextArray[$id];
750
        }
751
        $this->ensureHasFulltextIsSet();
752
        if ($this->hasFulltext) {
753
            // Load physical structure ...
754
            $this->magicGetPhysicalStructure();
755
            // ... and extension configuration.
756
            $extConf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get(self::$extKey);
757
            $fileGrpsFulltext = GeneralUtility::trimExplode(',', $extConf['fileGrpFulltext']);
758
            if (!empty($this->physicalStructureInfo[$id])) {
759
                while ($fileGrpFulltext = array_shift($fileGrpsFulltext)) {
760
                    if (!empty($this->physicalStructureInfo[$id]['files'][$fileGrpFulltext])) {
761
                        $rawText = parent::getFullTextFromXml($id);
762
                        break;
763
                    }
764
                }
765
                if ($extConf['indexAnnotations'] == 1) {
766
                    $iiifResource = $this->iiif->getContainedResourceById($id);
767
                    // Get annotation containers
768
                    $annotationContainerIds = $this->physicalStructureInfo[$id]['annotationContainers'];
769
                    if (!empty($annotationContainerIds)) {
770
                        $annotationTexts = $this->getAnnotationTexts($annotationContainerIds, $iiifResource->getId());
771
                        $rawText .= implode(' ', $annotationTexts);
772
                    }
773
                }
774
            } else {
775
                $this->logger->warning('Invalid structure resource @id "' . $id . '"');
776
                return $rawText;
777
            }
778
            $this->rawTextArray[$id] = $rawText;
779
        }
780
        return $rawText;
781
    }
782
783
    /**
784
     * Returns the underlying IiifResourceInterface.
785
     *
786
     * @access public
787
     *
788
     * @return IiifResourceInterface
789
     */
790
    public function getIiif(): IiifResourceInterface
791
    {
792
        return $this->iiif;
793
    }
794
795
    /**
796
     * @see AbstractDocument::init()
797
     */
798
    protected function init(string $location, array $settings = []): void
799
    {
800
        $this->logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(static::class);
801
    }
802
803
    /**
804
     * @see AbstractDocument::loadLocation()
805
     */
806
    protected function loadLocation(string $location): bool
807
    {
808
        $fileResource = GeneralUtility::getUrl($location);
809
        if ($fileResource !== false) {
810
            $conf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get(self::$extKey);
811
            IiifHelper::setUrlReader(IiifUrlReader::getInstance());
812
            IiifHelper::setMaxThumbnailHeight($conf['iiifThumbnailHeight']);
813
            IiifHelper::setMaxThumbnailWidth($conf['iiifThumbnailWidth']);
814
            $resource = IiifHelper::loadIiifResource($fileResource);
815
            if ($resource instanceof ManifestInterface) {
816
                $this->iiif = $resource;
817
                return true;
818
            }
819
        }
820
        $this->logger->error('Could not load IIIF manifest from "' . $location . '"');
821
        return false;
822
    }
823
824
    /**
825
     * @see AbstractDocument::prepareMetadataArray()
826
     */
827
    protected function prepareMetadataArray(int $cPid): void
828
    {
829
        $id = $this->iiif->getId();
830
        $this->metadataArray[(string) $id] = $this->getMetadata((string) $id, $cPid);
831
    }
832
833
    /**
834
     * @see AbstractDocument::setPreloadedDocument()
835
     */
836
    protected function setPreloadedDocument($preloadedDocument): bool
837
    {
838
        if ($preloadedDocument instanceof ManifestInterface) {
839
            $this->iiif = $preloadedDocument;
840
            return true;
841
        }
842
        return false;
843
    }
844
845
    /**
846
     * @see AbstractDocument::ensureHasFulltextIsSet()
847
     */
848
    protected function ensureHasFulltextIsSet(): void
849
    {
850
        /*
851
         *  TODO Check annotations and annotation lists of canvas for ALTO documents.
852
         *  Example:
853
         *  https://digi.ub.uni-heidelberg.de/diglit/iiif/hirsch_hamburg1933_04_25/manifest.json links
854
         *  https://digi.ub.uni-heidelberg.de/diglit/iiif/hirsch_hamburg1933_04_25/list/0001.json
855
         */
856
        if (!$this->hasFulltextSet && $this->iiif instanceof ManifestInterface) {
857
            $manifest = $this->iiif;
858
            $canvases = $manifest->getDefaultCanvases();
859
            foreach ($canvases as $canvas) {
860
                if (
861
                    !empty($canvas->getSeeAlsoUrlsForFormat("application/alto+xml")) ||
862
                    !empty($canvas->getSeeAlsoUrlsForProfile("http://www.loc.gov/standards/alto/"))
863
                ) {
864
                    $this->hasFulltextSet = true;
865
                    $this->hasFulltext = true;
866
                    return;
867
                }
868
                $extConf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get(self::$extKey);
869
                if ($extConf['indexAnnotations'] == 1 && !empty($canvas->getPossibleTextAnnotationContainers(Motivation::PAINTING))) {
870
                    foreach ($canvas->getPossibleTextAnnotationContainers(Motivation::PAINTING) as $annotationContainer) {
871
                        $textAnnotations = $annotationContainer->getTextAnnotations(Motivation::PAINTING);
872
                        if ($textAnnotations != null) {
873
                            foreach ($textAnnotations as $annotation) {
874
                                if (
875
                                    $annotation->getBody() != null &&
876
                                    $annotation->getBody()->getFormat() == "text/plain" &&
877
                                    $annotation->getBody()->getChars() != null
878
                                ) {
879
                                    $this->hasFulltextSet = true;
880
                                    $this->hasFulltext = true;
881
                                    return;
882
                                }
883
                            }
884
                        }
885
                    }
886
                }
887
            }
888
            $this->hasFulltextSet = true;
889
        }
890
    }
891
892
    /**
893
     * @see AbstractDocument::magicGetThumbnail()
894
     */
895
    protected function magicGetThumbnail(bool $forceReload = false): string
896
    {
897
        return $this->iiif->getThumbnailUrl();
898
    }
899
900
    /**
901
     * @see AbstractDocument::magicGetToplevelId()
902
     */
903
    protected function magicGetToplevelId(): string
904
    {
905
        if (empty($this->toplevelId)) {
906
            if (isset($this->iiif)) {
907
                $this->toplevelId = $this->iiif->getId();
908
            }
909
        }
910
        return $this->toplevelId;
911
    }
912
913
    /**
914
     * Get annotation texts.
915
     *
916
     * @access private
917
     *
918
     * @param array $annotationContainerIds
919
     * @param string $iiifId
920
     *
921
     * @return array
922
     */
923
    private function getAnnotationTexts($annotationContainerIds, $iiifId): array
924
    {
925
        $annotationTexts = [];
926
        foreach ($annotationContainerIds as $annotationListId) {
927
            $annotationContainer = $this->iiif->getContainedResourceById($annotationListId);
928
            /* @var $annotationContainer \Ubl\Iiif\Presentation\Common\Model\Resources\AnnotationContainerInterface */
929
            foreach ($annotationContainer->getTextAnnotations(Motivation::PAINTING) as $annotation) {
930
                if (
931
                    $annotation->getTargetResourceId() == $iiifId &&
932
                    $annotation->getBody() != null && $annotation->getBody()->getChars() != null
933
                ) {
934
                    $annotationTexts[] = $annotation->getBody()->getChars();
935
                }
936
            }
937
        }
938
        return $annotationTexts;
939
    }
940
941
    /**
942
     * Set files used for download (PDF).
943
     *
944
     * @access private
945
     *
946
     * @param string $iiifId
947
     * @param IiifResourceInterface $iiif
948
     *
949
     * @return void
950
     */
951
    private function setFileUseDownload(string $iiifId, $iiif): void
952
    {
953
        $fileUseDownload = $this->getUseGroups('fileGrpDownload');
954
955
        if (!empty($fileUseDownload)) {
956
            $docPdfRendering = $iiif->getRenderingUrlsForFormat('application/pdf');
957
            if (!empty($docPdfRendering)) {
958
                $this->physicalStructureInfo[$iiifId]['files'][$fileUseDownload[0]] = $docPdfRendering[0];
959
            }
960
        }
961
    }
962
963
    /**
964
     * Set files used for full text (ALTO).
965
     *
966
     * @access private
967
     *
968
     * @param string $iiifId
969
     * @param IiifResourceInterface $iiif
970
     *
971
     * @return void
972
     */
973
    private function setFileUseFulltext(string $iiifId, $iiif): void
974
    {
975
        $fileUseFulltext = $this->getUseGroups('fileGrpFulltext');
976
977
        if (!empty($fileUseFulltext)) {
978
            $alto = $iiif->getSeeAlsoUrlsForFormat('application/alto+xml');
979
            if (empty($alto)) {
980
                $alto = $iiif->getSeeAlsoUrlsForProfile('http://www.loc.gov/standards/alto/', true);
981
            }
982
            if (!empty($alto)) {
983
                $this->mimeTypes[$alto[0]] = 'application/alto+xml';
984
                $this->physicalStructureInfo[$iiifId]['files'][$fileUseFulltext[0]] = $alto[0];
985
                $this->hasFulltext = true;
986
                $this->hasFulltextSet = true;
987
            }
988
        }
989
    }
990
991
    /**
992
     * This magic method is executed after the object is deserialized
993
     * @see __sleep()
994
     *
995
     * @access public
996
     *
997
     * @return void
998
     */
999
    public function __wakeup(): void
1000
    {
1001
        $conf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get(self::$extKey);
1002
        IiifHelper::setUrlReader(IiifUrlReader::getInstance());
1003
        IiifHelper::setMaxThumbnailHeight($conf['iiifThumbnailHeight']);
1004
        IiifHelper::setMaxThumbnailWidth($conf['iiifThumbnailWidth']);
1005
        $resource = IiifHelper::loadIiifResource($this->asJson);
1006
        if ($resource instanceof ManifestInterface) {
1007
            $this->asJson = '';
1008
            $this->iiif = $resource;
1009
            $this->init('');
1010
        } else {
1011
            $this->logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(static::class);
1012
            $this->logger->error('Could not load IIIF after deserialization');
1013
        }
1014
    }
1015
1016
    /**
1017
     * @access public
1018
     *
1019
     * @return string[]
1020
     */
1021
    public function __sleep(): array
1022
    {
1023
        // TODO implement serialization in IIIF library
1024
        $jsonArray = $this->iiif->getOriginalJsonArray();
1025
        $this->asJson = json_encode($jsonArray);
1026
        return ['uid', 'pid', 'recordId', 'parentId', 'asJson'];
1027
    }
1028
}
1029