* * 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\DocumentList; use Kitodo\Dlf\Common\Helper; use Kitodo\Dlf\Common\Solr; /** * Plugin 'OAI-PMH Interface' for the 'dlf' extension * * @author Sebastian Meyer * @package TYPO3 * @subpackage dlf * @access public */ class OaiPmh extends \Kitodo\Dlf\Common\AbstractPlugin { public $scriptRelPath = 'Classes/Plugins/OaiPmh.php'; /** * Did an error occur? * * @var boolean * @access protected */ protected $error = FALSE; /** * This holds the OAI DOM object * * @var \DOMDocument * @access protected */ protected $oai; /** * This holds the configuration for all supported metadata prefixes * * @var array * @access protected */ protected $formats = [ 'oai_dc' => [ 'schema' => 'http://www.openarchives.org/OAI/2.0/oai_dc.xsd', 'namespace' => 'http://www.openarchives.org/OAI/2.0/oai_dc/', 'requiredFields' => ['record_id'], ], 'epicur' => [ 'schema' => 'http://www.persistent-identifier.de/xepicur/version1.0/xepicur.xsd', 'namespace' => 'urn:nbn:de:1111-2004033116', 'requiredFields' => ['purl', 'urn'], ], 'mets' => [ 'schema' => 'http://www.loc.gov/standards/mets/version17/mets.v1-7.xsd', 'namespace' => 'http://www.loc.gov/METS/', 'requiredFields' => ['location'], ] ]; /** * Delete expired resumption tokens * * @access protected * * @return void */ protected function deleteExpiredTokens() { // Delete expired resumption tokens. $result = $GLOBALS['TYPO3_DB']->exec_DELETEquery( 'tx_dlf_tokens', 'tx_dlf_tokens.ident="oai" AND tx_dlf_tokens.tstamp<'.intval($GLOBALS['EXEC_TIME'] - $this->conf['expired']) ); if ($GLOBALS['TYPO3_DB']->sql_affected_rows() === -1) { // Deletion failed. Helper::devLog('Could not delete expired resumption tokens', DEVLOG_SEVERITY_WARNING); } } /** * Process error * * @access protected * * @param string $type: Error type * * @return \DOMElement XML node to add to the OAI response */ protected function error($type) { $this->error = TRUE; $error = $this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'error', htmlspecialchars($this->pi_getLL($type, $type, FALSE), ENT_NOQUOTES, 'UTF-8')); $error->setAttribute('code', $type); return $error; } /** * Load URL parameters * * @access protected * * @return void */ protected function getUrlParams() { $allowedParams = [ 'verb', 'identifier', 'metadataPrefix', 'from', 'until', 'set', 'resumptionToken' ]; // Clear plugin variables. $this->piVars = []; // Set only allowed parameters. foreach ($allowedParams as $param) { if (\TYPO3\CMS\Core\Utility\GeneralUtility::_GP($param)) { $this->piVars[$param] = \TYPO3\CMS\Core\Utility\GeneralUtility::_GP($param); } } } /** * Get unqualified Dublin Core data. * @see http://www.openarchives.org/OAI/openarchivesprotocol.html#dublincore * * @access protected * * @param array $metadata: The metadata array * * @return \DOMElement XML node to add to the OAI response */ protected function getDcData(array $metadata) { $oai_dc = $this->oai->createElementNS($this->formats['oai_dc']['namespace'], 'oai_dc:dc'); $oai_dc->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:dc', 'http://purl.org/dc/elements/1.1/'); $oai_dc->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'); $oai_dc->setAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'xsi:schemaLocation', $this->formats['oai_dc']['namespace'].' '.$this->formats['oai_dc']['schema']); $oai_dc->appendChild($this->oai->createElementNS('http://purl.org/dc/elements/1.1/', 'dc:identifier', htmlspecialchars($metadata['record_id'], ENT_NOQUOTES, 'UTF-8'))); if (!empty($metadata['purl'])) { $oai_dc->appendChild($this->oai->createElementNS('http://purl.org/dc/elements/1.1/', 'dc:identifier', htmlspecialchars($metadata['purl'], ENT_NOQUOTES, 'UTF-8'))); } if (!empty($metadata['urn'])) { $oai_dc->appendChild($this->oai->createElementNS('http://purl.org/dc/elements/1.1/', 'dc:identifier', htmlspecialchars($metadata['urn'], ENT_NOQUOTES, 'UTF-8'))); } if (!empty($metadata['title'])) { $oai_dc->appendChild($this->oai->createElementNS('http://purl.org/dc/elements/1.1/', 'dc:title', htmlspecialchars($metadata['title'], ENT_NOQUOTES, 'UTF-8'))); } if (!empty($metadata['author'])) { $oai_dc->appendChild($this->oai->createElementNS('http://purl.org/dc/elements/1.1/', 'dc:creator', htmlspecialchars($metadata['author'], ENT_NOQUOTES, 'UTF-8'))); } if (!empty($metadata['year'])) { $oai_dc->appendChild($this->oai->createElementNS('http://purl.org/dc/elements/1.1/', 'dc:date', htmlspecialchars($metadata['year'], ENT_NOQUOTES, 'UTF-8'))); } if (!empty($metadata['place'])) { $oai_dc->appendChild($this->oai->createElementNS('http://purl.org/dc/elements/1.1/', 'dc:coverage', htmlspecialchars($metadata['place'], ENT_NOQUOTES, 'UTF-8'))); } $oai_dc->appendChild($this->oai->createElementNS('http://purl.org/dc/elements/1.1/', 'dc:format', 'application/mets+xml')); $oai_dc->appendChild($this->oai->createElementNS('http://purl.org/dc/elements/1.1/', 'dc:type', 'Text')); if (!empty($metadata['partof'])) { $result = $GLOBALS['TYPO3_DB']->exec_SELECTquery( 'tx_dlf_documents.record_id', 'tx_dlf_documents', 'tx_dlf_documents.uid='.intval($metadata['partof']) .Helper::whereClause('tx_dlf_documents'), '', '', '1' ); if ($GLOBALS['TYPO3_DB']->sql_num_rows($result)) { $partof = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($result); $oai_dc->appendChild($this->oai->createElementNS('http://purl.org/dc/elements/1.1/', 'dc:relation', htmlspecialchars($partof['record_id'], ENT_NOQUOTES, 'UTF-8'))); } } return $oai_dc; } /** * Get epicur data. * @see http://www.persistent-identifier.de/?link=210 * * @access protected * * @param array $metadata: The metadata array * * @return \DOMElement XML node to add to the OAI response */ protected function getEpicurData(array $metadata) { // Define all XML elements with or without qualified namespace. if (empty($this->conf['unqualified_epicur'])) { $epicur = $this->oai->createElementNS($this->formats['epicur']['namespace'], 'epicur:epicur'); $admin = $this->oai->createElementNS($this->formats['epicur']['namespace'], 'epicur:administrative_data'); $delivery = $this->oai->createElementNS($this->formats['epicur']['namespace'], 'epicur:delivery'); $update = $this->oai->createElementNS($this->formats['epicur']['namespace'], 'epicur:update_status'); $transfer = $this->oai->createElementNS($this->formats['epicur']['namespace'], 'epicur:transfer'); $format = $this->oai->createElementNS($this->formats['epicur']['namespace'], 'epicur:format', 'text/html'); $record = $this->oai->createElementNS($this->formats['epicur']['namespace'], 'epicur:record'); $identifier = $this->oai->createElementNS($this->formats['epicur']['namespace'], 'epicur:identifier', htmlspecialchars($metadata['urn'], ENT_NOQUOTES, 'UTF-8')); $resource = $this->oai->createElementNS($this->formats['epicur']['namespace'], 'epicur:resource'); $ident = $this->oai->createElementNS($this->formats['epicur']['namespace'], 'epicur:identifier', htmlspecialchars($metadata['purl'], ENT_NOQUOTES, 'UTF-8')); } else { $epicur = $this->oai->createElement('epicur'); $epicur->setAttribute('xmlns', $this->formats['epicur']['namespace']); $admin = $this->oai->createElement('administrative_data'); $delivery = $this->oai->createElement('delivery'); $update = $this->oai->createElement('update_status'); $transfer = $this->oai->createElement('transfer'); $format = $this->oai->createElement('format', 'text/html'); $record = $this->oai->createElement('record'); $identifier = $this->oai->createElement('identifier', htmlspecialchars($metadata['urn'], ENT_NOQUOTES, 'UTF-8')); $resource = $this->oai->createElement('resource'); $ident = $this->oai->createElement('identifier', htmlspecialchars($metadata['purl'], ENT_NOQUOTES, 'UTF-8')); } // Add attributes and build XML tree. $epicur->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'); $epicur->setAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'xsi:schemaLocation', $this->formats['epicur']['namespace'].' '.$this->formats['epicur']['schema']); // Do we update an URN or register a new one? if ($metadata['tstamp'] == $metadata['crdate']) { $update->setAttribute('type', 'urn_new'); } else { $update->setAttribute('type', 'url_update_general'); } $delivery->appendChild($update); $transfer->setAttribute('type', 'http'); $delivery->appendChild($transfer); $admin->appendChild($delivery); $epicur->appendChild($admin); $identifier->setAttribute('scheme', 'urn:nbn:de'); $record->appendChild($identifier); $ident->setAttribute('scheme', 'url'); $ident->setAttribute('type', 'frontpage'); $ident->setAttribute('role', 'primary'); $resource->appendChild($ident); $format->setAttribute('scheme', 'imt'); $resource->appendChild($format); $record->appendChild($resource); $epicur->appendChild($record); return $epicur; } /** * Get METS data. * @see http://www.loc.gov/standards/mets/docs/mets.v1-7.html * * @access protected * * @param array $metadata: The metadata array * * @return \DOMElement XML node to add to the OAI response */ protected function getMetsData(array $metadata) { $mets = NULL; // Load METS file. $xml = new \DOMDocument(); if ($xml->load($metadata['location'])) { // Get root element. $root = $xml->getElementsByTagNameNS($this->formats['mets']['namespace'], 'mets'); if ($root->item(0) instanceof \DOMNode) { // Import node into \DOMDocument. $mets = $this->oai->importNode($root->item(0), TRUE); } else { Helper::devLog('No METS part found in document with location "'.$metadata['location'].'"', DEVLOG_SEVERITY_ERROR); } } else { Helper::devLog('Could not load XML file from "'.$metadata['location'].'"', DEVLOG_SEVERITY_ERROR); } if ($mets === NULL) { $mets = $this->oai->createElementNS('http://kitodo.org/', 'kitodo:error', htmlspecialchars($this->pi_getLL('error', 'Error!', FALSE), ENT_NOQUOTES, 'UTF-8')); } return $mets; } /** * The main method of the PlugIn * * @access public * * @param string $content: The PlugIn content * @param array $conf: The PlugIn configuration * * @return void */ public function main($content, $conf) { // Initialize plugin. $this->init($conf); // Turn cache off. $this->setCache(FALSE); // Get GET and POST variables. $this->getUrlParams(); // Delete expired resumption tokens. $this->deleteExpiredTokens(); // Create XML document. $this->oai = new \DOMDocument('1.0', 'UTF-8'); // Add processing instruction (aka XSL stylesheet). if (!empty($this->conf['stylesheet'])) { // Resolve "EXT:" prefix in file path. if (substr($this->conf['stylesheet'], 0, 4) == 'EXT:') { list ($extKey, $filePath) = explode('/', substr($this->conf['stylesheet'], 4), 2); if (\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded($extKey)) { $this->conf['stylesheet'] = \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::siteRelPath($extKey).$filePath; } } $stylesheet = \TYPO3\CMS\Core\Utility\GeneralUtility::locationHeaderUrl($this->conf['stylesheet']); } else { // Use default stylesheet if no custom stylesheet is given. $stylesheet = \TYPO3\CMS\Core\Utility\GeneralUtility::locationHeaderUrl(\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::siteRelPath($this->extKey).'Resources/Public/Stylesheets/OaiPmh.xsl'); } $this->oai->appendChild($this->oai->createProcessingInstruction('xml-stylesheet', 'type="text/xsl" href="'.htmlspecialchars($stylesheet, ENT_NOQUOTES, 'UTF-8').'"')); // Create root element. $root = $this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'OAI-PMH'); $root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'); $root->setAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'xsi:schemaLocation', 'http://www.openarchives.org/OAI/2.0/ http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd'); // Add response date. $root->appendChild($this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'responseDate', gmdate('Y-m-d\TH:i:s\Z', $GLOBALS['EXEC_TIME']))); // Get response data. switch ($this->piVars['verb']) { case 'GetRecord': $response = $this->verbGetRecord(); break; case 'Identify': $response = $this->verbIdentify(); break; case 'ListIdentifiers': $response = $this->verbListIdentifiers(); break; case 'ListMetadataFormats': $response = $this->verbListMetadataFormats(); break; case 'ListRecords': $response = $this->verbListRecords(); break; case 'ListSets': $response = $this->verbListSets(); break; default: $response = $this->error('badVerb'); } // Add request. $linkConf = [ 'parameter' => $GLOBALS['TSFE']->id, 'forceAbsoluteUrl' => 1 ]; $request = $this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'request', htmlspecialchars($this->cObj->typoLink_URL($linkConf), ENT_NOQUOTES, 'UTF-8')); if (!$this->error) { foreach ($this->piVars as $key => $value) { $request->setAttribute($key, htmlspecialchars($value, ENT_NOQUOTES, 'UTF-8')); } } $root->appendChild($request); $root->appendChild($response); $this->oai->appendChild($root); $content = $this->oai->saveXML(); // Clean output buffer. Helper::cleanOutputBuffers(); // Send headers. header('HTTP/1.1 200 OK'); header('Cache-Control: no-cache'); header('Content-Length: '.strlen($content)); header('Content-Type: text/xml; charset=utf-8'); header('Date: '.date('r', $GLOBALS['EXEC_TIME'])); header('Expires: '.date('r', $GLOBALS['EXEC_TIME'] + $this->conf['expired'])); echo $content; // Flush output buffer and end script processing. ob_end_flush(); exit; } /** * Continue with resumption token * * @access protected * * @return string Substitution for subpart "###RESPONSE###" */ protected function resume() { // Get resumption token. $result = $GLOBALS['TYPO3_DB']->exec_SELECTquery( 'tx_dlf_tokens.options AS options', 'tx_dlf_tokens', 'tx_dlf_tokens.ident="oai"' .' AND tx_dlf_tokens.token='.$GLOBALS['TYPO3_DB']->fullQuoteStr($this->piVars['resumptionToken'], 'tx_dlf_tokens'), '', '', '1' ); if (!$GLOBALS['TYPO3_DB']->sql_num_rows($result)) { // No resumption token found or resumption token expired. return $this->error('badResumptionToken'); } $resArray = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($result); $resultSet = unserialize($resArray['options']); return $this->generateOutputForDocumentList($resultSet); } /** * Process verb "GetRecord" * * @access protected * * @return string Substitution for subpart "###RESPONSE###" */ protected function verbGetRecord() { if (count($this->piVars) != 3 || empty($this->piVars['metadataPrefix']) || empty($this->piVars['identifier'])) { return $this->error('badArgument'); } if (!in_array($this->piVars['metadataPrefix'], array_keys($this->formats))) { return $this->error('cannotDisseminateFormat'); } $where = ''; if (!$this->conf['show_userdefined']) { $where .= ' AND tx_dlf_collections.fe_cruser_id=0'; } $record = $GLOBALS['TYPO3_DB']->exec_SELECT_mm_query( 'tx_dlf_documents.*,GROUP_CONCAT(DISTINCT tx_dlf_collections.oai_name ORDER BY tx_dlf_collections.oai_name SEPARATOR " ") AS collections', 'tx_dlf_documents', 'tx_dlf_relations', 'tx_dlf_collections', 'AND tx_dlf_documents.record_id='.$GLOBALS['TYPO3_DB']->fullQuoteStr($this->piVars['identifier'], 'tx_dlf_documents') .' AND tx_dlf_documents.pid='.intval($this->conf['pages']) .' AND tx_dlf_collections.pid='.intval($this->conf['pages']) .' AND tx_dlf_relations.ident='.$GLOBALS['TYPO3_DB']->fullQuoteStr('docs_colls', 'tx_dlf_relations') .$where .Helper::whereClause('tx_dlf_collections'), 'tx_dlf_documents.uid', 'tx_dlf_documents.tstamp', '1' ); if (!$GLOBALS['TYPO3_DB']->sql_num_rows($record)) { return $this->error('idDoesNotExist'); } $resArray = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($record); // Check for required fields. foreach ($this->formats[$this->piVars['metadataPrefix']]['requiredFields'] as $required) { if (empty($resArray[$required])) { return $this->error('cannotDisseminateFormat'); } } $GetRecord = $this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'GetRecord'); $recordNode = $this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'record'); $headerNode = $this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'header'); $headerNode->appendChild($this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'identifier', htmlspecialchars($resArray['record_id'], ENT_NOQUOTES, 'UTF-8'))); $headerNode->appendChild($this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'datestamp', gmdate('Y-m-d\TH:i:s\Z', $resArray['tstamp']))); // Handle deleted documents. // TODO: Use TYPO3 API functions here! if ($resArray['deleted'] || $resArray['hidden']) { $headerNode->setAttribute('status', 'deleted'); $recordNode->appendChild($headerNode); } else { foreach (explode(' ', $resArray['collections']) as $spec) { $headerNode->appendChild($this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'setSpec', htmlspecialchars($spec, ENT_NOQUOTES, 'UTF-8'))); } $recordNode->appendChild($headerNode); $metadataNode = $this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'metadata'); switch ($this->piVars['metadataPrefix']) { case 'oai_dc': $metadataNode->appendChild($this->getDcData($resArray)); break; case 'epicur': $metadataNode->appendChild($this->getEpicurData($resArray)); break; case 'mets': $metadataNode->appendChild($this->getMetsData($resArray)); break; } $recordNode->appendChild($metadataNode); } $GetRecord->appendChild($recordNode); return $GetRecord; } /** * Process verb "Identify" * * @access protected * * @return \DOMElement XML node to add to the OAI response */ protected function verbIdentify() { // Check for invalid arguments. if (count($this->piVars) > 1) { return $this->error('badArgument'); } // Get repository name and administrative contact. // Use default values for an installation with incomplete plugin configuration. $adminEmail = 'unknown@example.org'; $repositoryName = 'Kitodo.Presentation OAI-PMH Interface (default configuration)'; $result = $GLOBALS['TYPO3_DB']->exec_SELECTquery( 'tx_dlf_libraries.oai_label AS oai_label,tx_dlf_libraries.contact AS contact', 'tx_dlf_libraries', 'tx_dlf_libraries.pid='.intval($this->conf['pages']) .' AND tx_dlf_libraries.uid='.intval($this->conf['library']) .Helper::whereClause('tx_dlf_libraries'), '', '', '' ); if ($GLOBALS['TYPO3_DB']->sql_num_rows($result)) { $resArray = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($result); $adminEmail = htmlspecialchars(trim(str_replace('mailto:', '', $resArray['contact'])), ENT_NOQUOTES); $repositoryName = htmlspecialchars($resArray['oai_label'], ENT_NOQUOTES); } else { Helper::devLog('Incomplete plugin configuration', DEVLOG_SEVERITY_NOTICE); } // Get earliest datestamp. Use a default value if that fails. $earliestDatestamp = '0000-00-00T00:00:00Z'; $result = $GLOBALS['TYPO3_DB']->exec_SELECTquery( 'tx_dlf_documents.tstamp AS tstamp', 'tx_dlf_documents', 'tx_dlf_documents.pid='.intval($this->conf['pages']), '', 'tx_dlf_documents.tstamp ASC', '1' ); if ($GLOBALS['TYPO3_DB']->sql_num_rows($result)) { list ($timestamp) = $GLOBALS['TYPO3_DB']->sql_fetch_row($result); $earliestDatestamp = gmdate('Y-m-d\TH:i:s\Z', $timestamp); } else { Helper::devLog('No records found with PID '.$this->conf['pages'], DEVLOG_SEVERITY_NOTICE); } $linkConf = [ 'parameter' => $GLOBALS['TSFE']->id, 'forceAbsoluteUrl' => 1 ]; $baseURL = htmlspecialchars($this->cObj->typoLink_URL($linkConf), ENT_NOQUOTES); // Add identification node. $Identify = $this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'Identify'); $Identify->appendChild($this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'repositoryName', $repositoryName)); $Identify->appendChild($this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'baseURL', $baseURL)); $Identify->appendChild($this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'protocolVersion', '2.0')); $Identify->appendChild($this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'adminEmail', $adminEmail)); $Identify->appendChild($this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'earliestDatestamp', $earliestDatestamp)); $Identify->appendChild($this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'deletedRecord', 'transient')); $Identify->appendChild($this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'granularity', 'YYYY-MM-DDThh:mm:ssZ')); return $Identify; } /** * Process verb "ListIdentifiers" * * @access protected * * @return string Substitution for subpart "###RESPONSE###" */ protected function verbListIdentifiers() { // If we have a resumption token we can continue our work if (!empty($this->piVars['resumptionToken'])) { // "resumptionToken" is an exclusive argument. if (count($this->piVars) > 2) { return $this->error('badArgument'); } else { return $this->resume(); } } // "metadataPrefix" is required and "identifier" is not allowed. if (empty($this->piVars['metadataPrefix']) || !empty($this->piVars['identifier'])) { return $this->error('badArgument'); } if (!in_array($this->piVars['metadataPrefix'], array_keys($this->formats))) { return $this->error('cannotDisseminateFormat'); } try { $documentSet = $this->fetchDocumentUIDs(); } catch (\Exception $exception) { return $this->error($exception->getMessage()); } $resultSet = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(DocumentList::class); $resultSet->reset(); $resultSet->add($documentSet); $resultSet->metadata = [ 'completeListSize' => count($documentSet), 'metadataPrefix' => $this->piVars['metadataPrefix'], ]; return $this->generateOutputForDocumentList($resultSet); } /** * Process verb "ListMetadataFormats" * * @access protected * * @return \DOMElement XML node to add to the OAI response */ protected function verbListMetadataFormats() { $resArray = []; // Check for invalid arguments. if (count($this->piVars) > 1) { if (empty($this->piVars['identifier']) || count($this->piVars) > 2) { return $this->error('badArgument'); } // Check given identifier. $result = $GLOBALS['TYPO3_DB']->exec_SELECTquery( 'tx_dlf_documents.*', 'tx_dlf_documents', 'tx_dlf_documents.pid='.intval($this->conf['pages']) .' AND tx_dlf_documents.record_id='.$GLOBALS['TYPO3_DB']->fullQuoteStr($this->piVars['identifier'], 'tx_dlf_documents'), '', '', '1' ); if (!$GLOBALS['TYPO3_DB']->sql_num_rows($result)) { return $this->error('idDoesNotExist'); } $resArray = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($result); } // Add metadata formats node. $ListMetadaFormats = $this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'ListMetadataFormats'); foreach ($this->formats as $prefix => $details) { if (!empty($resArray)) { foreach ($details['requiredFields'] as $required) { if (empty($resArray[$required])) { // Skip metadata formats whose requirements are not met. continue 2; } } } // Add format node. $format = $this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'metadataFormat'); $format->appendChild($this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'metadataPrefix', htmlspecialchars($prefix, ENT_NOQUOTES, 'UTF-8'))); $format->appendChild($this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'schema', htmlspecialchars($details['schema'], ENT_NOQUOTES, 'UTF-8'))); $format->appendChild($this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'metadataNamespace', htmlspecialchars($details['namespace'], ENT_NOQUOTES, 'UTF-8'))); $ListMetadaFormats->appendChild($format); } return $ListMetadaFormats; } /** * Process verb "ListRecords" * * @access protected * * @return string Substitution for subpart "###RESPONSE###" */ protected function verbListRecords() { // Check for invalid arguments. if (!empty($this->piVars['resumptionToken'])) { // "resumptionToken" is an exclusive argument. if (count($this->piVars) > 2) { return $this->error('badArgument'); } else { return $this->resume(); } } if (empty($this->piVars['metadataPrefix']) || !empty($this->piVars['identifier'])) { // "metadataPrefix" is required and "identifier" is not allowed. return $this->error('badArgument'); } // Check "metadataPrefix" for valid value. if (!in_array($this->piVars['metadataPrefix'], array_keys($this->formats))) { return $this->error('cannotDisseminateFormat'); } try { $documentSet = $this->fetchDocumentUIDs(); } catch (\Exception $exception) { return $this->error($exception->getMessage()); } $resultSet = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(DocumentList::class); $resultSet->reset(); $resultSet->add($documentSet); $resultSet->metadata = [ 'completeListSize' => count($documentSet), 'metadataPrefix' => $this->piVars['metadataPrefix'], ]; return $this->generateOutputForDocumentList($resultSet); } /** * Process verb "ListSets" * * @access protected * * @return string Substitution for subpart "###RESPONSE###" */ protected function verbListSets() { // Check for invalid arguments. if (count($this->piVars) > 1) { if (!empty($this->piVars['resumptionToken'])) { return $this->error('badResumptionToken'); } else { return $this->error('badArgument'); } } $where = ''; if (!$this->conf['show_userdefined']) { $where = ' AND tx_dlf_collections.fe_cruser_id=0'; } $result = $GLOBALS['TYPO3_DB']->exec_SELECTquery( 'tx_dlf_collections.oai_name AS oai_name,tx_dlf_collections.label AS label', 'tx_dlf_collections', 'tx_dlf_collections.sys_language_uid IN (-1,0)' .' AND NOT tx_dlf_collections.oai_name=""' .' AND tx_dlf_collections.pid='.intval($this->conf['pages']) .$where .Helper::whereClause('tx_dlf_collections'), 'tx_dlf_collections.oai_name', 'tx_dlf_collections.oai_name', '' ); if (!$GLOBALS['TYPO3_DB']->sql_num_rows($result)) { return $this->error('noSetHierarchy'); } $ListSets = $this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'ListSets'); while ($resArray = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($result)) { $set = $this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'set'); $set->appendChild($this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'setSpec', htmlspecialchars($resArray['oai_name'], ENT_NOQUOTES, 'UTF-8'))); $set->appendChild($this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'setName', htmlspecialchars($resArray['label'], ENT_NOQUOTES, 'UTF-8'))); $ListSets->appendChild($set); } return $ListSets; } /** * Fetch records * * @access protected * * @return array Array of matching records * @throws \Exception */ protected function fetchDocumentUIDs() { $solr_query = ''; if (!$this->conf['show_userdefined']) { $where = ' AND tx_dlf_collections.fe_cruser_id=0'; } // Check "set" for valid value. if (!empty($this->piVars['set'])) { // For SOLR we need the index_name of the collection, // For DB Query we need the UID of the collection $result = $GLOBALS['TYPO3_DB']->exec_SELECTquery( 'tx_dlf_collections.index_name AS index_name, tx_dlf_collections.uid AS uid, tx_dlf_collections.index_search as index_query ', 'tx_dlf_collections', 'tx_dlf_collections.pid='.intval($this->conf['pages']) .' AND tx_dlf_collections.oai_name='.$GLOBALS['TYPO3_DB']->fullQuoteStr($this->piVars['set'], 'tx_dlf_collections') .$where .Helper::whereClause('tx_dlf_collections'), '', '', '1' ); if (!$GLOBALS['TYPO3_DB']->sql_num_rows($result)) { throw new \Exception('noSetHierarchy'); } $resArray = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($result); if ($resArray['index_query'] != "") { $solr_query .= '('.$resArray['index_query'].')'; } else { $solr_query .= 'collection:'.'"'.$resArray['index_name'].'"'; } } else { // If no set is specified we have to query for all collections $solr_query .= 'collection:* NOT collection:""'; } // Check for required fields. foreach ($this->formats[$this->piVars['metadataPrefix']]['requiredFields'] as $required) { $solr_query .= ' NOT '.$required.':""'; } // toplevel="true" is always required $solr_query .= ' AND toplevel:"true"'; $from = "*"; // Check "from" for valid value. if (!empty($this->piVars['from'])) { // Is valid format? if (is_array($date_array = strptime($this->piVars['from'], '%Y-%m-%dT%H:%M:%SZ')) || is_array($date_array = strptime($this->piVars['from'], '%Y-%m-%d'))) { $timestamp = gmmktime($date_array['tm_hour'], $date_array['tm_min'], $date_array['tm_sec'], $date_array['tm_mon'] + 1, $date_array['tm_mday'], $date_array['tm_year'] + 1900); $from = date("Y-m-d", $timestamp).'T'.date("H:i:s", $timestamp).'.000Z'; } else { throw new \Exception('badArgument'); } } $until = "*"; // Check "until" for valid value. if (!empty($this->piVars['until'])) { // Is valid format? if (is_array($date_array = strptime($this->piVars['until'], '%Y-%m-%dT%H:%M:%SZ')) || is_array($date_array = strptime($this->piVars['until'], '%Y-%m-%d'))) { $timestamp = gmmktime($date_array['tm_hour'], $date_array['tm_min'], $date_array['tm_sec'], $date_array['tm_mon'] + 1, $date_array['tm_mday'], $date_array['tm_year'] + 1900); $until = date("Y-m-d", $timestamp).'T'.date("H:i:s", $timestamp).'.999Z'; if ($from != "*" && $from > $until) { throw new \Exception('badArgument'); } } else { throw new \Exception('badArgument'); } } // Check "from" and "until" for same granularity. if (!empty($this->piVars['from']) && !empty($this->piVars['until'])) { if (strlen($this->piVars['from']) != strlen($this->piVars['until'])) { throw new \Exception('badArgument'); } } $solr_query .= ' AND timestamp:['.$from.' TO '.$until.']'; $documentSet = []; $solr = Solr::getInstance($this->conf['solrcore']); if (intval($this->conf['solr_limit']) > 0) { $solr->limit = intval($this->conf['solr_limit']); } // We only care about the UID in the results and want them sorted $parameters = [ "fields" => "uid", "sort" => [ "uid" => "asc" ] ]; $result = $solr->search_raw($solr_query, $parameters); if (empty($result)) { throw new \Exception('noRecordsMatch'); } foreach ($result as $doc) { $documentSet[] = $doc->uid; } return $documentSet; } /** * Fetch more information for document list * @access protected * * @param \Kitodo\Dlf\Common\DocumentList $documentListSet * * @return \DOMElement XML of enriched records */ protected function generateOutputForDocumentList(DocumentList $documentListSet) { $documentsToProcess = $documentListSet->removeRange(0, intval($this->conf['limit'])); $verb = $this->piVars['verb']; $documents = $GLOBALS['TYPO3_DB']->exec_SELECT_mm_query( 'tx_dlf_documents.*,GROUP_CONCAT(DISTINCT tx_dlf_collections.oai_name ORDER BY tx_dlf_collections.oai_name SEPARATOR " ") AS collections', 'tx_dlf_documents', 'tx_dlf_relations', 'tx_dlf_collections', 'AND tx_dlf_documents.uid IN ('.implode(',', $GLOBALS['TYPO3_DB']->cleanIntArray($documentsToProcess)).')' .' AND tx_dlf_documents.pid='.intval($this->conf['pages']) .' AND tx_dlf_collections.pid='.intval($this->conf['pages']) .' AND tx_dlf_relations.ident='.$GLOBALS['TYPO3_DB']->fullQuoteStr('docs_colls', 'tx_dlf_relations') .Helper::whereClause('tx_dlf_collections'), 'tx_dlf_documents.uid', 'tx_dlf_documents.tstamp', $this->conf['limit'] ); $output = $this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', $verb); while ($resArray = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($documents)) { // Add header node. $header = $this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'header'); $header->appendChild($this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'identifier', htmlspecialchars($resArray['record_id'], ENT_NOQUOTES, 'UTF-8'))); $header->appendChild($this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'datestamp', gmdate('Y-m-d\TH:i:s\Z', $resArray['tstamp']))); // Check if document is deleted or hidden. // TODO: Use TYPO3 API functions here! if ($resArray['deleted'] || $resArray['hidden']) { // Add "deleted" status. $header->setAttribute('status', 'deleted'); if ($verb == 'ListRecords') { // Add record node. $record = $this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'record'); $record->appendChild($header); $output->appendChild($record); } elseif ($verb == 'ListIdentifiers') { $output->appendChild($header); } } else { // Add sets. foreach (explode(' ', $resArray['collections']) as $spec) { $header->appendChild($this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'setSpec', htmlspecialchars($spec, ENT_NOQUOTES, 'UTF-8'))); } if ($verb == 'ListRecords') { // Add record node. $record = $this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'record'); $record->appendChild($header); // Add metadata node. $metadata = $this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'metadata'); $metadataPrefix = $this->piVars['metadataPrefix']; if (!$metadataPrefix) { // If we resume an action the metadataPrefix is stored with the documentSet $metadataPrefix = $documentListSet->metadata['metadataPrefix']; } switch ($metadataPrefix) { case 'oai_dc': $metadata->appendChild($this->getDcData($resArray)); break; case 'epicur': $metadata->appendChild($this->getEpicurData($resArray)); break; case 'mets': $metadata->appendChild($this->getMetsData($resArray)); break; } $record->appendChild($metadata); $output->appendChild($record); } elseif ($verb == 'ListIdentifiers') { $output->appendChild($header); } } } $output->appendChild($this->generateResumptionTokenForDocumentListSet($documentListSet)); return $output; } /** * Generate resumption token * * @access protected * * @param \Kitodo\Dlf\Common\DocumentList $documentListSet * * @return \DOMElement XML for resumption token */ protected function generateResumptionTokenForDocumentListSet(DocumentList $documentListSet) { if ($documentListSet->count() != 0) { $token = uniqid(); $GLOBALS['TYPO3_DB']->exec_INSERTquery( 'tx_dlf_tokens', [ 'tstamp' => $GLOBALS['EXEC_TIME'], 'token' => $token, 'options' => serialize($documentListSet), 'ident' => 'oai', ] ); if ($GLOBALS['TYPO3_DB']->sql_affected_rows() == 1) { $resumptionToken = $this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'resumptionToken', htmlspecialchars($token, ENT_NOQUOTES, 'UTF-8')); } else { Helper::devLog('Could not create resumption token', DEVLOG_SEVERITY_ERROR); } } else { // Result set complete. We don't need a token. $resumptionToken = $this->oai->createElementNS('http://www.openarchives.org/OAI/2.0/', 'resumptionToken'); } $resumptionToken->setAttribute('cursor', intval($documentListSet->metadata['completeListSize']) - count($documentListSet)); $resumptionToken->setAttribute('completeListSize', $documentListSet->metadata['completeListSize']); $resumptionToken->setAttribute('expirationDate', gmdate('Y-m-d\TH:i:s\Z', $GLOBALS['EXEC_TIME'] + $this->conf['expired'])); return $resumptionToken; } }