diff --git a/.github/codeql.yml b/.github/codeql.yml
index 4e2fa3a6da..57294b5c18 100644
--- a/.github/codeql.yml
+++ b/.github/codeql.yml
@@ -9,8 +9,11 @@ query-filters:
- note
paths-ignore:
+ - Resources/Public/JavaScript/Gridstack
- Resources/Public/JavaScript/jPlayer
- Resources/Public/JavaScript/jQuery
- Resources/Public/JavaScript/jQueryUI
- Resources/Public/JavaScript/OpenLayers
- Resources/Public/JavaScript/Toastify
+ - Resources/Public/JavaScript/Verovio
+ - Resources/Public/JavaScript/WildWebMidi
diff --git a/.gitignore b/.gitignore
index e90ff9790e..9153c9038f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+*.bak
/.buildpath
/.cache
/.idea/
@@ -16,3 +17,4 @@
/public/
/var/
/vendor/
+.DS_Store
diff --git a/Classes/Command/IndexCommand.php b/Classes/Command/IndexCommand.php
index 1a392e2e10..64d9b0a016 100644
--- a/Classes/Command/IndexCommand.php
+++ b/Classes/Command/IndexCommand.php
@@ -160,7 +160,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
if ($document === null) {
$io->error('ERROR: Document with UID "' . $input->getOption('doc') . '" could not be found on PID ' . $this->storagePid . ' .');
- exit(1);
+ return BaseCommand::FAILURE;
} else {
$doc = AbstractDocument::getInstance($document->getLocation(), ['storagePid' => $this->storagePid], true);
}
diff --git a/Classes/Common/AnnotationRequest.php b/Classes/Common/AnnotationRequest.php
new file mode 100644
index 0000000000..3d7792a7bf
--- /dev/null
+++ b/Classes/Common/AnnotationRequest.php
@@ -0,0 +1,78 @@
+
+ *
+ * This file is part of the Kitodo and TYPO3 projects.
+ *
+ * @license GNU General Public License version 3 or later.
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ */
+
+class AnnotationRequest
+{
+ /**
+ * @var string
+ */
+ protected $apiUrl = '';
+
+ /**
+ * @param string $apiUrl The url of the annotation server api.
+ */
+ public function __construct($apiUrl)
+ {
+ $this->apiUrl = trim($apiUrl, "/ ");
+ }
+
+
+ /**
+ * Requests the annotation server
+ *
+ * @param string $url The annotation request url.
+ * @return array
+ */
+ protected function requestAnnotions($url) : array
+ {
+ $jsonld = Helper::getUrl($url);
+
+ if ($jsonld) {
+ $annotationData = json_decode($jsonld, true);
+
+ if ($annotationData) {
+ return $annotationData;
+ }
+ }
+
+ return [];
+ }
+
+ /**
+ * Returns all annotations of a document.
+ *
+ * @param string $id Document id (purl)
+ * @return array
+ */
+ public function getAll($id)
+ {
+ $annotations = [];
+
+ $annotationData = $this->requestAnnotions($this->apiUrl . '?target=' . urlencode($id . '/*'));
+
+ if (array_key_exists('first', $annotationData)) {
+ $annotationPageData = $annotationData['first'];
+ $annotations = array_merge($annotations, $annotationPageData["items"]);
+
+ while (array_key_exists('next', $annotationPageData)) {
+ $annotationPageData = $this->requestAnnotions($annotationPageData['next']);
+ if (array_key_exists('items', $annotationPageData)) {
+ $annotations = array_merge($annotations, $annotationPageData["items"]);
+ }
+ }
+ }
+
+ return $annotations;
+ }
+}
diff --git a/Classes/Common/DocumentAnnotation.php b/Classes/Common/DocumentAnnotation.php
new file mode 100644
index 0000000000..1b4e99e0f5
--- /dev/null
+++ b/Classes/Common/DocumentAnnotation.php
@@ -0,0 +1,404 @@
+
+ *
+ * This file is part of the Kitodo and TYPO3 projects.
+ *
+ * @license GNU General Public License version 3 or later.
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ */
+
+use DateTime;
+use Kitodo\Dlf\Domain\Model\Annotation;
+use Kitodo\Dlf\Domain\Model\Document;
+use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
+use TYPO3\CMS\Core\Log\LogManager;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Log\Logger;
+
+/**
+ * Implementation for displaying annotations from an annotation server to a document
+ *
+ * @package TYPO3
+ * @subpackage dlf
+ *
+ * @access public
+ */
+class DocumentAnnotation
+{
+ /**
+ * @var null|DocumentAnnotation
+ */
+ private static $instance;
+
+ /**
+ * @var array
+ */
+ protected $annotationData;
+
+ /**
+ * @var Document
+ */
+ protected $document;
+
+ /**
+ * @access protected
+ * @var Logger This holds the logger
+ */
+ protected Logger $logger;
+
+ /**
+ * @param array $annotationData
+ * @param Document $document
+ */
+ private function __construct($annotationData, $document)
+ {
+ $this->annotationData = $annotationData;
+ $this->document = $document;
+ $this->logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(static::class);
+ }
+
+ /**
+ * Returns all annotations with valid targets.
+ *
+ * @return Annotation[]|array
+ */
+ public function getAnnotations()
+ {
+ if (empty($this->annotationData)) {
+ return [];
+ }
+ $annotations = [];
+ foreach ($this->annotationData as $item) {
+ $annotation = new Annotation($item);
+ $annotationTargets = $annotation->getTargets();
+ $targetPages = [];
+ foreach ($annotationTargets as $annotationTarget) {
+ if ($annotationTarget->isValid()) {
+ if ($annotationTarget->getId()) {
+ if ($this->document->getCurrentDocument()->getFileLocation($annotationTarget->getId())) {
+ if ($this->document->getCurrentDocument() instanceof MetsDocument) {
+ if (
+ $meiTargetPages = $this->getMeasurePagesByFileId(
+ $annotationTarget->getId(), $annotationTarget->getRangeValue()
+ )
+ ) {
+ $targetPages[] = [
+ 'target' => $annotationTarget,
+ 'pages' => $meiTargetPages,
+ 'verovioRelevant' => true
+ ];
+ } elseif (
+ $audioTargetPages = $this->getAudioPagesByFileId(
+ $annotationTarget->getId(), $annotationTarget->getRangeValue()
+ )
+ ) {
+ $targetPages[] = [
+ 'target' => $annotationTarget,
+ 'pages' => $audioTargetPages
+ ];
+ } elseif ($fileIdTargetPages = $this->getPagesByFileId($annotationTarget->getId())) {
+ $targetPages[] = [
+ 'target' => $annotationTarget,
+ 'pages' => $fileIdTargetPages
+ ];
+ } else {
+ $this->logger->warning(
+ ' No target pages found! Annotation: "' . $annotation->getId() . '", '
+ . 'Target: "' . $annotationTarget->getUrl() . '"'
+ );
+ }
+ }
+ } elseif ($logicalTargetPages = $this->getPagesByLogicalId($annotationTarget->getId())) {
+ $targetPages[] = [
+ 'target' => $annotationTarget,
+ 'pages' => $logicalTargetPages
+ ];
+ } elseif ($physicalTargetPages = $this->getPagesByPhysicalId($annotationTarget->getId())) {
+ $targetPages[] = [
+ 'target' => $annotationTarget,
+ 'pages' => $physicalTargetPages
+ ];
+ } else {
+ $this->logger->warning(
+ ' No target pages found! Annotation: "' . $annotation->getId() . '", '
+ . 'Target: "' . $annotationTarget->getUrl() . '"'
+ );
+ }
+ } elseif ($annotationTarget->getObjectId()) {
+ $objectTargetPages = [];
+ foreach ($this->document->getCurrentDocument()->physicalStructureInfo as $physInfo) {
+ $order = $physInfo['order'];
+ if ($order) {
+ $objectTargetPages[] = $order;
+ }
+ }
+ if ($objectTargetPages) {
+ $targetPages[] = [
+ 'target' => $annotationTarget,
+ 'pages' => $objectTargetPages
+ ];
+ }
+ } else {
+ $this->logger->warning(
+ ' No target pages found! Annotation: "' . $annotation->getId() . '", '
+ . 'Target: "' . $annotationTarget->getUrl() . '"'
+ );
+ }
+ } else {
+ $this->logger->warning(
+ 'Invalid target! Annotation: "' . $annotation->getId() . '", '
+ . 'Target: "' . $annotationTarget->getUrl() . '"'
+ );
+ }
+ }
+ $annotation->setTargetPages($targetPages);
+ $annotations[] = $annotation;
+ }
+ return $annotations;
+ }
+
+ /**
+ * Gets the logicalId related page numbers
+ *
+ * @param string $logicalId
+ * @return array
+ */
+ protected function getPagesByLogicalId($logicalId)
+ {
+ $pages = [];
+ if (
+ array_key_exists('l2p', $this->document->getCurrentDocument()->smLinks) &&
+ array_key_exists($logicalId, $this->document->getCurrentDocument()->smLinks['l2p'])
+ ) {
+ $physicalIdentifiers = $this->document->getCurrentDocument()->smLinks['l2p'][$logicalId];
+ foreach ($physicalIdentifiers as $physicalIdentifier) {
+ if (array_key_exists($physicalIdentifier, $this->document->getCurrentDocument()->physicalStructureInfo)) {
+ $order = $this->document->getCurrentDocument()->physicalStructureInfo[$physicalIdentifier]['order'];
+ if (is_numeric($order)) {
+ $pages[] = $order;
+ }
+ }
+ }
+ }
+ return $pages;
+ }
+
+ /**
+ * Gets the physicalId related page numbers
+ * @param string $physicalId
+ * @return array
+ */
+ protected function getPagesByPhysicalId($physicalId)
+ {
+ $pages = [];
+ foreach ($this->document->getCurrentDocument()->physicalStructureInfo as $physicalInfo) {
+ $order = $physicalInfo['order'];
+ if (is_numeric($order)) {
+ $pages[] = $order;
+ }
+ }
+ if (array_key_exists($physicalId, $this->document->getCurrentDocument()->physicalStructureInfo)) {
+ if ($this->document->getCurrentDocument()->physicalStructureInfo[$physicalId]['type'] === 'physSequence') {
+ return $pages;
+ }
+ return [$this->document->getCurrentDocument()->physicalStructureInfo[$physicalId]['order']];
+ }
+ return [];
+ }
+
+ /**
+ * Gets the fileId related page numbers
+ *
+ * @param string $fileId
+ * @return array
+ */
+ protected function getPagesByFileId($fileId)
+ {
+ $pages = [];
+ foreach ($this->document->getCurrentDocument()->physicalStructureInfo as $physicalInfo) {
+ if (
+ array_key_exists('files', $physicalInfo) &&
+ is_array($physicalInfo['files']) &&
+ $physicalInfo['type'] !== 'physSequence'
+ ) {
+ foreach ($physicalInfo['files'] as $file) {
+ if ($file === $fileId) {
+ $pages[] = $physicalInfo['order'];
+ }
+ }
+ }
+ }
+ return $pages;
+ }
+
+ /**
+ * Gets the fileId and audio related page numbers
+ *
+ * @param string $fileId
+ * @param string $range
+ * @return array
+ */
+ protected function getAudioPagesByFileId($fileId, $range = null)
+ {
+ $tracks = [];
+ foreach ($this->document->getCurrentDocument()->physicalStructureInfo as $physicalInfo) {
+ if (array_key_exists('tracks', $physicalInfo) && is_array($physicalInfo['tracks'])) {
+ foreach ($physicalInfo['tracks'] as $track) {
+ if ($track['fileid'] === $fileId && $track['betype'] === 'TIME') {
+ $track['order'] = $physicalInfo['order'];
+ $tracks[] = $track;
+ }
+ }
+ }
+ }
+ if ($tracks && $range) {
+ list($from, $to) = array_map('trim', explode(',', $range));
+ $from = sprintf('%02.6f', (empty($from) ? "0" : $from));
+ $intervalFrom = \DateTime::createFromFormat('U.u', $from);
+ if (empty($to)) {
+ $intervalTo = null;
+ } else {
+ $to = sprintf('%02.6f', $to);
+ $intervalTo = \DateTime::createFromFormat('U.u', $to);
+ }
+ foreach ($tracks as $index => $track) {
+ $begin = new DateTime("1970-01-01 " . $track['begin']);
+ $extent = new DateTime("1970-01-01 " . $track['extent']);
+ $diff = (new DateTime("1970-01-01 00:00:00"))->diff($extent);
+ $end = (new DateTime("1970-01-01 " . $track['begin']))->add($diff);
+ if (
+ !(
+ $intervalFrom < $end && (
+ $intervalTo === null || $intervalTo > $begin
+ )
+ )
+ ) {
+ unset($tracks[$index]);
+ }
+ }
+ }
+ // Get the related page numbers
+ $trackPages = [];
+ foreach ($tracks as $track) {
+ if ($track['order'] !== null) {
+ $trackPages[] = $track['order'];
+ }
+ }
+ return $trackPages;
+ }
+
+ /**
+ * Gets the fileId and measure range related page numbers from the musical structMap
+ *
+ * @param string $fileId
+ * @param string $range
+ * @return array
+ */
+ protected function getMeasurePagesByFileId($fileId, $range = null)
+ {
+ // Get all measures referencing the fileid
+ $measures = [];
+ // Get the related page numbers
+ $measurePages = [];
+ $measureIndex = 1;
+ $startOrder = 0;
+ $endOrder = 0;
+ if ($this->document->getCurrentDocument() instanceof MetsDocument) {
+ foreach ($this->document->getCurrentDocument()->musicalStructureInfo as $key => $musicalInfo) {
+ if ($musicalInfo['type'] === 'measure' && is_array($musicalInfo['files'])) {
+ foreach ($musicalInfo['files'] as $file) {
+ if ($file['fileid'] === $fileId && $file['type'] === 'IDREF') {
+ $measures[] = $musicalInfo;
+ }
+ }
+ if ($measureIndex === 1) {
+ $startOrder = $musicalInfo['order'];
+ }
+ $endOrder = $musicalInfo['order'];
+ $measureIndex += 1;
+ }
+ }
+ // Filter measures by the given range of measure numbers
+ if ($measures && $range && !preg_match("/\ball\b/", $range)) {
+ $measureNumbers = [];
+ $range = preg_replace("/\bend\b/", $endOrder, $range);
+ $range = preg_replace("/\bstart\b/", $startOrder, $range);
+ $ranges = array_map('trim', explode(',', $range));
+ foreach ($ranges as $measureNumber) {
+ if (preg_match('/\d+-\d+/', $measureNumber)) {
+ list($from, $to) = array_map('trim', explode('-', $measureNumber));
+ $measureNumbers = array_merge($measureNumbers, range($from, $to));
+ } else {
+ $measureNumbers[] = (int) $measureNumber;
+ }
+ }
+ foreach ($measures as $key => $measure) {
+ if (!in_array($measure['order'], $measureNumbers)) {
+ unset($measures[$key]);
+ }
+ }
+ }
+ foreach ($measures as $measure) {
+ $measurePages[$measure['order']] = $this->document->getCurrentDocument()->musicalStructure[$measure['order']]['page'];
+ }
+ }
+ return $measurePages;
+ }
+
+ /**
+ * Returns the raw data of all annotations with a valid verovio target
+ *
+ * @return array
+ */
+ public function getVerovioRelevantAnnotations()
+ {
+ $annotations = [];
+ /** @var Annotation $annotation */
+ foreach ($this->getAnnotations() as $annotation) {
+ if ($annotation->isVerovioRelevant()) {
+ $annotations[] = $annotation->getRawData();
+ }
+ }
+ return $annotations;
+ }
+
+ /**
+ * Loads all annotation data from the annotation server
+ *
+ * @param Document $document
+ * @return array
+ */
+ protected static function loadData($document)
+ {
+ $annotationData = [];
+ $conf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('dlf');
+ $apiBaseUrl = $conf['annotationServerUrl'];
+ if ($apiBaseUrl && $document->getCurrentDocument() instanceof MetsDocument) {
+ $purl = $document->getCurrentDocument()->mets->xpath('//mods:mods/mods:identifier[@type="purl"]');
+ if (count($purl) > 0) {
+ $annotationRequest = new AnnotationRequest($apiBaseUrl);
+ $annotationData = $annotationRequest->getAll((string) $purl[0]);
+ }
+ }
+ return $annotationData;
+ }
+
+ /**
+ * @param $document
+ * @return DocumentAnnotation|null
+ *
+ */
+ public static function getInstance($document)
+ {
+ if (self::$instance == null) {
+ $annotationData = self::loadData($document);
+ self::$instance = new DocumentAnnotation($annotationData, $document);
+ }
+ return self::$instance;
+ }
+}
diff --git a/Classes/Common/Helper.php b/Classes/Common/Helper.php
index 4a5a761821..6037a96bba 100644
--- a/Classes/Common/Helper.php
+++ b/Classes/Common/Helper.php
@@ -424,7 +424,7 @@ public static function getIndexNameFromUid(int $uid, string $table, int $pid = -
if (
!$uid
// NOTE: Only use tables that don't have too many entries!
- || !in_array($table, ['tx_dlf_collections', 'tx_dlf_libraries', 'tx_dlf_metadata', 'tx_dlf_structures', 'tx_dlf_solrcores'])
+ || !in_array($table, ['tx_dlf_collections', 'tx_dlf_libraries', 'tx_dlf_metadata', 'tx_dlf_metadatasubentries', 'tx_dlf_structures', 'tx_dlf_solrcores'])
) {
self::log('Invalid UID "' . $uid . '" or table "' . $table . '"', LOG_SEVERITY_ERROR);
return '';
@@ -720,7 +720,7 @@ public static function translate(string $indexName, string $table, string $pid):
// Check if we already got a translation.
if (empty($labels[$table][$pid][$languageContentId][$indexName])) {
// Check if this table is allowed for translation.
- if (in_array($table, ['tx_dlf_collections', 'tx_dlf_libraries', 'tx_dlf_metadata', 'tx_dlf_structures'])) {
+ if (in_array($table, ['tx_dlf_collections', 'tx_dlf_libraries', 'tx_dlf_metadata', 'tx_dlf_metadatasubentries', 'tx_dlf_structures'])) {
$additionalWhere = $queryBuilder->expr()->in($table . '.sys_language_uid', [-1, 0]);
if ($languageContentId > 0) {
$additionalWhere = $queryBuilder->expr()->andX(
@@ -935,6 +935,11 @@ private static function getEncryptionKey(): ?string
private static function getLocalConfigurationByPath(string $path)
{
$configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
- return $configurationManager->getLocalConfigurationValueByPath($path);
+
+ if (array_key_exists(strtok($path, '/'), $configurationManager->getLocalConfiguration())) {
+ return $configurationManager->getLocalConfigurationValueByPath($path);
+ }
+
+ return ArrayUtility::getValueByPath($GLOBALS['TYPO3_CONF_VARS'], $path);
}
}
diff --git a/Classes/Common/Indexer.php b/Classes/Common/Indexer.php
index 98fa6885d3..c535564f58 100644
--- a/Classes/Common/Indexer.php
+++ b/Classes/Common/Indexer.php
@@ -510,6 +510,7 @@ private static function processMetadata($document, $metadata, &$solrDoc): array
{
$autocomplete = [];
foreach ($metadata as $indexName => $data) {
+ // TODO: Include also subentries if available.
if (
!empty($data)
&& substr($indexName, -8) !== '_sorting'
@@ -545,6 +546,7 @@ private static function processMetadata($document, $metadata, &$solrDoc): array
*/
private static function addFaceting($doc, &$solrDoc): void
{
+ // TODO: Include also subentries if available.
foreach ($doc->metadataArray[$doc->toplevelId] as $indexName => $data) {
if (
!empty($data)
diff --git a/Classes/Common/MetsDocument.php b/Classes/Common/MetsDocument.php
index 2e690dcc6a..30c4ad3bed 100644
--- a/Classes/Common/MetsDocument.php
+++ b/Classes/Common/MetsDocument.php
@@ -12,7 +12,10 @@
namespace Kitodo\Dlf\Common;
+use \DOMDocument;
use \DOMElement;
+use \DOMNode;
+use \DOMNodeList;
use \DOMXPath;
use \SimpleXMLElement;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
@@ -40,9 +43,12 @@
* @property-read array $metadataArray this holds the documents' parsed metadata array
* @property bool $metadataArrayLoaded flag with information if the metadata array is loaded
* @property-read int $numPages the holds the total number of pages
+ * @property-read int $numMeasures This holds the total number of measures
* @property-read int $parentId this holds the UID of the parent document or zero if not multi-volumed
* @property-read array $physicalStructure this holds the physical structure
* @property-read array $physicalStructureInfo this holds the physical structure metadata
+ * @property-read array $musicalStructure This holds the musical structure
+ * @property-read array $musicalStructureInfo This holds the musical structure metadata
* @property bool $physicalStructureLoaded flag with information if the physical structure is loaded
* @property-read int $pid this holds the PID of the document or zero if not in database
* @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
@@ -145,6 +151,39 @@ final class MetsDocument extends AbstractDocument
*/
protected array $settings = [];
+ /**
+ * This holds the musical structure
+ *
+ * @var array
+ * @access protected
+ */
+ protected array $musicalStructure = [];
+
+ /**
+ * This holds the musical structure metadata
+ *
+ * @var array
+ * @access protected
+ */
+ protected array $musicalStructureInfo = [];
+
+ /**
+ * Is the musical structure loaded?
+ * @see $musicalStructure
+ *
+ * @var bool
+ * @access protected
+ */
+ protected bool $musicalStructureLoaded = false;
+
+ /**
+ * The holds the total number of measures
+ *
+ * @var int
+ * @access protected
+ */
+ protected int $numMeasures;
+
/**
* This adds metadata from METS structural map to metadata array.
*
@@ -245,6 +284,22 @@ public function getFileLocation(string $id): string
}
/**
+ * This gets the measure beginning of a page
+ */
+ public function getPageBeginning($pageId, $fileId)
+ {
+ $mets = $this->mets
+ ->xpath(
+ './mets:structMap[@TYPE="PHYSICAL"]' .
+ '//mets:div[@ID="' . $pageId . '"]' .
+ '/mets:fptr[@FILEID="' . $fileId . '"]' .
+ '/mets:area/@BEGIN'
+ );
+ return empty($mets) ? '' : $mets[0]->__toString();
+ }
+
+ /**
+ * {@inheritDoc}
* @see AbstractDocument::getFileMimeType()
*/
public function getFileMimeType(string $id): string
@@ -563,6 +618,66 @@ private function processMetadataSections(string $id, int $cPid, array $metadata)
}
}
+ /**
+ * @param array $allSubentries
+ * @param string $parentIndex
+ * @param DOMNode $parentNode
+ * @return array|false
+ */
+ private function getSubentries($allSubentries, string $parentIndex, DOMNode $parentNode)
+ {
+ $domXPath = new DOMXPath($parentNode->ownerDocument);
+ $this->registerNamespaces($domXPath);
+ $theseSubentries = [];
+ foreach ($allSubentries as $subentry) {
+ if ($subentry['parent_index_name'] == $parentIndex) {
+ $values = $domXPath->evaluate($subentry['xpath'], $parentNode);
+ if (!empty($subentry['xpath']) && ($values)) {
+ $theseSubentries = array_merge($theseSubentries, $this->getSubentryValue($values, $subentry));
+ }
+ // Set default value if applicable.
+ if (
+ empty($theseSubentries[$subentry['index_name']][0])
+ && strlen($subentry['default_value']) > 0
+ ) {
+ $theseSubentries[$subentry['index_name']] = [$subentry['default_value']];
+ }
+ }
+ }
+ if (empty($theseSubentries)) {
+ return false;
+ }
+ return $theseSubentries;
+ }
+
+ /**
+ * @param $values
+ * @param $subentry
+ * @return array
+ */
+ private function getSubentryValue($values, $subentry)
+ {
+ $theseSubentries = [];
+ if (
+ ($values instanceof DOMNodeList
+ && $values->length > 0) || is_string($values)
+ ) {
+ if (is_string($values)) {
+ // if concat is used evaluate returns a string
+ $theseSubentries[$subentry['index_name']][] = trim($values);
+ } else {
+ foreach ($values as $value) {
+ if (!empty(trim((string) $value->nodeValue))) {
+ $theseSubentries[$subentry['index_name']][] = trim((string) $value->nodeValue);
+ }
+ }
+ }
+ } elseif (!($values instanceof DOMNodeList)) {
+ $theseSubentries[$subentry['index_name']] = [trim((string) $values->nodeValue)];
+ }
+ return $theseSubentries;
+ }
+
/**
* Get logical unit type.
*
@@ -611,7 +726,7 @@ private function extractAndProcessMetadata(string $dmdId, string $mdSectionType,
}
$additionalMetadata = $this->getAdditionalMetadataFromDatabase($cPid, $dmdId);
- // We need a \DOMDocument here, because SimpleXML doesn't support XPath functions properly.
+ // We need a DOMDocument here, because SimpleXML doesn't support XPath functions properly.
$domNode = dom_import_simplexml($this->mdSec[$dmdId]['xml']);
$domXPath = new DOMXPath($domNode->ownerDocument);
$this->registerNamespaces($domXPath);
@@ -651,8 +766,13 @@ private function hasMetadataSection(array $metadataSections, string $currentMeta
*/
private function processAdditionalMetadata(array $additionalMetadata, DOMXPath $domXPath, DOMElement $domNode, array &$metadata): void
{
+ $subentries = [];
+ if (isset($additionalMetadata['subentries'])) {
+ $subentries = $additionalMetadata['subentries'];
+ unset($additionalMetadata['subentries']);
+ }
foreach ($additionalMetadata as $resArray) {
- $this->setMetadataFieldValues($resArray, $domXPath, $domNode, $metadata);
+ $this->setMetadataFieldValues($resArray, $domXPath, $domNode, $metadata, $subentries);
$this->setDefaultMetadataValue($resArray, $metadata);
$this->setSortableMetadataValue($resArray, $domXPath, $domNode, $metadata);
}
@@ -667,19 +787,25 @@ private function processAdditionalMetadata(array $additionalMetadata, DOMXPath $
* @param DOMXPath $domXPath
* @param DOMElement $domNode
* @param array $metadata
+ * @param array $subentryResults
*
* @return void
*/
- private function setMetadataFieldValues(array $resArray, DOMXPath $domXPath, DOMElement $domNode, array &$metadata): void
+ private function setMetadataFieldValues(array $resArray, DOMXPath $domXPath, DOMElement $domNode, array &$metadata, array $subentryResults): void
{
if ($resArray['format'] > 0 && !empty($resArray['xpath'])) {
$values = $domXPath->evaluate($resArray['xpath'], $domNode);
- if ($values instanceof \DOMNodeList && $values->length > 0) {
+ if ($values instanceof DOMNodeList && $values->length > 0) {
$metadata[$resArray['index_name']] = [];
foreach ($values as $value) {
- $metadata[$resArray['index_name']][] = trim((string) $value->nodeValue);
+ $subentries = $this->getSubentries($subentryResults, $resArray['index_name'], $value);
+ if ($subentries) {
+ $metadata[$resArray['index_name']][] = $subentries;
+ } else {
+ $metadata[$resArray['index_name']][] = trim((string) $value->nodeValue);
+ }
}
- } elseif (!($values instanceof \DOMNodeList)) {
+ } elseif (!($values instanceof DOMNodeList)) {
$metadata[$resArray['index_name']] = [trim((string) $values)];
}
}
@@ -716,17 +842,25 @@ private function setDefaultMetadataValue(array $resArray, array &$metadata): voi
*/
private function setSortableMetadataValue(array $resArray, DOMXPath $domXPath, DOMElement $domNode, array &$metadata): void
{
- if (!empty($metadata[$resArray['index_name']]) && $resArray['is_sortable']) {
+ $indexName = $resArray['index_name'];
+ $currentMetadata = $metadata[$indexName][0];
+
+ if (!empty($metadata[$indexName]) && $resArray['is_sortable']) {
if ($resArray['format'] > 0 && !empty($resArray['xpath_sorting'])) {
$values = $domXPath->evaluate($resArray['xpath_sorting'], $domNode);
- if ($values instanceof \DOMNodeList && $values->length > 0) {
- $metadata[$resArray['index_name'] . '_sorting'][0] = trim((string) $values->item(0)->nodeValue);
- } elseif (!($values instanceof \DOMNodeList)) {
- $metadata[$resArray['index_name'] . '_sorting'][0] = trim((string) $values);
+ if ($values instanceof DOMNodeList && $values->length > 0) {
+ $metadata[$indexName . '_sorting'][0] = trim((string) $values->item(0)->nodeValue);
+ } elseif (!($values instanceof DOMNodeList)) {
+ $metadata[$indexName . '_sorting'][0] = trim((string) $values);
}
}
- if (empty($metadata[$resArray['index_name'] . '_sorting'][0])) {
- $metadata[$resArray['index_name'] . '_sorting'][0] = $metadata[$resArray['index_name']][0];
+ if (empty($metadata[$indexName . '_sorting'][0])) {
+ if (is_array($currentMetadata)) {
+ $sortingValue = implode(',', array_column($currentMetadata, 0));
+ $metadata[$indexName . '_sorting'][0] = $sortingValue;
+ } else {
+ $metadata[$indexName . '_sorting'][0] = $currentMetadata;
+ }
}
}
}
@@ -814,6 +948,7 @@ private function getAdditionalMetadataFromDatabase(int $cPid, string $dmdId)
->getRestrictions()
->removeByType(HiddenRestriction::class);
// Get all metadata with configured xpath and applicable format first.
+ // Exclude metadata with subentries, we will fetch them later.
$resultWithFormat = $queryBuilder
->select(
'tx_dlf_metadata.index_name AS index_name',
@@ -852,7 +987,7 @@ private function getAdditionalMetadataFromDatabase(int $cPid, string $dmdId)
// Get all metadata without a format, but with a default value next.
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tx_dlf_metadata');
- // Get hidden records, too.
+ // Get hidden records, too.
$queryBuilder
->getRestrictions()
->removeByType(HiddenRestriction::class);
@@ -872,7 +1007,52 @@ private function getAdditionalMetadataFromDatabase(int $cPid, string $dmdId)
)
->execute();
// Merge both result sets.
- return array_merge($resultWithFormat->fetchAllAssociative(), $resultWithoutFormat->fetchAllAssociative());
+ $allResults = array_merge($resultWithFormat->fetchAllAssociative(), $resultWithoutFormat->fetchAllAssociative());
+
+ // Get subentries separately.
+ $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
+ ->getQueryBuilderForTable('tx_dlf_metadata');
+ // Get hidden records, too.
+ $queryBuilder
+ ->getRestrictions()
+ ->removeByType(HiddenRestriction::class);
+ $subentries = $queryBuilder
+ ->select(
+ 'tx_dlf_subentries_joins.index_name AS index_name',
+ 'tx_dlf_metadata.index_name AS parent_index_name',
+ 'tx_dlf_subentries_joins.xpath AS xpath',
+ 'tx_dlf_subentries_joins.default_value AS default_value'
+ )
+ ->from('tx_dlf_metadata')
+ ->innerJoin(
+ 'tx_dlf_metadata',
+ 'tx_dlf_metadataformat',
+ 'tx_dlf_metadataformat_joins',
+ $queryBuilder->expr()->eq(
+ 'tx_dlf_metadataformat_joins.parent_id',
+ 'tx_dlf_metadata.uid'
+ )
+ )
+ ->innerJoin(
+ 'tx_dlf_metadataformat_joins',
+ 'tx_dlf_metadatasubentries',
+ 'tx_dlf_subentries_joins',
+ $queryBuilder->expr()->eq(
+ 'tx_dlf_subentries_joins.parent_id',
+ 'tx_dlf_metadataformat_joins.uid'
+ )
+ )
+ ->where(
+ $queryBuilder->expr()->eq('tx_dlf_metadata.pid', (int) $cPid),
+ $queryBuilder->expr()->gt('tx_dlf_metadataformat_joins.subentries', 0),
+ $queryBuilder->expr()->eq('tx_dlf_subentries_joins.l18n_parent', 0),
+ $queryBuilder->expr()->eq('tx_dlf_subentries_joins.pid', (int) $cPid)
+ )
+ ->orderBy('tx_dlf_subentries_joins.sorting')
+ ->execute();
+ $subentriesResult = $subentries->fetchAll();
+
+ return array_merge($allResults, ['subentries' => $subentriesResult]);
}
/**
@@ -1168,6 +1348,10 @@ protected function magicGetFileGrps(): array
if (!empty($extConf['fileGrpAudio'])) {
$useGrps = array_merge($useGrps, GeneralUtility::trimExplode(',', $extConf['fileGrpAudio']));
}
+ if (!empty($extConf['fileGrpScore'])) {
+ $useGrps = array_merge($useGrps, GeneralUtility::trimExplode(',', $extConf['fileGrpScore']));
+ }
+
// Get all file groups.
$fileGrps = $this->mets->xpath('./mets:fileSec/mets:fileGrp');
if (!empty($fileGrps)) {
@@ -1186,6 +1370,7 @@ protected function magicGetFileGrps(): array
}
}
}
+
// Are there any fulltext files available?
if (
!empty($extConf['fileGrpFulltext'])
@@ -1255,7 +1440,9 @@ protected function magicGetPhysicalStructure(): array
$this->physicalStructure = $this->getPhysicalElements($elementNodes, $fileUse);
}
$this->physicalStructureLoaded = true;
+
}
+
return $this->physicalStructure;
}
@@ -1275,7 +1462,9 @@ private function getFileRepresentation(string $id, SimpleXMLElement $physicalNod
$fileUse = $this->magicGetFileGrps();
foreach ($physicalNode->children('http://www.loc.gov/METS/')->fptr as $fptr) {
- $fileId = (string) $fptr->attributes()->FILEID;
+ $fileNode = $fptr->area ?? $fptr;
+ $fileId = (string) $fileNode->attributes()->FILEID;
+
// Check if file has valid @USE attribute.
if (!empty($fileUse[$fileId])) {
$this->physicalStructureInfo[$id]['files'][$fileUse[$fileId]] = $fileId;
@@ -1312,9 +1501,30 @@ private function getPhysicalElements(array $elementNodes, array $fileUse): array
$this->physicalStructureInfo[$elements[$order]]['contentIds'] = isset($elementNode['CONTENTIDS']) ? (string) $elementNode['CONTENTIDS'] : '';
// Get the file representations from fileSec node.
foreach ($elementNode->children('http://www.loc.gov/METS/')->fptr as $fptr) {
+ $fileNode = $fptr->area ?? $fptr;
+ $fileId = (string) $fileNode->attributes()->FILEID;
+
// Check if file has valid @USE attribute.
- if (!empty($fileUse[(string) $fptr->attributes()->FILEID])) {
- $this->physicalStructureInfo[$elements[$order]]['files'][$fileUse[(string) $fptr->attributes()->FILEID]] = (string) $fptr->attributes()->FILEID;
+ if (!empty($fileUse[(string) $fileId])) {
+ $this->physicalStructureInfo[$elements[$order]]['files'][$fileUse[$fileId]] = $fileId;
+ }
+ }
+
+ // Get track info wtih begin end extent time for later assignment with musical
+ if ((string) $elementNode['TYPE'] === 'track') {
+ foreach ($elementNode->children('http://www.loc.gov/METS/')->fptr as $fptr) {
+ if (isset($fptr->area) && ((string) $fptr->area->attributes()->BETYPE === 'TIME')) {
+ // Check if file has valid @USE attribute.
+ if (!empty($fileUse[(string) $fptr->area->attributes()->FILEID])) {
+ $this->physicalStructureInfo[$elements[(int) $elementNode['ORDER']]]['tracks'][$fileUse[(string) $fptr->area->attributes()->FILEID]] = [
+ 'fileid' => (string) $fptr->area->attributes()->FILEID,
+ 'begin' => (string) $fptr->area->attributes()->BEGIN,
+ 'betype' => (string) $fptr->area->attributes()->BETYPE,
+ 'extent' => (string) $fptr->area->attributes()->EXTENT,
+ 'exttype' => (string) $fptr->area->attributes()->EXTTYPE,
+ ];
+ }
+ }
}
}
}
@@ -1498,7 +1708,7 @@ public function __sleep(): array
*/
public function __toString(): string
{
- $xml = new \DOMDocument('1.0', 'utf-8');
+ $xml = new DOMDocument('1.0', 'utf-8');
$xml->appendChild($xml->importNode(dom_import_simplexml($this->mets), true));
$xml->formatOutput = true;
return $xml->saveXML();
@@ -1525,4 +1735,152 @@ public function __wakeup(): void
$this->logger->error('Could not load XML after deserialization');
}
}
+
+ /**
+ * This builds an array of the document's musical structure
+ *
+ * @access protected
+ *
+ * @return array Array of musical elements' id, type, label and file representations ordered
+ * by "@ORDER" attribute
+ */
+ protected function magicGetMusicalStructure(): array
+ {
+ // Is there no musical structure array yet?
+ if (!$this->musicalStructureLoaded) {
+ $this->numMeasures = 0;
+ // Does the document have a structMap node of type "MUSICAL"?
+ $elementNodes = $this->mets->xpath('./mets:structMap[@TYPE="MUSICAL"]/mets:div[@TYPE="measures"]/mets:div');
+ if (!empty($elementNodes)) {
+ $musicalSeq = [];
+ // Get file groups.
+ $fileUse = $this->magicGetFileGrps();
+
+ // Get the musical sequence's metadata.
+ $musicalNode = $this->mets->xpath('./mets:structMap[@TYPE="MUSICAL"]/mets:div[@TYPE="measures"]');
+ $musicalSeq[0] = (string) $musicalNode[0]['ID'];
+ $this->musicalStructureInfo[$musicalSeq[0]]['id'] = (string) $musicalNode[0]['ID'];
+ $this->musicalStructureInfo[$musicalSeq[0]]['dmdId'] = (isset($musicalNode[0]['DMDID']) ? (string) $musicalNode[0]['DMDID'] : '');
+ $this->musicalStructureInfo[$musicalSeq[0]]['order'] = (isset($musicalNode[0]['ORDER']) ? (string) $musicalNode[0]['ORDER'] : '');
+ $this->musicalStructureInfo[$musicalSeq[0]]['label'] = (isset($musicalNode[0]['LABEL']) ? (string) $musicalNode[0]['LABEL'] : '');
+ $this->musicalStructureInfo[$musicalSeq[0]]['orderlabel'] = (isset($musicalNode[0]['ORDERLABEL']) ? (string) $musicalNode[0]['ORDERLABEL'] : '');
+ $this->musicalStructureInfo[$musicalSeq[0]]['type'] = (string) $musicalNode[0]['TYPE'];
+ $this->musicalStructureInfo[$musicalSeq[0]]['contentIds'] = (isset($musicalNode[0]['CONTENTIDS']) ? (string) $musicalNode[0]['CONTENTIDS'] : '');
+ // Get the file representations from fileSec node.
+ // TODO: Do we need this for the measurement container element? Can it have any files?
+ foreach ($musicalNode[0]->children('http://www.loc.gov/METS/')->fptr as $fptr) {
+ // Check if file has valid @USE attribute.
+ if (!empty($fileUse[(string) $fptr->attributes()->FILEID])) {
+ $this->musicalStructureInfo[$musicalSeq[0]]['files'][$fileUse[(string) $fptr->attributes()->FILEID]] = [
+ 'fileid' => (string) $fptr->area->attributes()->FILEID,
+ 'begin' => (string) $fptr->area->attributes()->BEGIN,
+ 'end' => (string) $fptr->area->attributes()->END,
+ 'type' => (string) $fptr->area->attributes()->BETYPE,
+ 'shape' => (string) $fptr->area->attributes()->SHAPE,
+ 'coords' => (string) $fptr->area->attributes()->COORDS
+ ];
+ }
+
+ if ((string) $fptr->area->attributes()->BETYPE === 'TIME') {
+ $this->musicalStructureInfo[$musicalSeq[0]]['begin'] = (string) $fptr->area->attributes()->BEGIN;
+ $this->musicalStructureInfo[$musicalSeq[0]]['end'] = (string) $fptr->area->attributes()->END;
+ }
+ }
+
+ $elements = [];
+
+ // Build the physical elements' array from the physical structMap node.
+ foreach ($elementNodes as $elementNode) {
+ $elements[(int) $elementNode['ORDER']] = (string) $elementNode['ID'];
+ $this->musicalStructureInfo[$elements[(int) $elementNode['ORDER']]]['id'] = (string) $elementNode['ID'];
+ $this->musicalStructureInfo[$elements[(int) $elementNode['ORDER']]]['dmdId'] = (isset($elementNode['DMDID']) ? (string) $elementNode['DMDID'] : '');
+ $this->musicalStructureInfo[$elements[(int) $elementNode['ORDER']]]['order'] = (isset($elementNode['ORDER']) ? (string) $elementNode['ORDER'] : '');
+ $this->musicalStructureInfo[$elements[(int) $elementNode['ORDER']]]['label'] = (isset($elementNode['LABEL']) ? (string) $elementNode['LABEL'] : '');
+ $this->musicalStructureInfo[$elements[(int) $elementNode['ORDER']]]['orderlabel'] = (isset($elementNode['ORDERLABEL']) ? (string) $elementNode['ORDERLABEL'] : '');
+ $this->musicalStructureInfo[$elements[(int) $elementNode['ORDER']]]['type'] = (string) $elementNode['TYPE'];
+ $this->musicalStructureInfo[$elements[(int) $elementNode['ORDER']]]['contentIds'] = (isset($elementNode['CONTENTIDS']) ? (string) $elementNode['CONTENTIDS'] : '');
+ // Get the file representations from fileSec node.
+
+ foreach ($elementNode->children('http://www.loc.gov/METS/')->fptr as $fptr) {
+ // Check if file has valid @USE attribute.
+ if (!empty($fileUse[(string) $fptr->area->attributes()->FILEID])) {
+ $this->musicalStructureInfo[$elements[(int) $elementNode['ORDER']]]['files'][$fileUse[(string) $fptr->area->attributes()->FILEID]] = [
+ 'fileid' => (string) $fptr->area->attributes()->FILEID,
+ 'begin' => (string) $fptr->area->attributes()->BEGIN,
+ 'end' => (string) $fptr->area->attributes()->END,
+ 'type' => (string) $fptr->area->attributes()->BETYPE,
+ 'shape' => (string) $fptr->area->attributes()->SHAPE,
+ 'coords' => (string) $fptr->area->attributes()->COORDS
+ ];
+ }
+
+ if ((string) $fptr->area->attributes()->BETYPE === 'TIME') {
+ $this->musicalStructureInfo[$elements[(int) $elementNode['ORDER']]]['begin'] = (string) $fptr->area->attributes()->BEGIN;
+ $this->musicalStructureInfo[$elements[(int) $elementNode['ORDER']]]['end'] = (string) $fptr->area->attributes()->END;
+ }
+ }
+ }
+
+ // Sort array by keys (= @ORDER).
+ ksort($elements);
+ // Set total number of measures.
+ $this->numMeasures = count($elements);
+
+ // Get the track/page info (begin and extent time).
+ $this->musicalStructure = [];
+ $measurePages = [];
+ foreach ($this->magicGetPhysicalStructureInfo() as $physicalId => $page) {
+ if ($page['files']['DEFAULT']) {
+ $measurePages[$physicalId] = $page['files']['DEFAULT'];
+ }
+ }
+ // Build final musicalStructure: assign pages to measures.
+ foreach ($this->musicalStructureInfo as $measureId => $measureInfo) {
+ foreach ($measurePages as $physicalId => $file) {
+ if ($measureInfo['files']['DEFAULT']['fileid'] === $file) {
+ $this->musicalStructure[$measureInfo['order']] = [
+ 'measureid' => $measureId,
+ 'physicalid' => $physicalId,
+ 'page' => array_search($physicalId, $this->physicalStructure)
+ ];
+ }
+ }
+ }
+
+ }
+ $this->musicalStructureLoaded = true;
+ }
+
+ return $this->musicalStructure;
+ }
+
+ /**
+ * This gives an array of the document's musical structure metadata
+ *
+ * @access protected
+ *
+ * @return array Array of elements' type, label and file representations ordered by "@ID" attribute
+ */
+ protected function magicGetMusicalStructureInfo(): array
+ {
+ // Is there no musical structure array yet?
+ if (!$this->musicalStructureLoaded) {
+ // Build musical structure array.
+ $this->magicGetMusicalStructure();
+ }
+ return $this->musicalStructureInfo;
+ }
+
+ /**
+ * This returns $this->numMeasures via __get()
+ *
+ * @access protected
+ *
+ * @return int The total number of measres
+ */
+ protected function magicGetNumMeasures(): int
+ {
+ $this->magicGetMusicalStructure();
+ return $this->numMeasures;
+ }
}
diff --git a/Classes/Common/Solr/SolrSearch.php b/Classes/Common/Solr/SolrSearch.php
index ee33c5367e..06ff7b4298 100644
--- a/Classes/Common/Solr/SolrSearch.php
+++ b/Classes/Common/Solr/SolrSearch.php
@@ -543,7 +543,7 @@ public function submit($start, $rows, $processResults = true)
$documents[$doc['uid']]['metadata'][$indexName] = $doc['metadata'][$indexName];
}
}
- if ($this->searchParams['fulltext'] != '1') {
+ if (!array_key_exists('fulltext', $this->searchParams) || $this->searchParams['fulltext'] != '1') {
$documents[$doc['uid']]['page'] = 1;
$children = $childrenOf[$doc['uid']] ?? [];
diff --git a/Classes/Controller/AbstractController.php b/Classes/Controller/AbstractController.php
index b30395c396..9d64e091f1 100644
--- a/Classes/Controller/AbstractController.php
+++ b/Classes/Controller/AbstractController.php
@@ -63,6 +63,12 @@ public function injectDocumentRepository(DocumentRepository $documentRepository)
*/
protected ?Document $document = null;
+ /**
+ * @access protected
+ * @var array
+ */
+ protected $documentArray;
+
/**
* @access protected
* @var array
@@ -193,7 +199,8 @@ protected function configureProxyUrl(string &$url): void
*/
protected function isDocMissingOrEmpty(): bool
{
- return $this->isDocMissing() || $this->document->getCurrentDocument()->numPages < 1;
+ $multiViewType = $this->settings['multiViewType'] ?? '';
+ return $this->isDocMissing() || ($this->document->getCurrentDocument()->numPages < 1 && $this->document->getCurrentDocument()->tableOfContents[0]['type'] !== $multiViewType);
}
/**
@@ -298,12 +305,22 @@ protected function setDefaultPage(): void
{
// Set default values if not set.
// $this->requestData['page'] may be integer or string (physical structure @ID)
- if (empty($this->requestData['page'])) {
- $this->requestData['page'] = 1;
- } elseif ((int) $this->requestData['page'] > 0) {
- $this->requestData['page'] = MathUtility::forceIntegerInRange((int) $this->requestData['page'], 1, $this->document->getCurrentDocument()->numPages, 1);
- } else {
- $this->requestData['page'] = array_search($this->requestData['page'], $this->document->getCurrentDocument()->physicalStructure);
+ if (
+ (int) $this->requestData['page'] > 0
+ || empty($this->requestData['page'])
+ || is_array($this->requestData['docPage'])
+ ) {
+ if (isset($this->settings['multiViewType']) && $this->document->getCurrentDocument()->tableOfContents[0]['type'] === $this->settings['multiViewType']) {
+ $i = 0;
+ foreach ($this->documentArray as $document) {
+ if ($document !== null) {
+ $this->requestData['docPage'][$i] = MathUtility::forceIntegerInRange((int) $this->requestData['docPage'][$i], 1, $document->numPages, 1);
+ $i++;
+ }
+ }
+ } else {
+ $this->requestData['page'] = MathUtility::forceIntegerInRange((int) $this->requestData['page'], 1, $this->document->getCurrentDocument()->numPages, 1);
+ }
}
// reassign viewData to get correct page
$this->viewData['requestData'] = $this->requestData;
@@ -478,15 +495,39 @@ private function getDocumentByUrl(string $documentId)
{
$doc = AbstractDocument::getInstance($documentId, $this->settings, true);
+ if (isset($this->settings['multiViewType']) && $doc->tableOfContents[0]['type'] === $this->settings['multiViewType']) {
+ $childDocuments = $doc->tableOfContents[0]['children'];
+ $i = 0;
+ foreach ($childDocuments as $document) {
+ $this->documentArray[] = AbstractDocument::getInstance($document['points'], $this->settings, true);
+ if (!isset($this->requestData['docPage'][$i]) && isset(explode('#', $document['points'])[1])) {
+ $initPage = explode('#', $document['points'])[1];
+ $this->requestData['docPage'][$i] = $initPage;
+ }
+ $i++;
+ }
+ } else {
+ $this->documentArray[] = $doc;
+ }
+ if ($this->requestData['multipleSource'] && is_array($this->requestData['multipleSource'])) {
+ $i = 0;
+ foreach ($this->requestData['multipleSource'] as $location) {
+ $document = AbstractDocument::getInstance($location, $this->settings, true);
+ if ($document !== null) {
+ $this->documentArray['extra_' . $i] = $document;
+ }
+ $i++;
+ }
+ }
+
if ($doc !== null) {
+ $this->document = GeneralUtility::makeInstance(Document::class);
+
if ($doc->recordId) {
// find document from repository by recordId
$docFromRepository = $this->documentRepository->findOneByRecordId($doc->recordId);
if ($docFromRepository !== null) {
$this->document = $docFromRepository;
- } else {
- // create new dummy Document object
- $this->document = GeneralUtility::makeInstance(Document::class);
}
}
diff --git a/Classes/Controller/AnnotationController.php b/Classes/Controller/AnnotationController.php
new file mode 100644
index 0000000000..9d5b2a577f
--- /dev/null
+++ b/Classes/Controller/AnnotationController.php
@@ -0,0 +1,48 @@
+
+ *
+ * This file is part of the Kitodo and TYPO3 projects.
+ *
+ * @license GNU General Public License version 3 or later.
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ */
+
+use Kitodo\Dlf\Common\DocumentAnnotation;
+
+/**
+ * Controller class for plugin 'Annotation'.
+ *
+ * @package TYPO3
+ * @subpackage dlf
+ * @access public
+ */
+class AnnotationController extends AbstractController
+{
+ /**
+ * The main method of the plugin
+ *
+ * @return void
+ */
+ public function mainAction()
+ {
+ $this->loadDocument();
+
+ if (
+ $this->document === null
+ || $this->document->getCurrentDocument() === null
+ ) {
+ // Quit without doing anything if required variables are not set.
+ return;
+ } else {
+ $documentAnnotation = DocumentAnnotation::getInstance($this->document);
+
+ $this->view->assign('annotations', $documentAnnotation->getAnnotations());
+ $this->view->assign('currentPage', $this->requestData['page']);
+ }
+ }
+}
diff --git a/Classes/Controller/MetadataController.php b/Classes/Controller/MetadataController.php
index 7979d40118..67afcbc7f7 100644
--- a/Classes/Controller/MetadataController.php
+++ b/Classes/Controller/MetadataController.php
@@ -171,6 +171,17 @@ protected function printMetadata(array $metadata): void
foreach ($section as $name => $value) {
// NOTE: Labels are to be escaped in Fluid template
+ $metadata[$i][$name] = is_array($value)
+ ? implode($this->settings['separator'], $value)
+ : $value;
+
+ if ($metadata[$i][$name] === 'Array') {
+ $metadata[$i][$name] = [];
+ foreach ($value as $subKey => $subValue) {
+ $metadata[$i][$name][$subKey] = $subValue;
+ }
+ }
+
$this->parseMetadata($i, $name, $value, $metadata);
if (is_array($metadata[$i][$name])) {
diff --git a/Classes/Controller/NavigationController.php b/Classes/Controller/NavigationController.php
index 7f9341b434..698d8a4799 100644
--- a/Classes/Controller/NavigationController.php
+++ b/Classes/Controller/NavigationController.php
@@ -11,6 +11,7 @@
namespace Kitodo\Dlf\Controller;
+use Kitodo\Dlf\Common\MetsDocument;
use Kitodo\Dlf\Domain\Model\PageSelectForm;
use TYPO3\CMS\Core\Utility\MathUtility;
@@ -106,6 +107,7 @@ public function mainAction(): void
$orderLabel = $this->document->getCurrentDocument()->physicalStructureInfo[$this->document->getCurrentDocument()->physicalStructure[$i]]['orderlabel'];
$pageOptions[$i] = '[' . $i . ']' . ($orderLabel ? ' - ' . htmlspecialchars($orderLabel) : '');
}
+
$this->view->assign('pageOptions', $pageOptions);
// prepare feature array for fluid
@@ -114,5 +116,27 @@ public function mainAction(): void
$features[$feature] = true;
}
$this->view->assign('features', $features);
+
+ if ($this->document->getCurrentDocument() instanceof MetsDocument) {
+ if ($this->document->getCurrentDocument()->numMeasures > 0) {
+ $measureOptions = [];
+ $measurePages = [];
+ for ($i = 1; $i <= $this->document->getCurrentDocument()->numMeasures; $i++) {
+ $measureOptions[$i] = '[' . $i . ']' . ($this->document->getCurrentDocument()->musicalStructureInfo[$this->document->getCurrentDocument()->musicalStructure[$i]['measureid']]['orderlabel'] ? ' - ' . htmlspecialchars($this->document->getCurrentDocument()->musicalStructureInfo[$this->document->getCurrentDocument()->musicalStructureInfo[$i]]['orderlabel']) : '');
+ $measurePages[$i] = $this->document->getCurrentDocument()->musicalStructure[$i]['page'];
+ }
+
+ if (!isset($this->requestData['measure'])) {
+ $currentMeasure = array_search($this->requestData['page'], $measurePages);
+ } else {
+ $currentMeasure = $this->requestData['measure'];
+ }
+
+ $this->view->assign('currentMeasure', $currentMeasure);
+ $this->view->assign('numMeasures', $this->document->getCurrentDocument()->numMeasures);
+ $this->view->assign('measureOptions', $measureOptions);
+ $this->view->assign('measurePages', $measurePages);
+ }
+ }
}
}
diff --git a/Classes/Controller/PageViewController.php b/Classes/Controller/PageViewController.php
index 1af8443bce..286ca02228 100644
--- a/Classes/Controller/PageViewController.php
+++ b/Classes/Controller/PageViewController.php
@@ -11,7 +11,12 @@
namespace Kitodo\Dlf\Controller;
+use Kitodo\Dlf\Common\AbstractDocument;
+use Kitodo\Dlf\Common\DocumentAnnotation;
use Kitodo\Dlf\Common\IiifManifest;
+use Kitodo\Dlf\Common\MetsDocument;
+use Kitodo\Dlf\Domain\Model\Document;
+use Kitodo\Dlf\Domain\Model\FormAddDocument;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use Ubl\Iiif\Presentation\Common\Model\Resources\ManifestInterface;
@@ -40,6 +45,24 @@ class PageViewController extends AbstractController
protected array $images = [];
/**
+ * Holds the current scores' URL, MIME types and the
+ * id of the current page
+ *
+ * @var array
+ * @access protected
+ */
+ protected $scores = [];
+
+ /**
+ * @var array
+ * @access protected
+ */
+ protected $measures = [];
+
+ /**
+ * Holds the current fulltexts' URLs
+ *
+ * @var array
* @access protected
* @var array Holds the current full texts' URLs
*/
@@ -53,6 +76,14 @@ class PageViewController extends AbstractController
*/
protected array $annotationContainers = [];
+
+ /**
+ * Holds the verovio relevant annotations
+ *
+ * @var array
+ */
+ protected $verovioAnnotations = [];
+
/**
* The main method of the plugin
*
@@ -67,6 +98,24 @@ public function mainAction(): void
if ($this->isDocMissingOrEmpty()) {
// Quit without doing anything if required variables are not set.
return;
+ } else {
+ if (isset($this->settings['multiViewType']) && $this->document->getCurrentDocument()->tableOfContents[0]['type'] === $this->settings['multiViewType'] && empty($this->requestData['multiview'])) {
+ $params = array_merge(
+ ['tx_dlf' => $this->requestData],
+ ['tx_dlf[multiview]' => 1]
+ );
+ $uriBuilder = $this->uriBuilder;
+ $uri = $uriBuilder
+ ->setArguments($params)
+ ->setArgumentPrefix('tx_dlf')
+ ->uriFor('main');
+ $this->redirectToUri($uri);
+ }
+ $this->setPage();
+ $this->requestData['double'] = MathUtility::forceIntegerInRange($this->requestData['double'], 0, 1, 0);
+
+ $documentAnnotation = DocumentAnnotation::getInstance($this->document);
+ $this->verovioAnnotations = $documentAnnotation->getVerovioRelevantAnnotations();
}
$this->setPage();
@@ -81,6 +130,9 @@ public function mainAction(): void
$this->annotationContainers[1] = $this->getAnnotationContainers($this->requestData['page'] + 1);
}
+ $this->scores = $this->getScore($this->requestData['page']);
+ $this->measures = $this->getMeasures($this->requestData['page']);
+
// Get the controls for the map.
$this->controls = explode(',', $this->settings['features']);
@@ -88,9 +140,285 @@ public function mainAction(): void
$this->addViewerJS();
+ if ($this->documentArray !== null) {
+ $this->view->assign('docCount', count($this->documentArray));
+ $this->view->assign('docArray', $this->documentArray);
+ }
+
+ $this->view->assign('docPage', $this->requestData['docPage']);
+ $this->view->assign('docType', $this->document->getCurrentDocument()->tableOfContents[0]['type']);
+
+ $this->view->assign('multiview', $this->requestData['multiview']);
+ if ($this->requestData['multiview']) {
+ $this->multipageNavigation();
+ }
+
$this->view->assign('images', $this->images);
$this->view->assign('docId', $this->requestData['id']);
$this->view->assign('page', $this->requestData['page']);
+
+ }
+
+ /**
+ * Add multi page navigation
+ * @return void
+ */
+ protected function multipageNavigation(): void
+ {
+ $navigationArray = [];
+ $navigationMeasureArray = [];
+ $navigateAllPageNext = [];
+ $navigateAllPagePrev = [];
+ $navigateAllMeasureNext = [];
+ $navigateAllMeasurePrev = [];
+ $docNumPages = [];
+ $i = 0;
+ foreach ($this->documentArray as $document) {
+ // convert either page or measure if requestData exists
+ if ($this->requestData['docPage'][$i] && empty($this->requestData['docMeasure'][$i])) {
+ // convert document page information to measure count information
+ $this->requestData['docMeasure'][$i] = $this->convertMeasureOrPage($document, null, $this->requestData['docPage'][$i]);
+
+ } elseif ((empty($this->requestData['docPage'][$i]) || $this->requestData['docPage'][$i] === 1) && $this->requestData['docMeasure'][$i]) {
+ $this->requestData['docPage'][$i] = $this->convertMeasureOrPage($document, $this->requestData['docMeasure'][$i]);
+ }
+
+ $navigationArray[$i]['next'] = [
+ 'tx_dlf[docPage][' . $i . ']' =>
+ MathUtility::forceIntegerInRange((int) $this->requestData['docPage'][$i] + 1, 1, $document->numPages, 1)
+ ];
+ $navigationArray[$i]['prev'] = [
+ 'tx_dlf[docPage][' . $i . ']' =>
+ MathUtility::forceIntegerInRange((int) $this->requestData['docPage'][$i] - 1, 1, $document->numPages, 1)
+ ];
+
+ $navigateAllPageNext = array_merge(
+ $navigateAllPageNext,
+ [
+ 'tx_dlf[docPage][' . $i . ']' =>
+ MathUtility::forceIntegerInRange((int) $this->requestData['docPage'][$i] + 1, 1, $document->numPages, 1)
+ ]
+ );
+
+ $navigateAllPagePrev = array_merge(
+ $navigateAllPagePrev,
+ [
+ 'tx_dlf[docPage][' . $i . ']' =>
+ MathUtility::forceIntegerInRange((int) $this->requestData['docPage'][$i] - 1, 1, $document->numPages, 1)
+ ]
+ );
+
+ $navigateAllMeasureNext = array_merge(
+ $navigateAllMeasureNext,
+ [
+ 'tx_dlf[docMeasure][' . $i . ']' =>
+ MathUtility::forceIntegerInRange((int) $this->requestData['docMeasure'][$i] + 1, 1, $document->numMeasures, 1)
+ ]
+ );
+
+ $navigateAllMeasurePrev = array_merge(
+ $navigateAllMeasurePrev,
+ [
+ 'tx_dlf[docMeasure][' . $i . ']' =>
+ MathUtility::forceIntegerInRange((int) $this->requestData['docMeasure'][$i] - 1, 1, $document->numMeasures, 1)
+ ]
+ );
+
+ if ($document->numMeasures > 0) {
+ $navigationMeasureArray[$i]['next'] = [
+ 'tx_dlf[docMeasure][' . $i . ']' =>
+ MathUtility::forceIntegerInRange((int) $this->requestData['docMeasure'][$i] + 1, 1, $document->numMeasures, 1)
+ ];
+
+ $navigationMeasureArray[$i]['prev'] = [
+ 'tx_dlf[docMeasure][' . $i . ']' =>
+ MathUtility::forceIntegerInRange((int) $this->requestData['docMeasure'][$i] - 1, 1, $document->numMeasures, 1)
+ ];
+ }
+
+ $docNumPages[$i] = $document->numPages;
+ $i++;
+ }
+
+ // page navigation
+ $this->view->assign('navigationArray', $navigationArray);
+ $this->view->assign('navigateAllPageNext', $navigateAllPageNext);
+ $this->view->assign('navigateAllPagePrev', $navigateAllPagePrev);
+ // measure navigation
+ $this->view->assign('navigateAllMeasurePrev', $navigateAllMeasurePrev);
+ $this->view->assign('navigateAllMeasureNext', $navigateAllMeasureNext);
+ $this->view->assign('navigationMeasureArray', $navigationMeasureArray);
+
+ $this->view->assign('docNumPage', $docNumPages);
+ }
+
+ /**
+ * Converts either measure into page or page into measure
+ * @param $document
+ * @param $measure
+ * @param $page
+ * @return false|int|mixed|string|null
+ */
+ public function convertMeasureOrPage($document, $measure = null, $page = null)
+ {
+ $return = null;
+ $measure2Page = array_column($document->musicalStructure, 'page');
+ if ($measure) {
+ $return = $measure2Page[$measure];
+ } elseif ($page) {
+ $return = array_search($page, $measure2Page);
+ }
+
+ return $return;
+ }
+
+ /**
+ * Action to add multiple mets sources (multi page view)
+ * @return void
+ */
+ public function addDocumentAction(FormAddDocument $formAddDocument)
+ {
+ if (GeneralUtility::isValidUrl($formAddDocument->getLocation())) {
+ $nextMultipleSourceKey = 0;
+ if ($this->requestData['multipleSource']) {
+ $nextMultipleSourceKey = max(array_keys($this->requestData['multipleSource'])) + 1;
+ }
+ $params = array_merge(
+ ['tx_dlf' => $this->requestData],
+ ['tx_dlf[multipleSource][' . $nextMultipleSourceKey . ']' => $formAddDocument->getLocation()],
+ ['tx_dlf[multiview]' => 1]
+ );
+ $uriBuilder = $this->uriBuilder;
+ $uri = $uriBuilder
+ ->setArguments($params)
+ ->setArgumentPrefix('tx_dlf')
+ ->uriFor('main');
+
+ $this->redirectToUri($uri);
+ }
+
+ }
+
+ /**
+ * Get all measures from musical struct
+ * @param int $page
+ * @param ?MetsDocument $specificDoc
+ * @param int|null $docNumber
+ * @return array
+ */
+ protected function getMeasures(int $page, MetsDocument $specificDoc = null, $docNumber = null): array
+ {
+ if ($specificDoc) {
+ $doc = $specificDoc;
+ } else {
+ $doc = $this->document->getCurrentDocument();
+ }
+ $currentPhysId = $doc->physicalStructure[$page];
+ $measureCoordsFromCurrentSite = [];
+ $measureCounterToMeasureId = [];
+ $measureLinks = [];
+ $defaultFileId = $doc->physicalStructureInfo[$currentPhysId]['files']['DEFAULT'];
+ if ($doc instanceof MetsDocument) {
+ if (isset($defaultFileId)) {
+ $musicalStruct = $doc->musicalStructureInfo;
+
+ $i = 0;
+ foreach ($musicalStruct as $measureData) {
+ if ($defaultFileId == $measureData['files']['DEFAULT']['fileid']) {
+ $measureCoordsFromCurrentSite[$measureData['files']['SCORE']['begin']] = $measureData['files']['DEFAULT']['coords'];
+ $measureCounterToMeasureId[$i] = $measureData['files']['SCORE']['begin'];
+
+ if ($specificDoc) {
+ // build link for each measure
+ $params = [
+ 'tx_dlf' => $this->requestData,
+ 'tx_dlf[docMeasure][' . $docNumber . ']' => $i
+ ];
+ } else {
+ // build link for each measure
+ $params = [
+ 'tx_dlf' => $this->requestData,
+ 'tx_dlf[measure]' => $i
+ ];
+ }
+ $uriBuilder = $this->uriBuilder;
+ $uri = $uriBuilder
+ ->setArguments($params)
+ ->setArgumentPrefix('tx_dlf')
+ ->uriFor('main');
+ $measureLinks[$measureData['files']['SCORE']['begin']] = $uri;
+
+ }
+ $i++;
+ }
+ }
+ }
+ return [
+ 'measureCoordsCurrentSite' => $measureCoordsFromCurrentSite,
+ 'measureCounterToMeasureId' => $measureCounterToMeasureId,
+ 'measureLinks' => $measureLinks
+ ];
+ }
+
+ /**
+ * Get score URL and MIME type
+ *
+ * @access protected
+ *
+ * @param int $page: Page number
+ * @param ?MetsDocument $specificDoc
+ *
+ * @return array URL and MIME type of fulltext file
+ */
+ protected function getScore(int $page, MetsDocument $specificDoc = null)
+ {
+ $score = [];
+ $loc = '';
+ if ($specificDoc) {
+ $doc = $specificDoc;
+ } else {
+ $doc = $this->document->getCurrentDocument();
+ }
+ if ($doc instanceof MetsDocument) {
+ $fileGrpsScores = GeneralUtility::trimExplode(',', $this->extConf['files']['fileGrpScore']);
+
+ $pageId = $doc->physicalStructure[$page];
+ $files = $doc->physicalStructureInfo[$pageId]['files'] ?? [];
+
+ foreach ($fileGrpsScores as $fileGrpScore) {
+ if (isset($files[$fileGrpScore])) {
+ $loc = $files[$fileGrpScore];
+ break;
+ }
+ }
+
+ if (!empty($loc)) {
+ $score['mimetype'] = $doc->getFileMimeType($loc);
+ $score['pagebeginning'] = $doc->getPageBeginning($pageId, $loc);
+ $score['url'] = $doc->getFileLocation($loc);
+ if ($this->settings['useInternalProxy']) {
+ // Configure @action URL for form.
+ $uri = $this->uriBuilder->reset()
+ ->setTargetPageUid($this->pageUid)
+ ->setCreateAbsoluteUri(!empty($this->settings['forceAbsoluteUrl']) ? true : false)
+ ->setArguments(
+ [
+ 'eID' => 'tx_dlf_pageview_proxy',
+ 'url' => $score['url'],
+ 'uHash' => GeneralUtility::hmac($score['url'], 'PageViewProxy')
+ ]
+ )
+ ->build();
+
+ $score['url'] = $uri;
+ }
+ }
+ }
+
+ if (empty($score)) {
+ $this->logger->notice('No score file found for page "' . $page . '" in fileGrps "' . $this->settings['fileGrpScore'] . '"');
+ }
+ return $score;
}
/**
@@ -137,20 +465,92 @@ protected function getFulltext(int $page): array
*/
protected function addViewerJS(): void
{
- // Viewer configuration.
- $viewerConfiguration = '$(document).ready(function() {
- if (dlfUtils.exists(dlfViewer)) {
- tx_dlf_viewer = new dlfViewer({
- controls: ["' . implode('", "', $this->controls) . '"],
- div: "' . $this->settings['elementId'] . '",
- progressElementId: "' . $this->settings['progressElementId'] . '",
- images: ' . json_encode($this->images) . ',
- fulltexts: ' . json_encode($this->fulltexts) . ',
- annotationContainers: ' . json_encode($this->annotationContainers) . ',
- useInternalProxy: ' . ($this->settings['useInternalProxy'] ? 1 : 0) . '
- });
+ if ($this->documentArray !== null && count($this->documentArray) > 1) {
+ $jsViewer = 'tx_dlf_viewer = [];';
+ $i = 0;
+ foreach ($this->documentArray as $document) {
+ if ($document !== null) {
+ $docPage = $this->requestData['docPage'][$i];
+ $docImage = [];
+ $docFulltext = [];
+ $docAnnotationContainers = [];
+
+ if ($this->document->getCurrentDocument() instanceof MetsDocument) {
+ // check if page or measure is set
+ if ($this->requestData['docMeasure'][$i]) {
+ // convert document page information to measure count information
+ $measure2Page = array_column($document->musicalStructure, 'page');
+ $docPage = $measure2Page[$this->requestData['docMeasure'][$i]];
+ }
+ }
+ if ($docPage == null) {
+ $docPage = 1;
+ }
+ $docImage[0] = $this->getImage($docPage, $document);
+ $currentMeasureId = '';
+
+ $docScore = $this->getScore($docPage, $document);
+ $docMeasures = $this->getMeasures($docPage, $document);
+
+ if ($this->requestData['docMeasure'][$i]) {
+ $currentMeasureId = $docMeasures['measureCounterToMeasureId'][$this->requestData['docMeasure'][$i]];
+ }
+
+ $jsViewer .= 'tx_dlf_viewer[' . $i . '] = new dlfViewer({
+ controls: ["' . implode('", "', $this->controls) . '"],
+ div: "tx-dfgviewer-map-' . $i . '",
+ progressElementId: "' . $this->settings['progressElementId'] . '",
+ counter: "' . $i . '",
+ images: ' . json_encode($docImage) . ',
+ fulltexts: ' . json_encode($docFulltext) . ',
+ score: ' . json_encode($docScore) . ',
+ annotationContainers: ' . json_encode($docAnnotationContainers) . ',
+ measureCoords: ' . json_encode($docMeasures['measureCoordsCurrentSite']) . ',
+ useInternalProxy: ' . ($this->settings['useInternalProxy'] ? 1 : 0) . ',
+ currentMeasureId: "' . $currentMeasureId . '",
+ measureIdLinks: ' . json_encode($docMeasures['measureLinks']) . '
+ });
+ ';
+ $i++;
}
- });';
+ }
+
+ // Viewer configuration.
+ $viewerConfiguration = '$(document).ready(function() {
+ if (dlfUtils.exists(dlfViewer)) {
+ ' . $jsViewer . '
+ viewerCount = ' . ($i - 1) . ';
+ }
+ });';
+ } else {
+ $currentMeasureId = '';
+ $docPage = $this->requestData['page'];
+
+ $docMeasures = $this->getMeasures($docPage);
+ if ($this->requestData['measure']) {
+ $currentMeasureId = $docMeasures['measureCounterToMeasureId'][$this->requestData['measure']];
+ }
+
+ // Viewer configuration.
+ $viewerConfiguration = '$(document).ready(function() {
+ if (dlfUtils.exists(dlfViewer)) {
+ tx_dlf_viewer = new dlfViewer({
+ controls: ["' . implode('", "', $this->controls) . '"],
+ div: "' . $this->settings['elementId'] . '",
+ progressElementId: "' . $this->settings['progressElementId'] . '",
+ images: ' . json_encode($this->images) . ',
+ fulltexts: ' . json_encode($this->fulltexts) . ',
+ score: ' . json_encode($this->scores) . ',
+ annotationContainers: ' . json_encode($this->annotationContainers) . ',
+ measureCoords: ' . json_encode($docMeasures['measureCoordsCurrentSite']) . ',
+ useInternalProxy: ' . ($this->settings['useInternalProxy'] ? 1 : 0) . ',
+ verovioAnnotations: ' . json_encode($this->verovioAnnotations) . ',
+ currentMeasureId: "' . $currentMeasureId . '",
+ measureIdLinks: ' . json_encode($docMeasures['measureLinks']) . '
+ });
+ }
+ });';
+ }
$this->view->assign('viewerConfiguration', $viewerConfiguration);
}
@@ -217,30 +617,54 @@ protected function getAnnotationContainers(int $page): array
*
* @param int $page Page number
*
+ * @param ?MetsDocument $specificDoc
+ *
* @return array URL and MIME type of image file
*/
- protected function getImage(int $page): array
+ protected function getImage(int $page, MetsDocument $specificDoc = null): array
{
$image = [];
// Get @USE value of METS fileGrp.
$fileGrpsImages = GeneralUtility::trimExplode(',', $this->extConf['files']['fileGrpImages']);
while ($fileGrpImages = array_pop($fileGrpsImages)) {
- // Get image link.
- $physicalStructureInfo = $this->document->getCurrentDocument()->physicalStructureInfo[$this->document->getCurrentDocument()->physicalStructure[$page]];
- $files = $physicalStructureInfo['files'];
- if (!empty($files[$fileGrpImages])) {
- $file = $this->document->getCurrentDocument()->getFileInfo($files[$fileGrpImages]);
- $image['url'] = $file['location'];
- $image['mimetype'] = $file['mimeType'];
-
- // Only deliver static images via the internal PageViewProxy.
- // (For IIP and IIIF, the viewer needs to build and access a separate metadata URL, see `getMetadataURL` in `OLSources.js`.)
- if ($this->settings['useInternalProxy'] && !str_contains(strtolower($image['mimetype']), 'application')) {
- $this->configureProxyUrl($image['url']);
+ if ($specificDoc) {
+ // Get image link.
+ $physicalStructureInfo = $specificDoc->physicalStructureInfo[$specificDoc->physicalStructure[$page]];
+ $files = $physicalStructureInfo['files'];
+ if (!empty($files[$fileGrpImages])) {
+ $file = $specificDoc->getFileInfo($files[$fileGrpImages]);
+ $image['url'] = $file['location'];
+ $image['mimetype'] = $file['mimeType'];
+
+ // Only deliver static images via the internal PageViewProxy.
+ // (For IIP and IIIF, the viewer needs to build and access a separate metadata URL, see `getMetadataURL` in `OLSources.js`.)
+ if ($this->settings['useInternalProxy'] && !str_contains(strtolower($image['mimetype']), 'application')) {
+ $this->configureProxyUrl($image['url']);
+ }
+ break;
+ } else {
+ $this->logger->notice('No image file found for page "' . $page . '" in fileGrp "' . $fileGrpImages . '"');
}
- break;
+
} else {
- $this->logger->notice('No image file found for page "' . $page . '" in fileGrp "' . $fileGrpImages . '"');
+
+ // Get image link.
+ $physicalStructureInfo = $this->document->getCurrentDocument()->physicalStructureInfo[$this->document->getCurrentDocument()->physicalStructure[$page]];
+ $files = $physicalStructureInfo['files'];
+ if (!empty($files[$fileGrpImages])) {
+ $file = $this->document->getCurrentDocument()->getFileInfo($files[$fileGrpImages]);
+ $image['url'] = $file['location'];
+ $image['mimetype'] = $file['mimeType'];
+
+ // Only deliver static images via the internal PageViewProxy.
+ // (For IIP and IIIF, the viewer needs to build and access a separate metadata URL, see `getMetadataURL` in `OLSources.js`.)
+ if ($this->settings['useInternalProxy'] && !str_contains(strtolower($image['mimetype']), 'application')) {
+ $this->configureProxyUrl($image['url']);
+ }
+ break;
+ } else {
+ $this->logger->notice('No image file found for page "' . $page . '" in fileGrp "' . $fileGrpImages . '"');
+ }
}
}
if (empty($image)) {
diff --git a/Classes/Controller/SearchController.php b/Classes/Controller/SearchController.php
index 127943c54f..6ebc52383f 100644
--- a/Classes/Controller/SearchController.php
+++ b/Classes/Controller/SearchController.php
@@ -335,7 +335,7 @@ public function makeFacetsMenuArray(array $facets): array
}
foreach (array_keys($facets) as $field) {
- $search['params']['component']['facetset']['facet'][] = [
+ $search['params']['component']['facetset']['facet'][$field] = [
'type' => 'field',
'mincount' => '1',
'key' => $field,
diff --git a/Classes/Controller/ToolboxController.php b/Classes/Controller/ToolboxController.php
index d17dc004b6..e239567a41 100644
--- a/Classes/Controller/ToolboxController.php
+++ b/Classes/Controller/ToolboxController.php
@@ -98,6 +98,9 @@ private function renderTools(): void
case 'searchindocumenttool':
$this->renderToolByName('renderSearchInDocumentTool');
break;
+ case 'scoretool':
+ $this->renderToolByName('renderScoreTool');
+ break;
default:
$this->logger->warning('Incorrect tool configuration: "' . $this->settings['tools'] . '". Tool "' . $tool . '" does not exist.');
}
@@ -201,6 +204,42 @@ private function renderFulltextTool(): void
}
/**
+ * Renders the score tool
+ *
+ * @return void
+ */
+ public function renderScoreTool()
+ {
+ if (
+ $this->isDocMissingOrEmpty()
+ || empty($this->extConf['files']['fileGrpScore'])
+ ) {
+ // Quit without doing anything if required variables are not set.
+ return;
+ }
+
+ if ($this->requestData['page']) {
+ $currentPhysPage = $this->document->getCurrentDocument()->physicalStructure[$this->requestData['page']];
+ } else {
+ $currentPhysPage = $this->document->getCurrentDocument()->physicalStructure[1];
+ }
+
+ $fileGrpsScores = GeneralUtility::trimExplode(',', $this->extConf['files']['fileGrpScore']);
+ foreach ($fileGrpsScores as $fileGrpScore) {
+ if ($this->document->getCurrentDocument()->physicalStructureInfo[$currentPhysPage]['files'][$fileGrpScore]) {
+ $scoreFile = $this->document->getCurrentDocument()->physicalStructureInfo[$currentPhysPage]['files'][$fileGrpScore];
+ }
+ }
+ if (!empty($scoreFile)) {
+ $this->view->assign('score', true);
+ $this->view->assign('activateScoreInitially', MathUtility::forceIntegerInRange($this->settings['activateScoreInitially'], 0, 1, 0));
+ } else {
+ $this->view->assign('score', false);
+ }
+ }
+
+ /**
+ * Renders the image download tool
* Renders the image download tool (used in template)
* @SuppressWarnings(PHPMD.UnusedPrivateMethod)
*
diff --git a/Classes/Controller/View3DController.php b/Classes/Controller/View3DController.php
index ba9f1c5be0..e89edf577b 100644
--- a/Classes/Controller/View3DController.php
+++ b/Classes/Controller/View3DController.php
@@ -21,6 +21,9 @@
*/
class View3DController extends AbstractController
{
+
+ const MIDDLEWARE_DLF_EMBEDDED_3D_VIEWER_PREFIX = '/?middleware=dlf/embedded3DViewer';
+
/**
* @access public
*
@@ -28,40 +31,49 @@ class View3DController extends AbstractController
*/
public function mainAction(): void
{
+
+ if (!empty($this->requestData['model'])) {
+ $this->view->assign('is3DViewer', $this->is3dViewer($this->requestData['model']));
+ $embedded3DViewerUrl = $this->buildEmbedded3dViewerUrl($this->requestData['model']);
+ if (!empty($this->requestData['viewer'])) {
+ $embedded3DViewerUrl .= '&viewer=' . $this->requestData['viewer'];
+ }
+ $this->view->assign('embedded3DViewerUrl', $embedded3DViewerUrl);
+ return;
+ }
+
// Load current document.
$this->loadDocument();
if (
- $this->isDocMissingOrEmpty()
- || $this->document->getCurrentDocument()->metadataArray['LOG_0001']['type'][0] != 'object'
+ !($this->isDocMissingOrEmpty()
+ || $this->document->getCurrentDocument()->metadataArray['LOG_0001']['type'][0] != 'object')
) {
- // Quit without doing anything if required variables are not set.
- return;
- } else {
$model = trim($this->document->getCurrentDocument()->getFileLocation($this->document->getCurrentDocument()->physicalStructureInfo[$this->document->getCurrentDocument()->physicalStructure[1]]['files']['DEFAULT']));
- $this->view->assign('3d', $model);
-
- $modelConverted = trim($this->document->getCurrentDocument()->getFileLocation($this->document->getCurrentDocument()->physicalStructureInfo[$this->document->getCurrentDocument()->physicalStructure[1]]['files']['CONVERTED']));
- $xml = $this->requestData['id'];
+ $this->view->assign('is3DViewer', $this->is3dViewer($model));
+ $this->view->assign('embedded3DViewerUrl', $this->buildEmbedded3dViewerUrl($model));
+ }
+ }
- $settingsParts = explode("/", $model);
- $fileName = end($settingsParts);
- $path = substr($model, 0, strrpos($model, $fileName));
- $modelSettings = $path . "metadata/" . $fileName . "_viewer";
+ /**
+ * Checks if the 3D viewer can be rendered.
+ *
+ * @return bool True if the 3D viewer can be rendered
+ */
+ private function is3dViewer($model): bool
+ {
+ return !empty($model);
+ }
- if (!empty($modelConverted)) {
- $model = $modelConverted;
- }
+ /**
+ * Builds the embedded 3D viewer url.
+ *
+ * @param string $model The model url
+ * @return string The embedded 3D viewer url
+ */
+ public function buildEmbedded3dViewerUrl(string $model): string
+ {
+ return self::MIDDLEWARE_DLF_EMBEDDED_3D_VIEWER_PREFIX . '&model=' . $model;
+ }
- if ($this->settings['useInternalProxy']) {
- $this->configureProxyUrl($model);
- $this->configureProxyUrl($xml);
- $this->configureProxyUrl($modelSettings);
- }
- $this->view->assign('model', $model);
- $this->view->assign('xml', $xml);
- $this->view->assign('settings', $modelSettings);
- $this->view->assign('proxy', $this->settings['useInternalProxy']);
- }
- }
}
diff --git a/Classes/Domain/Model/Annotation.php b/Classes/Domain/Model/Annotation.php
new file mode 100644
index 0000000000..47b3e8ba6f
--- /dev/null
+++ b/Classes/Domain/Model/Annotation.php
@@ -0,0 +1,263 @@
+
+ *
+ * This file is part of the Kitodo and TYPO3 projects.
+ *
+ * @license GNU General Public License version 3 or later.
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ */
+
+class Annotation
+{
+ /**
+ * The complete data of the annotation
+ *
+ * @var array
+ */
+ protected $data;
+
+ /**
+ * @var array
+ */
+ protected $targetPages;
+
+
+ /**
+ * @param array $data
+ */
+ public function __construct($data)
+ {
+ $this->data = $data;
+ }
+
+ /**
+ * Returns the full data of the annotation
+ *
+ * @return array
+ */
+ public function getRawData()
+ {
+ return $this->data;
+ }
+
+ /**
+ * Gets the annotation id
+ *
+ * @return string
+ */
+ public function getId()
+ {
+ return $this->data['id'] ?? '';
+ }
+
+ /**
+ * Gets the annotation title
+ *
+ * @return string
+ */
+ public function getTitle()
+ {
+ return $this->data['title'] ?? '';
+ }
+
+ /**
+ * Gets the annotation body data
+ *
+ * @return array
+ */
+ public function getBody()
+ {
+ $body = $this->data['body'] ?? '';
+
+ if (is_array($body)) {
+ return $body;
+ }
+
+ return [$body];
+ }
+
+ /**
+ * Gets the name of the annotation creator
+ * @return string
+ */
+ public function getCreatorName()
+ {
+ return $this->data['creator']['displayName'] ?? '';
+ }
+
+ /**
+ * Gets the creation date of the annotation
+ * @return string
+ */
+ public function getCreated()
+ {
+ return $this->data['created'] ?? '';
+ }
+
+ /**
+ * Gets the modification date of the annotation
+ * @return string
+ */
+ public function getModified()
+ {
+ return $this->data['modified'] ?? '';
+ }
+
+ /**
+ * Gets the targets
+ *
+ * @return AnnotationTarget[]
+ */
+ public function getTargets()
+ {
+ if (is_string($this->data['target'])) {
+ return [new AnnotationTarget($this->data['target'])];
+ }
+
+ $annotationTargets = [];
+ foreach ($this->data['target'] as $target) {
+ $annotationTargets[] = new AnnotationTarget($target);
+ }
+
+ return $annotationTargets;
+ }
+
+ /**
+ * Sets the target pages for which the annotation is relevant
+ *
+ * @param array $targetPages
+ * @return void
+ */
+ public function setTargetPages($targetPages)
+ {
+ $this->targetPages = $targetPages;
+ }
+
+ /**
+ * Gets the target pages for which the annotation is relevant
+ *
+ * @return array
+ */
+ public function getTargetPages()
+ {
+ return $this->targetPages;
+ }
+
+ /**
+ * Gets the page numbers for which the annotation is relevant
+ *
+ * @return array
+ */
+ public function getPageNumbers()
+ {
+ $pages = [];
+ if (is_array($this->targetPages)) {
+ foreach ($this->targetPages as $target) {
+ $pages = array_merge($pages, $target['pages']);
+ }
+ }
+
+ return $pages;
+ }
+
+ /**
+ * Gets the annotation targets ordered by page numbers
+ *
+ * @return array
+ */
+ public function getPageTargets()
+ {
+ $pageTargets = [];
+ if (is_array($this->targetPages)) {
+ foreach ($this->targetPages as $target) {
+ foreach ($target['pages'] as $page) {
+ $pageTargets[$page][$target['target']->getUrl()] = $target['target'];
+ }
+ }
+ }
+
+ return $pageTargets;
+ }
+
+ /**
+ * Gets the audio ranges from the annotation targets ordered by page number
+ *
+ * @return array
+ */
+ public function getPageAudioRanges()
+ {
+ $ranges = [];
+ if (is_array($this->getPageTargets())) {
+ foreach ($this->getPageTargets() as $pageNumber => $targets) {
+ foreach ($targets as $target) {
+ if ($target->isValid() && $target->isAudioRange()) {
+ $ranges[$pageNumber][] = $target->getRangeValue();
+ }
+ }
+ }
+ }
+ return $ranges;
+ }
+
+ /**
+ * Gets the score ranges from the annotation targets ordered by page number
+ *
+ * @return array
+ */
+ public function getPageScoreRanges()
+ {
+ $ranges = [];
+ if (is_array($this->getPageTargets())) {
+ foreach ($this->getPageTargets() as $pageNumber => $targets) {
+ foreach ($targets as $target) {
+ if ($target->isValid() && $target->isScoreRange()) {
+ $ranges[$pageNumber][] = $target->getRangeValue();
+ }
+ }
+ }
+ }
+ return $ranges;
+ }
+
+ /**
+ * Gets the facsimile ranges from the annotation targets ordered by page number
+ *
+ * @return array
+ */
+ public function getPageFacsimileRanges()
+ {
+ $ranges = [];
+ if (is_array($this->getPageTargets())) {
+ foreach ($this->getPageTargets() as $pageNumber => $targets) {
+ foreach ($targets as $target) {
+ if ($target->isValid() && $target->isFacsimileRange()) {
+ $ranges[$pageNumber][] = $target->getRangeValue();
+ }
+ }
+ }
+ }
+
+ return $ranges;
+ }
+
+ /**
+ * Returns if the annotation is relevant for verovio
+ *
+ * @return bool
+ */
+ public function isVerovioRelevant()
+ {
+ foreach ($this->targetPages as $target) {
+ if (array_key_exists('verovioRelevant', $target) && $target['verovioRelevant']) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/Classes/Domain/Model/AnnotationTarget.php b/Classes/Domain/Model/AnnotationTarget.php
new file mode 100644
index 0000000000..e63f3def98
--- /dev/null
+++ b/Classes/Domain/Model/AnnotationTarget.php
@@ -0,0 +1,154 @@
+
+ *
+ * This file is part of the Kitodo and TYPO3 projects.
+ *
+ * @license GNU General Public License version 3 or later.
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ */
+
+class AnnotationTarget
+{
+ /**
+ * @var string
+ */
+ protected $url;
+
+ /**
+ * @var string
+ */
+ protected $objectId;
+
+ /**
+ * @var string
+ */
+ protected $id;
+
+ /**
+ * @var string
+ */
+ protected $rangeParameterName;
+
+ /**
+ * @var string
+ */
+ protected $rangeValue;
+
+ public function __construct($url)
+ {
+ $this->url = $url;
+
+ $path = parse_url($url, PHP_URL_PATH);
+ $fragment = parse_url($url, PHP_URL_FRAGMENT);
+ list($objectId, $id) = explode('/', trim($path, '/'));
+ list($rangeParameterName, $rangeValue) = explode('=', $fragment);
+
+ $this->objectId = $objectId;
+ $this->id = $id;
+ $this->rangeParameterName = $rangeParameterName;
+ $this->rangeValue = preg_replace('/\s+/', '', $rangeValue);
+ }
+
+ /**
+ * @return string
+ */
+ public function getUrl(): string
+ {
+ return $this->url;
+ }
+
+ /**
+ * @return string
+ */
+ public function getObjectId()
+ {
+ return $this->objectId;
+ }
+
+ /**
+ * @return string
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * @return string
+ */
+ public function getRangeParameterName()
+ {
+ return $this->rangeParameterName;
+ }
+
+ /**
+ * @return string
+ */
+ public function getRangeValue()
+ {
+ return $this->rangeValue;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isValid()
+ {
+ if (empty($this->getObjectId())) {
+ return false;
+ }
+
+ if (parse_url($this->getUrl(), PHP_URL_FRAGMENT)) {
+ return !empty($this->getId()) && $this->isValidRange();
+ }
+
+ return true;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isValidRange()
+ {
+ if (empty($this->rangeParameterName) && empty($this->rangeValue)) {
+ return true;
+ } elseif ($this->isFacsimileRange()) {
+ return preg_match("/^(\d+)(,\d+){3}?$/", $this->rangeValue) === 1;
+ } elseif ($this->isAudioRange()) {
+ return preg_match("/^(?:\d+(?:\.\d*)?|\.\d+){0,1}(?:,(?:\d+(?:\.\d*)?|\.\d+))*$/", $this->rangeValue) === 1;
+ } elseif ($this->isScoreRange()) {
+ return preg_match("/^((\d+|start|end|all|(\d+|start)(-(\d+|end)){0,1})+)(,(\d+|start|end|all|(\d+|start)(-(\d+|end)){0,1})+){0,}?$/", $this->rangeValue) === 1;
+ }
+
+ return false;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isScoreRange()
+ {
+ return $this->getRangeParameterName() === 'measureRanges';
+ }
+
+ /**
+ * @return bool
+ */
+ public function isAudioRange()
+ {
+ return $this->getRangeParameterName() === 't';
+ }
+
+ /**
+ * @return bool
+ */
+ public function isFacsimileRange()
+ {
+ return $this->getRangeParameterName() === 'xywh';
+ }
+}
diff --git a/Classes/Domain/Model/FormAddDocument.php b/Classes/Domain/Model/FormAddDocument.php
new file mode 100644
index 0000000000..e1c06b95ab
--- /dev/null
+++ b/Classes/Domain/Model/FormAddDocument.php
@@ -0,0 +1,44 @@
+
+ *
+ * This file is part of the Kitodo and TYPO3 projects.
+ *
+ * @license GNU General Public License version 3 or later.
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ */
+
+namespace Kitodo\Dlf\Domain\Model;
+
+/**
+ * Form to load another document (multi page view)
+ *
+ * @package TYPO3
+ * @subpackage dlf
+ * @access public
+ */
+class FormAddDocument extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
+{
+ /**
+ * @var string
+ */
+ protected $location = '';
+
+ /**
+ * @return string
+ */
+ public function getLocation(): string
+ {
+ return $this->location;
+ }
+
+ /**
+ * @param string $location
+ */
+ public function setLocation(string $location): void
+ {
+ $this->location = $location;
+ }
+}
diff --git a/Classes/Domain/Model/MetadataFormat.php b/Classes/Domain/Model/MetadataFormat.php
index 30157c4296..ad40ec008a 100644
--- a/Classes/Domain/Model/MetadataFormat.php
+++ b/Classes/Domain/Model/MetadataFormat.php
@@ -12,6 +12,8 @@
namespace Kitodo\Dlf\Domain\Model;
+use TYPO3\CMS\Extbase\Annotation as Extbase;
+use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
/**
@@ -55,11 +57,36 @@ class MetadataFormat extends AbstractEntity
protected $xpathSorting;
/**
+ * Collection of ``tx_dlf_metadatasubentries`` specified with this metadata entry.
+ *
+ * @var \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\Kitodo\Dlf\Domain\Model\MetadataSubentry>
+ * @Extbase\ORM\Lazy
+ * @Extbase\ORM\Cascade("remove")
+ */
+ protected $subentries;
+
+ /**
+ * Whether or not the field is mandatory. Not used at the moment (originally planned to be used in METS validator).
+ *
+ * @var int
* @access protected
- * @var int Whether or not the field is mandatory. Not used at the moment (originally planned to be used in METS validator).
*/
protected $mandatory;
+ /**
+ * constructor
+ */
+ public function __construct()
+ {
+ // Do not remove the next line: It would break the functionality
+ $this->initStorageObjects();
+ }
+
+ protected function initStorageObjects()
+ {
+ $this->subentries = new ObjectStorage();
+ }
+
/**
* @return int
*/
@@ -124,6 +151,40 @@ public function setXpathSorting(string $xpathSorting): void
$this->xpathSorting = $xpathSorting;
}
+ public function getSubentries()
+ {
+ return $this->subentries;
+ }
+
+ public function setSubentries(ObjectStorage $subentries): void
+ {
+ $this->subentries = $subentries;
+ }
+
+ /**
+ * Adds a Subentry
+ *
+ * @param \Kitodo\Dlf\Domain\Model\MetadataSubentry $subentry
+ *
+ * @return void
+ */
+ public function addSubentry(MetadataSubentry $subentry)
+ {
+ $this->subentries->attach($subentry);
+ }
+
+ /**
+ * Removes a Subentry
+ *
+ * @param \Kitodo\Dlf\Domain\Model\MetadataSubentry $subentryToRemove
+ *
+ * @return void
+ */
+ public function removeSubentry(MetadataSubentry $subentryToRemove)
+ {
+ $this->subentries->detach($subentryToRemove);
+ }
+
/**
* @return int
*/
diff --git a/Classes/Domain/Model/MetadataSubentry.php b/Classes/Domain/Model/MetadataSubentry.php
new file mode 100644
index 0000000000..030222ac52
--- /dev/null
+++ b/Classes/Domain/Model/MetadataSubentry.php
@@ -0,0 +1,179 @@
+
+ *
+ * This file is part of the Kitodo and TYPO3 projects.
+ *
+ * @license GNU General Public License version 3 or later.
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ */
+
+namespace Kitodo\Dlf\Domain\Model;
+
+/**
+ * This specifies a way how a metadatum (``tx_dlf_metadata``) may be encoded in a specific data format (``tx_dlf_format``).
+ *
+ * For instance, the title of a document may be obtained from either the MODS
+ * title field, or from the TEIHDR caption. This is modeled as two ``tx_dlf_metadaformat``
+ * that refer to the same ``tx_dlf_metadata`` but different ``tx_dlf_format``.
+ *
+ * This contains the xpath expressions on the model 'Metadata'.
+ *
+ * @package TYPO3
+ * @subpackage dlf
+ * @access public
+ */
+class MetadataSubentry extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
+{
+ /**
+ * @var \Kitodo\Dlf\Domain\Model\MetadataSubentry
+ */
+ protected $l18nParent;
+
+ /**
+ * @var int
+ */
+ protected $sorting;
+
+ /**
+ * @var string
+ */
+ protected $label;
+
+ /**
+ * @var string
+ */
+ protected $indexName;
+
+ /**
+ * XPath/JSONPath expression to extract the metadatum (relative to the data format root).
+ * TODO
+ *
+ * @var string
+ */
+ protected $xpath;
+
+ /**
+ * @var string
+ */
+ protected $defaultValue;
+
+ /**
+ * @var string
+ */
+ protected $wrap;
+
+ /**
+ * @return \Kitodo\Dlf\Domain\Model\MetadataSubentry
+ */
+ public function getL18nParent(): MetadataSubentry
+ {
+ return $this->l18nParent;
+ }
+
+ /**
+ * @param \Kitodo\Dlf\Domain\Model\MetadataSubentry $l18nParent
+ */
+ public function setL18nParent(MetadataSubentry $l18nParent): void
+ {
+ $this->l18nParent = $l18nParent;
+ }
+
+ /**
+ * @return int
+ */
+ public function getSorting(): int
+ {
+ return $this->sorting;
+ }
+
+ /**
+ * @param int $sorting
+ */
+ public function setSorting(int $sorting): void
+ {
+ $this->sorting = $sorting;
+ }
+
+ /**
+ * @return string
+ */
+ public function getLabel(): string
+ {
+ return $this->label;
+ }
+
+ /**
+ * @param string $label
+ */
+ public function setLabel(string $label): void
+ {
+ $this->label = $label;
+ }
+
+ /**
+ * @return string
+ */
+ public function getIndexName(): string
+ {
+ return $this->indexName;
+ }
+
+ /**
+ * @param string $indexName
+ */
+ public function setIndexName(string $indexName): void
+ {
+ $this->indexName = $indexName;
+ }
+
+ /**
+ * @return string
+ */
+ public function getXpath(): string
+ {
+ return $this->xpath;
+ }
+
+ /**
+ * @param string $xpath
+ */
+ public function setXpath(string $xpath): void
+ {
+ $this->xpath = $xpath;
+ }
+
+ /**
+ * @return string
+ */
+ public function getDefaultValue(): string
+ {
+ return $this->defaultValue;
+ }
+
+ /**
+ * @param string $defaultValue
+ */
+ public function setDefaultValue(string $defaultValue): void
+ {
+ $this->defaultValue = $defaultValue;
+ }
+
+ /**
+ * @return string
+ */
+ public function getWrap(): string
+ {
+ return $this->wrap;
+ }
+
+ /**
+ * @param string $wrap
+ */
+ public function setWrap(string $wrap): void
+ {
+ $this->wrap = $wrap;
+ }
+}
diff --git a/Classes/Domain/Repository/MetadataSubentryRepository.php b/Classes/Domain/Repository/MetadataSubentryRepository.php
new file mode 100644
index 0000000000..e5c3d94e16
--- /dev/null
+++ b/Classes/Domain/Repository/MetadataSubentryRepository.php
@@ -0,0 +1,18 @@
+
+ *
+ * This file is part of the Kitodo and TYPO3 projects.
+ *
+ * @license GNU General Public License version 3 or later.
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ */
+
+namespace Kitodo\Dlf\Domain\Repository;
+
+class MetadataSubentryRepository extends \TYPO3\CMS\Extbase\Persistence\Repository
+{
+
+}
diff --git a/Classes/ExpressionLanguage/DocumentTypeFunctionProvider.php b/Classes/ExpressionLanguage/DocumentTypeFunctionProvider.php
index b9b374fa54..0e10cd30c7 100644
--- a/Classes/ExpressionLanguage/DocumentTypeFunctionProvider.php
+++ b/Classes/ExpressionLanguage/DocumentTypeFunctionProvider.php
@@ -54,10 +54,10 @@ public function getFunctions(): array
/**
* This holds the current document
*
- * @var Document|null
+ * @var Document
* @access protected
*/
- protected ?Document $document;
+ protected Document $document;
/**
* @var ConfigurationManager
@@ -73,7 +73,7 @@ public function injectConfigurationManager(ConfigurationManager $configurationMa
* @var DocumentRepository
*/
protected $documentRepository;
-
+
/**
* @param DocumentRepository $documentRepository
*/
@@ -98,10 +98,8 @@ protected function initializeRepositories(int $storagePid): void
$configurationManager = $objectManager->get(ConfigurationManager::class);
$this->injectConfigurationManager($configurationManager);
$frameworkConfiguration = $this->configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK);
-
$frameworkConfiguration['persistence']['storagePid'] = MathUtility::forceIntegerInRange((int) $storagePid, 0);
$this->configurationManager->setConfiguration($frameworkConfiguration);
-
$this->documentRepository = GeneralUtility::makeInstance(DocumentRepository::class);
}
@@ -136,7 +134,7 @@ function($arguments, $cPid)
// Load document with current plugin parameters.
$this->loadDocument($queryParams['tx_dlf'], $cPid);
- if ($this->document === null || $this->document->getCurrentDocument() === null) {
+ if (!isset($this->document) || $this->document->getCurrentDocument() === null) {
return $type;
}
// Set PID for metadata definitions.
@@ -171,9 +169,7 @@ protected function loadDocument(array $requestData, int $pid): void
{
// Try to get document format from database
if (!empty($requestData['id'])) {
-
$this->initializeRepositories($pid);
-
$doc = null;
if (MathUtility::canBeInterpretedAsInteger($requestData['id'])) {
// find document from repository by uid
@@ -183,34 +179,26 @@ protected function loadDocument(array $requestData, int $pid): void
} else {
$this->logger->error('Invalid UID "' . $requestData['id'] . '" or PID "' . $pid . '" for document loading');
}
- } else if (GeneralUtility::isValidUrl($requestData['id'])) {
-
+ } elseif (GeneralUtility::isValidUrl($requestData['id'])) {
$doc = AbstractDocument::getInstance($requestData['id'], ['storagePid' => $pid], true);
-
if ($doc !== null) {
if ($doc->recordId) {
$this->document = $this->documentRepository->findOneByRecordId($doc->recordId);
}
-
- if ($this->document === null) {
+ if (!isset($this->document)) {
// create new dummy Document object
$this->document = GeneralUtility::makeInstance(Document::class);
}
-
$this->document->setLocation($requestData['id']);
} else {
$this->logger->error('Invalid location given "' . $requestData['id'] . '" for document loading');
}
}
-
if ($this->document !== null && $doc !== null) {
$this->document->setCurrentDocument($doc);
}
-
} elseif (!empty($requestData['recordId'])) {
-
$this->document = $this->documentRepository->findOneByRecordId($requestData['recordId']);
-
if ($this->document !== null) {
$doc = AbstractDocument::getInstance($this->document->getLocation(), ['storagePid' => $pid], true);
if ($doc !== null) {
diff --git a/Classes/Format/Mods.php b/Classes/Format/Mods.php
index 271ae88c75..73721ac5e7 100644
--- a/Classes/Format/Mods.php
+++ b/Classes/Format/Mods.php
@@ -275,7 +275,7 @@ private function getHolderFromXml(array $holders, int $i): void
* Get holder from XML display form.
*
* @access private
- *
+ *
* @param array $holders
* @param int $i
*
@@ -284,7 +284,7 @@ private function getHolderFromXml(array $holders, int $i): void
private function getHolderFromXmlDisplayForm(array $holders, int $i): void
{
// Check if there is a display form.
- $displayForms = $holders[$i]->getDisplayForm();
+ $displayForms = $holders[$i]->getDisplayForms();
if ($displayForms) {
$this->metadata['holder'][$i] = $displayForms[0]->getValue();
}
diff --git a/Classes/Hooks/DataHandler.php b/Classes/Hooks/DataHandler.php
index 6304efcbd6..dde5ae9366 100644
--- a/Classes/Hooks/DataHandler.php
+++ b/Classes/Hooks/DataHandler.php
@@ -87,6 +87,7 @@ public function processDatamap_postProcessFieldArray(string $status, string $tab
}
break;
// Field post-processing for table "tx_dlf_metadata".
+ // TODO: Include also subentries if available.
case 'tx_dlf_metadata':
// Store field in index if it should appear in lists.
if (!empty($fieldArray['is_listed'])) {
diff --git a/Classes/Middleware/Embedded3DViewer.php b/Classes/Middleware/Embedded3DViewer.php
new file mode 100644
index 0000000000..67c3f65e3b
--- /dev/null
+++ b/Classes/Middleware/Embedded3DViewer.php
@@ -0,0 +1,235 @@
+
+ *
+ * This file is part of the Kitodo and TYPO3 projects.
+ *
+ * @license GNU General Public License version 3 or later.
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ */
+
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\MiddlewareInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+use Psr\Log\LoggerAwareTrait;
+use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
+use TYPO3\CMS\Core\Configuration\Loader\YamlFileLoader;
+use TYPO3\CMS\Core\Http\HtmlResponse;
+use TYPO3\CMS\Core\Resource\Folder;
+use TYPO3\CMS\Core\Resource\ResourceFactory;
+use TYPO3\CMS\Core\Resource\StorageRepository;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\PathUtility;
+use TYPO3\CMS\Frontend\Controller\ErrorController;
+
+/**
+ * Middleware for embedding custom 3D Viewer implementation of the 'dlf' extension.
+ *
+ * @package TYPO3
+ * @subpackage dlf
+ * @access public
+ */
+class Embedded3DViewer implements MiddlewareInterface
+{
+ use LoggerAwareTrait;
+
+ const VIEWER_FOLDER = "dlf_3d_viewers";
+ const VIEWER_CONFIG_YML = "dlf-3d-viewer.yml";
+ const EXT_KEY = "dlf";
+
+ /**
+ * The main method of the middleware.
+ *
+ * @access public
+ *
+ * @param ServerRequestInterface $request for processing
+ * @param RequestHandlerInterface $handler for processing
+ *
+ * @return ResponseInterface
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+ {
+ $response = $handler->handle($request);
+ // parameters are sent by POST --> use getParsedBody() instead of getQueryParams()
+ $parameters = $request->getQueryParams();
+ // Return if not this middleware
+ if (!isset($parameters['middleware']) || ($parameters['middleware'] != 'dlf/embedded3DViewer')) {
+ return $response;
+ }
+
+ if (empty($parameters['model'])) {
+ return $this->warningResponse('Model url is missing.', $request);
+ }
+
+ $modelInfo = PathUtility::pathinfo($parameters['model']);
+ $modelFormat = $modelInfo["extension"];
+ if (empty($modelFormat)) {
+ return $this->warningResponse('Model path "' . $parameters['model'] . '" has no extension format', $request);
+ }
+
+ if (empty($parameters['viewer'])) {
+ // determine viewer from extension configuration
+ $viewer = $this->getViewerByExtensionConfiguration($modelFormat);
+ } else {
+ $viewer = $parameters['viewer'];
+ }
+
+ if (empty($viewer)) {
+ return $this->renderDefaultViewer($parameters['model']);
+ }
+
+ /** @var StorageRepository $storageRepository */
+ $storageRepository = GeneralUtility::makeInstance(StorageRepository::class);
+ $defaultStorage = $storageRepository->getDefaultStorage();
+
+ if (!$defaultStorage->hasFolder(self::VIEWER_FOLDER)) {
+ return $this->errorResponse('Required folder "' . self::VIEWER_FOLDER . '" was not found in the default storage "' . $defaultStorage->getName() . '"', $request);
+ }
+
+ $viewerModules = $defaultStorage->getFolder(self::VIEWER_FOLDER);
+ if (!$viewerModules->hasFolder($viewer)) {
+ return $this->errorResponse('Viewer folder "' . $viewer . '" was not found under the folder "' . self::VIEWER_FOLDER . '"', $request);
+ }
+
+ $viewerFolder = $viewerModules->getSubfolder($viewer);
+ if (!$viewerFolder->hasFile(self::VIEWER_CONFIG_YML)) {
+ return $this->errorResponse('Viewer folder "' . $viewer . '" does not contain a file named "' . self::VIEWER_CONFIG_YML . '"', $request);
+ }
+
+ /** @var YamlFileLoader $yamlFileLoader */
+ $yamlFileLoader = GeneralUtility::makeInstance(YamlFileLoader::class);
+ $viewerConfigPath = $defaultStorage->getName() . "/" . self::VIEWER_FOLDER . "/" . $viewer . "/";
+ $config = $yamlFileLoader->load($viewerConfigPath . self::VIEWER_CONFIG_YML)["viewer"];
+
+ if (!isset($config["supportedModelFormats"]) || empty($config["supportedModelFormats"])) {
+ return $this->errorResponse('Required key "supportedModelFormats" does not exist in the file "' . self::VIEWER_CONFIG_YML . '" of viewer "' . $viewer . '" or has no value', $request);
+ }
+
+ if (array_search(strtolower($modelFormat), array_map('strtolower', $config["supportedModelFormats"])) === false) {
+ return $this->warningResponse('Viewer "' . $viewer . '" does not support the model format "' . $modelFormat . '"', $request);
+ }
+
+ $html = $this->getViewerHtml($config, $viewerConfigPath, $viewerFolder, $parameters['model'], $modelInfo);
+ return new HtmlResponse($html);
+ }
+
+ /**
+ * Build the error response.
+ *
+ * Logs the given message as error and return internal error response.
+ *
+ * @param string $message
+ * @param ServerRequestInterface $request
+ * @return ResponseInterface
+ * @throws \TYPO3\CMS\Core\Error\Http\InternalServerErrorException
+ */
+ public function errorResponse(string $message, ServerRequestInterface $request): ResponseInterface
+ {
+ /** @var ErrorController $errorController */
+ $errorController = GeneralUtility::makeInstance(ErrorController::class);
+ $this->logger->error($message);
+ return $errorController->internalErrorAction($request, $message);
+ }
+
+ /**
+ * Build the warning response.
+ *
+ * Logs the given message as warning and return page not found response.
+ *
+ * @param string $message
+ * @param ServerRequestInterface $request
+ * @return ResponseInterface
+ * @throws \TYPO3\CMS\Core\Error\Http\PageNotFoundException
+ */
+ public function warningResponse(string $message, ServerRequestInterface $request): ResponseInterface
+ {
+ /** @var ErrorController $errorController */
+ $errorController = GeneralUtility::makeInstance(ErrorController::class);
+ $this->logger->warning($message);
+ return $errorController->pageNotFoundAction($request, $message);
+ }
+
+ /**
+ * Determines the viewer based on the extension configuration and the given model format.
+ *
+ * @param $modelFormat string The model format
+ * @return string The 3D viewer
+ */
+ private function getViewerByExtensionConfiguration($modelFormat): string
+ {
+ $extConf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get(self::EXT_KEY, '3dviewer');
+ $viewerModelFormatMappings = explode(";", $extConf['viewerModelFormatMapping']);
+ foreach ($viewerModelFormatMappings as $viewerModelFormatMapping) {
+ $explodedViewerModelMapping = explode(":", $viewerModelFormatMapping);
+ if (count($explodedViewerModelMapping) == 2) {
+ $viewer = trim($explodedViewerModelMapping[0]);
+ $viewerModelFormats = array_map('trim', explode(",", $explodedViewerModelMapping[1]));
+ if (in_array($modelFormat, $viewerModelFormats)) {
+ return $viewer;
+ }
+ }
+ }
+
+ return $extConf['defaultViewer'] ?? "";
+ }
+
+ /**
+ * @param string $viewerUrl
+ * @param string $html
+ * @param string $modelUrl
+ * @param array $modelInfo
+ * @return string
+ */
+ public function replacePlaceholders(string $viewerUrl, string $html, $modelUrl, array $modelInfo): string
+ {
+ $html = str_replace("{{viewerPath}}", $viewerUrl, $html);
+ $html = str_replace("{{modelUrl}}", $modelUrl, $html);
+ $html = str_replace("{{modelPath}}", $modelInfo["dirname"], $html);
+ return str_replace("{{modelResource}}", $modelInfo["basename"], $html);
+ }
+
+ /**
+ * @param $model
+ * @return HtmlResponse
+ * @throws \TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException
+ */
+ public function renderDefaultViewer($model): HtmlResponse
+ {
+ /** @var ResourceFactory $resourceFactory */
+ $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
+ $html = $resourceFactory->retrieveFileOrFolderObject('EXT:dlf/Resources/Private/Templates/View3D/Standalone.html')->getContents();
+ $file = $resourceFactory->retrieveFileOrFolderObject('EXT:dlf/Resources/Public/JavaScript/3DViewer/model-viewer-3.5.0.min.js');
+ $html = str_replace('{{modelViewerJS}}', $file->getPublicUrl(), $html);
+ $html = str_replace("{{modelUrl}}", $model, $html);
+ return new HtmlResponse($html);
+ }
+
+ /**
+ * @param array $config
+ * @param string $viewerConfigPath
+ * @param Folder $viewerFolder
+ * @param string $modelUrl
+ * @param array $modelInfo
+ * @return string
+ */
+ public function getViewerHtml(array $config, string $viewerConfigPath, Folder $viewerFolder, string $modelUrl, array $modelInfo): string
+ {
+ $htmlFile = "index.html";
+ if (isset($config["base"]) && !empty($config["base"])) {
+ $htmlFile = $config["base"];
+ }
+
+ $viewerUrl = $viewerConfigPath;
+ if (isset($config["url"]) && !empty($config["url"])) {
+ $viewerUrl = rtrim($config["url"]);
+ }
+
+ $html = $viewerFolder->getFile($htmlFile)->getContents();
+ return $this->replacePlaceholders($viewerUrl, $html, $modelUrl, $modelInfo);
+ }
+}
diff --git a/Classes/Task/BaseAdditionalFieldProvider.php b/Classes/Task/BaseAdditionalFieldProvider.php
new file mode 100644
index 0000000000..455360f6ed
--- /dev/null
+++ b/Classes/Task/BaseAdditionalFieldProvider.php
@@ -0,0 +1,304 @@
+
+ *
+ * This file is part of the Kitodo and TYPO3 projects.
+ *
+ * @license GNU General Public License version 3 or later.
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ */
+namespace Kitodo\Dlf\Task;
+
+use Kitodo\Dlf\Common\Helper;
+use TYPO3\CMS\Backend\Tree\Repository\PageTreeRepository;
+use TYPO3\CMS\Core\Database\Connection;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Messaging\FlashMessage;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface;
+use TYPO3\CMS\Scheduler\Controller\SchedulerModuleController;
+use TYPO3\CMS\Scheduler\Task\AbstractTask;
+
+/**
+ * Base class for additional fields classes of scheduler tasks.
+ *
+ * @package TYPO3
+ * @subpackage dlf
+ *
+ * @access public
+ */
+class BaseAdditionalFieldProvider implements AdditionalFieldProviderInterface
+{
+ /**
+ * Gets additional fields to render in the form to add/edit a task
+ *
+ * @param array $taskInfo Values of the fields from the add/edit task form
+ * @param \TYPO3\CMS\Scheduler\Task\AbstractTask $task The task object being edited. Null when adding a task!
+ * @param \TYPO3\CMS\Scheduler\Controller\SchedulerModuleController $schedulerModule Reference to the scheduler backend module
+ * @return array A two dimensional array, array('Identifier' => array('fieldId' => array('code' => '', 'label' => '', 'cshKey' => '', 'cshLabel' => ''))
+ */
+ public function getAdditionalFields(array &$taskInfo, $task, SchedulerModuleController $schedulerModule)
+ {
+ return [];
+ }
+
+ /**
+ * Validates the additional fields' values
+ *
+ * @param array $submittedData An array containing the data submitted by the add/edit task form
+ * @param \TYPO3\CMS\Scheduler\Controller\SchedulerModuleController $schedulerModule Reference to the scheduler backend module
+ * @return bool TRUE if validation was ok (or selected class is not relevant), FALSE otherwise
+ */
+ public function validateAdditionalFields(array &$submittedData, SchedulerModuleController $schedulerModule)
+ {
+ $fieldsValid = true;
+
+ Helper::getLanguageService()->includeLLFile('EXT:dlf/Resources/Private/Language/locallang_tasks.xlf');
+
+ $messageTitle = Helper::getLanguageService()->getLL('additionalFields.error');
+ $messageSeverity = FlashMessage::ERROR;
+
+ if (isset($submittedData['doc']) && empty($submittedData['doc'])) {
+ Helper::addMessage(
+ Helper::getLanguageService()->getLL('additionalFields.doc') . ' ' . Helper::getLanguageService()->getLL('additionalFields.valid'),
+ $messageTitle,
+ $messageSeverity,
+ true,
+ 'core.template.flashMessages'
+ );
+ $fieldsValid = false;
+ }
+
+ if ((isset($submittedData['pid']) && (int) $submittedData['pid'] <= 0) || !isset($submittedData['pid'])) {
+ Helper::addMessage(
+ Helper::getLanguageService()->getLL('additionalFields.pid') . ' ' . Helper::getLanguageService()->getLL('additionalFields.valid'),
+ $messageTitle,
+ $messageSeverity,
+ true,
+ 'core.template.flashMessages'
+ );
+ $fieldsValid = false;
+ }
+
+ if (!$submittedData['uid']) {
+ $messageTitle = Helper::getLanguageService()->getLL('additionalFields.warning');
+ $messageSeverity = FlashMessage::WARNING;
+ }
+
+ if ((isset($submittedData['lib']) && (int) $submittedData['lib'] <= 0)) {
+ Helper::addMessage(
+ Helper::getLanguageService()->getLL('additionalFields.lib') . ' ' . Helper::getLanguageService()->getLL('additionalFields.valid'),
+ $messageTitle,
+ $messageSeverity,
+ true,
+ 'core.template.flashMessages'
+ );
+ $fieldsValid = false;
+ }
+
+ if ((isset($submittedData['solr']) && (int) $submittedData['solr'] <= 0) || !isset($submittedData['solr'])) {
+ Helper::addMessage(
+ Helper::getLanguageService()->getLL('additionalFields.solr') . ' ' . Helper::getLanguageService()->getLL('additionalFields.valid'),
+ $messageTitle,
+ $messageSeverity,
+ true,
+ 'core.template.flashMessages'
+ );
+ $fieldsValid = false;
+ }
+
+ if (((isset($submittedData['coll']) && isset($submittedData['all'])) || (!isset($submittedData['coll']) && !isset($submittedData['all'])))
+ && !isset($submittedData['doc']) && !isset($submittedData['lib'])) {
+ Helper::addMessage(
+ Helper::getLanguageService()->getLL('additionalFields.collOrAll'),
+ $messageTitle,
+ $messageSeverity,
+ true,
+ 'core.template.flashMessages'
+ );
+ $fieldsValid = false;
+ }
+ return $fieldsValid;
+ }
+
+ /**
+ * Takes care of saving the additional fields' values in the task's object
+ *
+ * @param array $submittedData An array containing the data submitted by the add/edit task form
+ * @param BaseTask $task Reference to the scheduler backend module
+ * @return void
+ */
+ public function saveAdditionalFields(array $submittedData, AbstractTask $task)
+ {
+ /** @var BaseTask $task */
+ $task->setDryRun(!empty($submittedData['dryRun']));
+ if (isset($submittedData['doc'])) {
+ $task->setDoc(htmlspecialchars($submittedData['doc']));
+ }
+ if (isset($submittedData['lib'])) {
+ $task->setLib((int) $submittedData['lib']);
+ }
+ if (isset($submittedData['coll']) && is_array($submittedData['coll'])) {
+ $task->setColl($submittedData['coll']);
+ } else {
+ $task->setColl([]);
+ }
+ if (isset($submittedData['pid'])) {
+ $task->setPid((int) $submittedData['pid']);
+ }
+ if (isset($submittedData['solr'])) {
+ $task->setSolr((int) $submittedData['solr']);
+ }
+ if (isset($submittedData['owner'])) {
+ $task->setOwner(htmlspecialchars($submittedData['owner']));
+ }
+ $task->setAll(!empty($submittedData['all']));
+ if (isset($submittedData['from'])) {
+ $task->setFrom(htmlspecialchars($submittedData['from']));
+ }
+ if (isset($submittedData['until'])) {
+ $task->setUntil(htmlspecialchars($submittedData['until']));
+ }
+ if (isset($submittedData['set'])) {
+ $task->setSet(htmlspecialchars($submittedData['set']));
+ }
+ }
+
+ /**
+ * Return HTML for dry run checkbox
+ *
+ * @access protected
+ *
+ * @param bool $dryRun
+ *
+ * @return array additional field dry run checkbox
+ */
+ protected function getDryRunField(bool $dryRun): array
+ {
+ $fieldName = 'dryRun';
+ $fieldId = 'task_' . $fieldName;
+ $fieldHtml = '';
+ return [
+ 'code' => $fieldHtml,
+ 'label' => 'LLL:EXT:dlf/Resources/Private/Language/locallang_tasks.xlf:additionalFields.dryRun',
+ 'cshKey' => '_MOD_system_txschedulerM1',
+ 'cshLabel' => $fieldId
+ ];
+ }
+
+ /**
+ * Return HTML for solr dropdown
+ *
+ * @access protected
+ *
+ * @param int $solr UID of the selected Solr core
+ * @param int $pid UID of the selected storage page
+ *
+ * @return array additional field solr dropdown
+ */
+ protected function getSolrField(int $solr, int $pid): array
+ {
+ $fieldName = 'solr';
+ $fieldId = 'task_' . $fieldName;
+
+ $allSolrCores = $this->getSolrCores($pid);
+ $options = [];
+ $options[] = '';
+ foreach ($allSolrCores as $label => $uid) {
+ $options[] = '';
+ };
+ $fieldHtml = '';
+ return [
+ 'code' => $fieldHtml,
+ 'label' => 'LLL:EXT:dlf/Resources/Private/Language/locallang_tasks.xlf:additionalFields.solr',
+ 'cshKey' => '_MOD_system_txschedulerM1',
+ 'cshLabel' => $fieldId
+ ];
+ }
+
+ /**
+ * Return html for page dropdown
+ *
+ * @access protected
+ *
+ * @param int $pid UID of the selected storage page
+ *
+ * @return array additional field storage page dropdown
+ */
+ protected function getPidField(int $pid): array
+ {
+ $fieldName = 'pid';
+ $fieldId = 'task_' . $fieldName;
+
+ $pageRepository = GeneralUtility::makeInstance(PageTreeRepository::class);
+ $pages = $pageRepository->getTree(0);
+
+ $options = [];
+ foreach ($pages['_children'] as $page) {
+ if ($page['doktype'] == 254) {
+ $options[] = '';
+ }
+ }
+
+ $fieldHtml = '';
+ return [
+ 'code' => $fieldHtml,
+ 'label' => 'LLL:EXT:dlf/Resources/Private/Language/locallang_tasks.xlf:additionalFields.pid',
+ 'cshKey' => '_MOD_system_txschedulerM1',
+ 'cshLabel' => $fieldId
+ ];
+ }
+
+ /**
+ * Return HTML for owner text field
+ *
+ * @access protected
+ *
+ * @param string $owner registered owner
+ *
+ * @return array additional field owner text field
+ */
+ protected function getOwnerField(string $owner): array
+ {
+ $fieldName = 'owner';
+ $fieldId = 'task_' . $fieldName;
+ $fieldHtml = '';
+ return [
+ 'code' => $fieldHtml,
+ 'label' => 'LLL:EXT:dlf/Resources/Private/Language/locallang_tasks.xlf:additionalFields.owner',
+ 'cshKey' => '_MOD_system_txschedulerM1',
+ 'cshLabel' => $fieldId
+ ];
+ }
+
+ /**
+ * Fetches all Solr cores on given page.
+ *
+ * @access protected
+ *
+ * @param int $pid UID of storage page
+ *
+ * @return array Array of valid Solr cores
+ */
+ private function getSolrCores(int $pid): array
+ {
+ $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tx_dlf_solrcores');
+
+ $solrCores = [];
+ $result = $queryBuilder->select('uid', 'label')
+ ->from('tx_dlf_solrcores')
+ ->where(
+ $queryBuilder->expr()
+ ->eq('pid', $queryBuilder->createNamedParameter((int) $pid, Connection::PARAM_INT))
+ )
+ ->execute();
+
+ while ($record = $result->fetchAssociative()) {
+ $solrCores[$record['label']] = $record['uid'];
+ }
+
+ return $solrCores;
+ }
+}
diff --git a/Classes/Task/BaseTask.php b/Classes/Task/BaseTask.php
new file mode 100644
index 0000000000..9f6548779e
--- /dev/null
+++ b/Classes/Task/BaseTask.php
@@ -0,0 +1,326 @@
+
+ *
+ * This file is part of the Kitodo and TYPO3 projects.
+ *
+ * @license GNU General Public License version 3 or later.
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ */
+namespace Kitodo\Dlf\Task;
+
+use Kitodo\Dlf\Common\Helper;
+use TYPO3\CMS\Core\Messaging\FlashMessage;
+use TYPO3\CMS\Scheduler\Task\AbstractTask;
+
+/**
+ * Base class for Scheduler task classes.
+ *
+ * @package TYPO3
+ * @subpackage dlf
+ *
+ * @access public
+ */
+class BaseTask extends AbstractTask
+{
+
+ /**
+ * @access protected
+ * @var bool
+ */
+ protected bool $dryRun = false;
+
+ /**
+ * @access protected
+ * @var string
+ */
+ protected string $doc = 'https://';
+
+ /**
+ * @access protected
+ * @var int
+ */
+ protected int $lib = - 1;
+
+ /**
+ * @access protected
+ * @var int
+ */
+ protected int $pid = - 1;
+
+ /**
+ * @access protected
+ * @var array
+ */
+ protected array $coll = [];
+
+ /**
+ * @access protected
+ * @var int
+ */
+ protected int $solr = 0;
+
+ /**
+ * @access protected
+ * @var string
+ */
+ protected string $owner = '';
+
+ /**
+ * @access protected
+ * @var bool
+ */
+ protected bool $all = false;
+
+ /**
+ * @access protected
+ * @var string
+ */
+ protected string $from = '';
+
+ /**
+ * @access protected
+ * @var string
+ */
+ protected string $until = '';
+
+ /**
+ * @access protected
+ * @var string
+ */
+ protected string $set = '';
+
+ public function execute()
+ {
+ return true;
+ }
+
+ /**
+ *
+ * @return bool
+ */
+ public function isDryRun(): bool
+ {
+ return $this->dryRun;
+ }
+
+ /**
+ *
+ * @param bool $dryRun
+ */
+ public function setDryRun(bool $dryRun): void
+ {
+ $this->dryRun = $dryRun;
+ }
+
+ /**
+ *
+ * @return string
+ */
+ public function getDoc(): string
+ {
+ return $this->doc;
+ }
+
+ /**
+ *
+ * @param string $doc
+ */
+ public function setDoc(string $doc): void
+ {
+ $this->doc = $doc;
+ }
+
+ /**
+ *
+ * @return int
+ */
+ public function getLib(): int
+ {
+ return $this->lib;
+ }
+
+ /**
+ *
+ * @param int $lib
+ */
+ public function setLib(int $lib): void
+ {
+ $this->lib = $lib;
+ }
+
+ /**
+ *
+ * @return int
+ */
+ public function getPid(): int
+ {
+ return $this->pid;
+ }
+
+ /**
+ *
+ * @param int $pid
+ */
+ public function setPid(int $pid): void
+ {
+ $this->pid = $pid;
+ }
+
+ /**
+ *
+ * @return array
+ */
+ public function getColl(): array
+ {
+ return $this->coll;
+ }
+
+ /**
+ *
+ * @param array $coll
+ */
+ public function setColl(array $coll): void
+ {
+ $this->coll = $coll;
+ }
+
+ /**
+ *
+ * @return int
+ */
+ public function getSolr(): int
+ {
+ return $this->solr;
+ }
+
+ /**
+ *
+ * @param int $solr
+ */
+ public function setSolr(int $solr): void
+ {
+ $this->solr = $solr;
+ }
+
+ /**
+ *
+ * @return string
+ */
+ public function getOwner(): string
+ {
+ return $this->owner;
+ }
+
+ /**
+ *
+ * @param string $owner
+ */
+ public function setOwner(string $owner): void
+ {
+ $this->owner = $owner;
+ }
+
+ /**
+ *
+ * @return bool
+ */
+ public function isAll(): bool
+ {
+ return $this->all;
+ }
+
+ /**
+ *
+ * @param bool $all
+ */
+ public function setAll(bool $all): void
+ {
+ $this->all = $all;
+ }
+
+ /**
+ *
+ * @return string
+ */
+ public function getFrom(): string
+ {
+ return $this->from;
+ }
+
+ /**
+ *
+ * @param string $from
+ */
+ public function setFrom(string $from): void
+ {
+ $this->from = $from;
+ }
+
+ /**
+ *
+ * @return string
+ */
+ public function getUntil(): string
+ {
+ return $this->until;
+ }
+
+ /**
+ *
+ * @param string $until
+ */
+ public function setUntil(string $until): void
+ {
+ $this->until = $until;
+ }
+
+ /**
+ *
+ * @return string
+ */
+ public function getSet(): string
+ {
+ return $this->set;
+ }
+
+ /**
+ *
+ * @param string $set
+ */
+ public function setSet(string $set): void
+ {
+ $this->set = $set;
+ }
+
+ /**
+ * Generates and adds flash messages based on a string seperated by PHP_EOL.
+ *
+ * @access protected
+ *
+ * @param string $message Messages seperated by PHP_EOL
+ * @param int $severity
+ *
+ * @return void
+ */
+ protected function outputFlashMessages(string $message, int $severity): void
+ {
+ $messages = explode(PHP_EOL, $message);
+
+ foreach ($messages as $message) {
+ if (empty($message) || (substr_count($message, '=') == strlen($message))) {
+ continue;
+ }
+
+ Helper::addMessage(
+ $message,
+ '',
+ $severity == FlashMessage::ERROR ? FlashMessage::ERROR : FlashMessage::OK,
+ true,
+ 'core.template.flashMessages'
+ );
+ }
+ }
+}
diff --git a/Classes/Task/HarvestAdditionalFieldProvider.php b/Classes/Task/HarvestAdditionalFieldProvider.php
new file mode 100644
index 0000000000..446f6fe424
--- /dev/null
+++ b/Classes/Task/HarvestAdditionalFieldProvider.php
@@ -0,0 +1,155 @@
+
+ *
+ * This file is part of the Kitodo and TYPO3 projects.
+ *
+ * @license GNU General Public License version 3 or later.
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ */
+namespace Kitodo\Dlf\Task;
+
+use TYPO3\CMS\Core\Database\Connection;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Scheduler\Controller\SchedulerModuleController;
+use TYPO3\CMS\Scheduler\Task\Enumeration\Action;
+
+/**
+ * Additional fields for harvest documents task.
+ *
+ * @package TYPO3
+ * @subpackage dlf
+ *
+ * @access public
+ */
+class HarvestAdditionalFieldProvider extends BaseAdditionalFieldProvider
+{
+ /**
+ * Gets additional fields to render in the form to add/edit a task
+ *
+ * @param array $taskInfo Values of the fields from the add/edit task form
+ * @param BaseTask $task The task object being edited. Null when adding a task!
+ * @param \TYPO3\CMS\Scheduler\Controller\SchedulerModuleController $schedulerModule Reference to the scheduler backend module
+ * @return array A two dimensional array, array('Identifier' => array('fieldId' => array('code' => '', 'label' => '', 'cshKey' => '', 'cshLabel' => ''))
+ */
+ public function getAdditionalFields(array &$taskInfo, $task, SchedulerModuleController $schedulerModule)
+ {
+ $currentSchedulerModuleAction = $schedulerModule->getCurrentAction();
+
+ /** @var BaseTask $task */
+ if ($currentSchedulerModuleAction->equals(Action::EDIT)) {
+ $taskInfo['dryRun'] = $task->isDryRun();
+ $taskInfo['lib'] = $task->getLib();
+ $taskInfo['pid'] = $task->getPid();
+ $taskInfo['solr'] = $task->getSolr();
+ $taskInfo['from'] = $task->getFrom();
+ $taskInfo['until'] = $task->getUntil();
+ $taskInfo['set'] = $task->getSet();
+ } else {
+ $taskInfo['dryRun'] = false;
+ $taskInfo['lib'] = - 1;
+ $taskInfo['pid'] = - 1;
+ $taskInfo['solr'] = - 1;
+ $taskInfo['from'] = '';
+ $taskInfo['until'] = '';
+ $taskInfo['set'] = '';
+ }
+
+ $additionalFields = [];
+
+ // Checkbox for dry-run
+ $additionalFields['dryRun'] = $this->getDryRunField($taskInfo['dryRun']);
+
+ // Text field for library
+ $fieldName = 'lib';
+ $fieldId = 'task_' . $fieldName;
+
+ $allLibraries = $this->getLibraries($taskInfo['pid']);
+ $options = [];
+ $options[] = '';
+ foreach ($allLibraries as $label => $uid) {
+ $options[] = '';
+ }
+
+ $fieldHtml = '';
+ $additionalFields[$fieldId] = [
+ 'code' => $fieldHtml,
+ 'label' => 'LLL:EXT:dlf/Resources/Private/Language/locallang_tasks.xlf:additionalFields.lib',
+ 'cshKey' => '_MOD_system_txschedulerM1',
+ 'cshLabel' => $fieldId
+ ];
+
+ // DropDown for Pid
+ $additionalFields['pid'] = $this->getPidField($taskInfo['pid']);
+
+ // DropDown for Solr core
+ $additionalFields['solr'] = $this->getSolrField($taskInfo['solr'], $taskInfo['pid']);
+
+ // Text field for from
+ $fieldName = 'from';
+ $fieldId = 'task_' . $fieldName;
+ $fieldHtml = '';
+ $additionalFields[$fieldId] = [
+ 'code' => $fieldHtml,
+ 'label' => 'LLL:EXT:dlf/Resources/Private/Language/locallang_tasks.xlf:additionalFields.from',
+ 'cshKey' => '_MOD_system_txschedulerM1',
+ 'cshLabel' => $fieldId
+ ];
+
+ // Text field for until
+ $fieldName = 'until';
+ $fieldId = 'task_' . $fieldName;
+ $fieldHtml = '';
+ $additionalFields[$fieldId] = [
+ 'code' => $fieldHtml,
+ 'label' => 'LLL:EXT:dlf/Resources/Private/Language/locallang_tasks.xlf:additionalFields.until',
+ 'cshKey' => '_MOD_system_txschedulerM1',
+ 'cshLabel' => $fieldId
+ ];
+
+ // Text field for set
+ $fieldName = 'set';
+ $fieldId = 'task_' . $fieldName;
+ $fieldHtml = '';
+ $additionalFields[$fieldId] = [
+ 'code' => $fieldHtml,
+ 'label' => 'LLL:EXT:dlf/Resources/Private/Language/locallang_tasks.xlf:additionalFields.set',
+ 'cshKey' => '_MOD_system_txschedulerM1',
+ 'cshLabel' => $fieldId
+ ];
+
+ return $additionalFields;
+ }
+
+ /**
+ * Fetches all libraries from given page.
+ *
+ * @access private
+ *
+ * @param int $pid The UID of the storage page
+ *
+ * @return array Array of libraries
+ */
+ private function getLibraries(int $pid): array
+ {
+ $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tx_dlf_libraries');
+
+ $libraries = [];
+ $result = $queryBuilder->select('uid', 'label')
+ ->from('tx_dlf_libraries')
+ ->where(
+ $queryBuilder->expr()
+ ->eq('pid', $queryBuilder->createNamedParameter((int) $pid, Connection::PARAM_INT))
+ )
+ ->execute();
+
+ while ($record = $result->fetchAssociative()) {
+ $libraries[$record['label']] = $record['uid'];
+ }
+
+ return $libraries;
+ }
+}
diff --git a/Classes/Task/HarvestTask.php b/Classes/Task/HarvestTask.php
new file mode 100644
index 0000000000..7960529b80
--- /dev/null
+++ b/Classes/Task/HarvestTask.php
@@ -0,0 +1,62 @@
+
+ *
+ * This file is part of the Kitodo and TYPO3 projects.
+ *
+ * @license GNU General Public License version 3 or later.
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ */
+namespace Kitodo\Dlf\Task;
+
+use TYPO3\CMS\Core\Core\Environment;
+use TYPO3\CMS\Core\Messaging\FlashMessage;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Task for harvesting documents.
+ *
+ * @package TYPO3
+ * @subpackage dlf
+ *
+ * @access public
+ */
+class HarvestTask extends BaseTask
+{
+ public function execute()
+ {
+ $inputArray = [];
+ if ($this->dryRun) {
+ $inputArray['--dry-run'] = true;
+ }
+ $inputArray['-l'] = $this->lib;
+ $inputArray['-p'] = $this->pid;
+ $inputArray['-s'] = $this->solr;
+ if (!empty($this->from)) {
+ $inputArray['--from'] = $this->from;
+ }
+ if (!empty($this->until)) {
+ $inputArray['--until'] = $this->until;
+ }
+ if (!empty($this->set)) {
+ $inputArray['--set'] = $this->set;
+ }
+
+ $harvestCommand = GeneralUtility::makeInstance(\Kitodo\Dlf\Command\HarvestCommand::class);
+ $inputInterface = GeneralUtility::makeInstance(\Symfony\Component\Console\Input\ArrayInput::class, $inputArray);
+ if (Environment::isCli()) {
+ $outputInterface = GeneralUtility::makeInstance(\Symfony\Component\Console\Output\ConsoleOutput::class);
+ } else {
+ $outputInterface = GeneralUtility::makeInstance(\Symfony\Component\Console\Output\BufferedOutput::class);
+ }
+
+ $return = $harvestCommand->run($inputInterface, $outputInterface);
+
+ if (!Environment::isCli()) {
+ $this->outputFlashMessages($outputInterface->fetch(), $return ? FlashMessage::ERROR : FlashMessage::OK);
+ }
+ return !$return;
+ }
+}
diff --git a/Classes/Task/IndexAdditionalFieldProvider.php b/Classes/Task/IndexAdditionalFieldProvider.php
new file mode 100644
index 0000000000..aee9ec2c50
--- /dev/null
+++ b/Classes/Task/IndexAdditionalFieldProvider.php
@@ -0,0 +1,82 @@
+
+ *
+ * This file is part of the Kitodo and TYPO3 projects.
+ *
+ * @license GNU General Public License version 3 or later.
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ */
+namespace Kitodo\Dlf\Task;
+
+use TYPO3\CMS\Scheduler\Controller\SchedulerModuleController;
+use TYPO3\CMS\Scheduler\Task\AbstractTask;
+use TYPO3\CMS\Scheduler\Task\Enumeration\Action;
+
+/**
+ * Additional fields for index document task.
+ *
+ * @package TYPO3
+ * @subpackage dlf
+ *
+ * @access public
+ */
+class IndexAdditionalFieldProvider extends BaseAdditionalFieldProvider
+{
+ /**
+ * Gets additional fields to render in the form to add/edit a task
+ *
+ * @param array $taskInfo Values of the fields from the add/edit task form
+ * @param BaseTask $task The task object being edited. Null when adding a task!
+ * @param \TYPO3\CMS\Scheduler\Controller\SchedulerModuleController $schedulerModule Reference to the scheduler backend module
+ * @return array A two dimensional array, array('Identifier' => array('fieldId' => array('code' => '', 'label' => '', 'cshKey' => '', 'cshLabel' => ''))
+ */
+ public function getAdditionalFields(array &$taskInfo, $task, SchedulerModuleController $schedulerModule)
+ {
+ $currentSchedulerModuleAction = $schedulerModule->getCurrentAction();
+
+ /** @var BaseTask $task */
+ if ($currentSchedulerModuleAction->equals(Action::EDIT)) {
+ $taskInfo['dryRun'] = $task->isDryRun();
+ $taskInfo['doc'] = $task->getDoc();
+ $taskInfo['pid'] = $task->getPid();
+ $taskInfo['solr'] = $task->getSolr();
+ $taskInfo['owner'] = $task->getOwner();
+ } else {
+ $taskInfo['dryRun'] = false;
+ $taskInfo['doc'] = 'https://';
+ $taskInfo['pid'] = - 1;
+ $taskInfo['solr'] = - 1;
+ $taskInfo['owner'] = '';
+ }
+
+ $additionalFields = [];
+
+ // Checkbox for dry-run
+ $additionalFields['dryRun'] = $this->getDryRunField($taskInfo['dryRun']);
+
+ // Text field for document URL
+ $fieldName = 'doc';
+ $fieldId = 'task_' . $fieldName;
+ $fieldHtml = '';
+ $additionalFields[$fieldId] = [
+ 'code' => $fieldHtml,
+ 'label' => 'LLL:EXT:dlf/Resources/Private/Language/locallang_tasks.xlf:additionalFields.doc',
+ 'cshKey' => '_MOD_system_txschedulerM1',
+ 'cshLabel' => $fieldId
+ ];
+
+ // DropDown for storage page
+ $additionalFields['pid'] = $this->getPidField($taskInfo['pid']);
+
+ // DropDown for Solr core
+ $additionalFields['solr'] = $this->getSolrField($taskInfo['solr'], $taskInfo['pid']);
+
+ // Text field for owner
+ $additionalFields['owner'] = $this->getOwnerField($taskInfo['owner']);
+
+ return $additionalFields;
+ }
+}
diff --git a/Classes/Task/IndexTask.php b/Classes/Task/IndexTask.php
new file mode 100644
index 0000000000..7c7436e281
--- /dev/null
+++ b/Classes/Task/IndexTask.php
@@ -0,0 +1,56 @@
+
+ *
+ * This file is part of the Kitodo and TYPO3 projects.
+ *
+ * @license GNU General Public License version 3 or later.
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ */
+namespace Kitodo\Dlf\Task;
+
+use TYPO3\CMS\Core\Core\Environment;
+use TYPO3\CMS\Core\Messaging\FlashMessage;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Task for index a document.
+ *
+ * @package TYPO3
+ * @subpackage dlf
+ *
+ * @access public
+ */
+class IndexTask extends BaseTask
+{
+ public function execute()
+ {
+ $inputArray = [];
+ if ($this->dryRun) {
+ $inputArray['--dry-run'] = true;
+ }
+ $inputArray['-d'] = $this->doc;
+ $inputArray['-p'] = $this->pid;
+ $inputArray['-s'] = $this->solr;
+ if (!empty($this->owner)) {
+ $inputArray['-o'] = $this->owner;
+ }
+
+ $indexCommand = GeneralUtility::makeInstance(\Kitodo\Dlf\Command\IndexCommand::class);
+ $inputInterface = GeneralUtility::makeInstance(\Symfony\Component\Console\Input\ArrayInput::class, $inputArray);
+ if (Environment::isCli()) {
+ $outputInterface = GeneralUtility::makeInstance(\Symfony\Component\Console\Output\ConsoleOutput::class);
+ } else {
+ $outputInterface = GeneralUtility::makeInstance(\Symfony\Component\Console\Output\BufferedOutput::class);
+ }
+
+ $return = $indexCommand->run($inputInterface, $outputInterface);
+
+ if (!Environment::isCli()) {
+ $this->outputFlashMessages($outputInterface->fetch(), $return ? FlashMessage::ERROR : FlashMessage::OK);
+ }
+ return !$return;
+ }
+}
diff --git a/Classes/Task/ReindexAdditionalFieldProvider.php b/Classes/Task/ReindexAdditionalFieldProvider.php
new file mode 100644
index 0000000000..6b7568c891
--- /dev/null
+++ b/Classes/Task/ReindexAdditionalFieldProvider.php
@@ -0,0 +1,147 @@
+
+ *
+ * This file is part of the Kitodo and TYPO3 projects.
+ *
+ * @license GNU General Public License version 3 or later.
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ */
+namespace Kitodo\Dlf\Task;
+
+use TYPO3\CMS\Core\Database\Connection;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Scheduler\Controller\SchedulerModuleController;
+use TYPO3\CMS\Scheduler\Task\Enumeration\Action;
+
+/**
+ * Additional fields for reindex documents task.
+ *
+ * @package TYPO3
+ * @subpackage dlf
+ *
+ * @access public
+ */
+class ReindexAdditionalFieldProvider extends BaseAdditionalFieldProvider
+{
+ /**
+ * Gets additional fields to render in the form to add/edit a task
+ *
+ * @param array $taskInfo Values of the fields from the add/edit task form
+ * @param BaseTask $task The task object being edited. Null when adding a task!
+ * @param \TYPO3\CMS\Scheduler\Controller\SchedulerModuleController $schedulerModule Reference to the scheduler backend module
+ * @return array A two dimensional array, array('Identifier' => array('fieldId' => array('code' => '', 'label' => '', 'cshKey' => '', 'cshLabel' => ''))
+ */
+ public function getAdditionalFields(array &$taskInfo, $task, SchedulerModuleController $schedulerModule)
+ {
+ $currentSchedulerModuleAction = $schedulerModule->getCurrentAction();
+
+ /** @var BaseTask $task */
+ if ($currentSchedulerModuleAction->equals(Action::EDIT)) {
+ $taskInfo['dryRun'] = $task->isDryRun();
+ $taskInfo['coll'] = $task->getColl();
+ $taskInfo['pid'] = $task->getPid();
+ $taskInfo['solr'] = $task->getSolr();
+ $taskInfo['owner'] = $task->getOwner();
+ $taskInfo['all'] = $task->isAll();
+ } else {
+ $taskInfo['dryRun'] = false;
+ $taskInfo['coll'] = [];
+ $taskInfo['pid'] = - 1;
+ $taskInfo['solr'] = - 1;
+ $taskInfo['owner'] = '';
+ $taskInfo['all'] = false;
+ }
+
+ $additionalFields = [];
+
+ // Checkbox for dry-run
+ $additionalFields['dryRun'] = $this->getDryRunField($taskInfo['dryRun']);
+
+ // Select for collection(s)
+ $fieldName = 'coll';
+ $fieldId = 'task_' . $fieldName;
+ $options = $this->getCollOptions($taskInfo['coll'], $taskInfo['pid']);
+ ;
+ $fieldHtml = '';
+ $additionalFields[$fieldId] = [
+ 'code' => $fieldHtml,
+ 'label' => 'LLL:EXT:dlf/Resources/Private/Language/locallang_tasks.xlf:additionalFields.coll',
+ 'cshKey' => '_MOD_system_txschedulerM1',
+ 'cshLabel' => $fieldId
+ ];
+
+ // DropDown for storage page
+ $additionalFields['pid'] = $this->getPidField($taskInfo['pid']);
+
+ // DropDown for Solr core
+ $additionalFields['solr'] = $this->getSolrField($taskInfo['solr'], $taskInfo['pid']);
+
+ // Text field for owner
+ $additionalFields['owner'] = $this->getOwnerField($taskInfo['owner']);
+
+ // Checkbox for all
+ $fieldName = 'all';
+ $fieldId = 'task_' . $fieldName;
+ $fieldHtml = '';
+ $additionalFields[$fieldId] = [
+ 'code' => $fieldHtml,
+ 'label' => 'LLL:EXT:dlf/Resources/Private/Language/locallang_tasks.xlf:additionalFields.all',
+ 'cshKey' => '_MOD_system_txschedulerM1',
+ 'cshLabel' => $fieldId
+ ];
+
+ return $additionalFields;
+ }
+
+ /**
+ * Generates HTML options for collections
+ *
+ * @param array $coll Selected collections
+ * @param int $pid UID of storage page
+ *
+ * @return array HTML of selectbox options
+ */
+ private function getCollOptions(array $coll, int $pid): array
+ {
+ $options = [];
+ $collections = $this->getCollections($pid);
+ foreach ($collections as $label => $uid) {
+ $options[] = '';
+ }
+ return $options;
+ }
+
+ /**
+ * Fetches all collections on given storage page.
+ *
+ * @access protected
+ *
+ * @param int $pid The UID of the storage page
+ *
+ * @return array Array of collections
+ */
+ private function getCollections(int $pid): array
+ {
+ $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tx_dlf_collections');
+
+ $collections = [];
+ $result = $queryBuilder->select('uid', 'label')
+ ->from('tx_dlf_collections')
+ ->where(
+ $queryBuilder->expr()
+ ->eq('pid', $queryBuilder->createNamedParameter((int) $pid, Connection::PARAM_INT))
+ )
+ ->execute();
+
+ while ($record = $result->fetchAssociative()) {
+ $collections[$record['label']] = $record['uid'];
+ }
+
+ return $collections;
+ }
+}
diff --git a/Classes/Task/ReindexTask.php b/Classes/Task/ReindexTask.php
new file mode 100644
index 0000000000..e488cd7ac9
--- /dev/null
+++ b/Classes/Task/ReindexTask.php
@@ -0,0 +1,61 @@
+
+ *
+ * This file is part of the Kitodo and TYPO3 projects.
+ *
+ * @license GNU General Public License version 3 or later.
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ */
+namespace Kitodo\Dlf\Task;
+
+use TYPO3\CMS\Core\Core\Environment;
+use TYPO3\CMS\Core\Messaging\FlashMessage;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Task for reindexing documents.
+ *
+ * @package TYPO3
+ * @subpackage dlf
+ *
+ * @access public
+ */
+class ReindexTask extends BaseTask
+{
+ public function execute()
+ {
+ $inputArray = [];
+ if ($this->dryRun) {
+ $inputArray['--dry-run'] = true;
+ }
+ if (count($this->coll)) {
+ $inputArray['-c'] = implode(',', $this->coll);
+ }
+ $inputArray['-p'] = $this->pid;
+ $inputArray['-s'] = $this->solr;
+ if (!empty($this->owner)) {
+ $inputArray['-o'] = $this->owner;
+ }
+ if ($this->all) {
+ $inputArray['--all'] = true;
+ }
+
+ $reindexCommand = GeneralUtility::makeInstance(\Kitodo\Dlf\Command\ReindexCommand::class);
+ $inputInterface = GeneralUtility::makeInstance(\Symfony\Component\Console\Input\ArrayInput::class, $inputArray);
+ if (Environment::isCli()) {
+ $outputInterface = GeneralUtility::makeInstance(\Symfony\Component\Console\Output\ConsoleOutput::class);
+ } else {
+ $outputInterface = GeneralUtility::makeInstance(\Symfony\Component\Console\Output\BufferedOutput::class);
+ }
+
+ $return = $reindexCommand->run($inputInterface, $outputInterface);
+
+ if (!Environment::isCli()) {
+ $this->outputFlashMessages($outputInterface->fetch(), $return ? FlashMessage::ERROR : FlashMessage::OK);
+ }
+ return !$return;
+ }
+}
diff --git a/Classes/ViewHelpers/ImplodeViewHelper.php b/Classes/ViewHelpers/ImplodeViewHelper.php
new file mode 100644
index 0000000000..589ef77402
--- /dev/null
+++ b/Classes/ViewHelpers/ImplodeViewHelper.php
@@ -0,0 +1,37 @@
+
+ *
+ * This file is part of the Kitodo and TYPO3 projects.
+ *
+ * @license GNU General Public License version 3 or later.
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ */
+
+namespace Kitodo\Dlf\ViewHelpers;
+
+use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
+
+class ImplodeViewHelper extends AbstractViewHelper
+{
+ public function initializeArguments()
+ {
+ parent::initializeArguments();
+ $this->registerArgument('value', 'array', 'The array to be imploded', true);
+ $this->registerArgument('delimiter', 'string', 'The delimiter ', true);
+ }
+
+ /**
+ * Checks if a value exists in an array.
+ *
+ * @return string
+ */
+ public function render()
+ {
+ $value = $this->arguments['value'];
+ $delimiter = $this->arguments['delimiter'];
+
+ return implode($delimiter, $value);
+ }
+}
diff --git a/Classes/ViewHelpers/IsArrayViewHelper.php b/Classes/ViewHelpers/IsArrayViewHelper.php
new file mode 100644
index 0000000000..eda640c572
--- /dev/null
+++ b/Classes/ViewHelpers/IsArrayViewHelper.php
@@ -0,0 +1,41 @@
+
+ *
+ * This file is part of the Kitodo and TYPO3 projects.
+ *
+ * @license GNU General Public License version 3 or later.
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ */
+
+namespace Kitodo\Dlf\ViewHelpers;
+
+use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
+use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
+
+/**
+ * Checks if the given subject is an array.
+ */
+class IsArrayViewHelper extends AbstractViewHelper
+{
+ public function initializeArguments()
+ {
+ parent::initializeArguments();
+ $this->registerArgument('subject', 'string', 'The subject');
+ }
+
+ /**
+ * @return bool
+ */
+ public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext)
+ {
+ $subject = $arguments['subject'];
+ if ($subject === null) {
+ $subject = $renderChildrenClosure();
+ }
+
+ return \is_array($subject);
+ }
+}
diff --git a/Classes/ViewHelpers/IsInArrayViewHelper.php b/Classes/ViewHelpers/IsInArrayViewHelper.php
new file mode 100644
index 0000000000..c72b13fac9
--- /dev/null
+++ b/Classes/ViewHelpers/IsInArrayViewHelper.php
@@ -0,0 +1,37 @@
+
+ *
+ * This file is part of the Kitodo and TYPO3 projects.
+ *
+ * @license GNU General Public License version 3 or later.
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ */
+
+namespace Kitodo\Dlf\ViewHelpers;
+
+use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
+
+class IsInArrayViewHelper extends AbstractViewHelper
+{
+ public function initializeArguments()
+ {
+ parent::initializeArguments();
+ $this->registerArgument('needle', 'mixed', 'The searched value', true);
+ $this->registerArgument('haystack', 'array', 'The array', true);
+ }
+
+ /**
+ * Checks if a value exists in an array.
+ *
+ * @return bool
+ */
+ public function render()
+ {
+ $needle = $this->arguments['needle'];
+ $haystack = $this->arguments['haystack'];
+
+ return in_array($needle, $haystack);
+ }
+}
diff --git a/Configuration/Extbase/Persistence/Classes.php b/Configuration/Extbase/Persistence/Classes.php
index 454c039a19..cb15374ff6 100644
--- a/Configuration/Extbase/Persistence/Classes.php
+++ b/Configuration/Extbase/Persistence/Classes.php
@@ -29,6 +29,9 @@
\Kitodo\Dlf\Domain\Model\MetadataFormat::class => [
'tableName' => 'tx_dlf_metadataformat',
],
+ \Kitodo\Dlf\Domain\Model\MetadataSubentry::class => [
+ 'tableName' => 'tx_dlf_metadatasubentries',
+ ],
\Kitodo\Dlf\Domain\Model\Printer::class => [
'tableName' => 'tx_dlf_printer',
],
diff --git a/Configuration/FlexForms/Navigation.xml b/Configuration/FlexForms/Navigation.xml
index e1c551be23..5901229431 100644
--- a/Configuration/FlexForms/Navigation.xml
+++ b/Configuration/FlexForms/Navigation.xml
@@ -72,8 +72,16 @@