config = $config;
// Initialize parameters from GET or POST variables
$this->params = [];
if (isset($GLOBALS['HTTP_RAW_POST_DATA']) && !empty($GLOBALS['HTTP_RAW_POST_DATA'])) {
OAIUtils::parseStr($GLOBALS['HTTP_RAW_POST_DATA'], $this->params);
} elseif (!empty($_SERVER['QUERY_STRING'])) {
OAIUtils::parseStr($_SERVER['QUERY_STRING'], $this->params);
} else {
$this->params = array_merge($_GET, $_POST);
}
// Clean input variables
OAIUtils::prepInput($this->params);
// Encode data with gzip, deflate, or none, depending on browser support
if (!ini_get('zlib.output_compression')) {
@ob_start('ob_gzhandler');
}
}
/**
* Execute the requested OAI protocol request
* and output the response.
*/
public function execute()
{
switch ($this->getParam('verb')) {
case 'GetRecord':
$this->GetRecord();
break;
case 'Identify':
$this->Identify();
break;
case 'ListIdentifiers':
$this->ListIdentifiers();
break;
case 'ListMetadataFormats':
$this->ListMetadataFormats();
break;
case 'ListRecords':
$this->ListRecords();
break;
case 'ListSets':
$this->ListSets();
break;
default:
$this->error('badVerb', 'Illegal OAI verb');
break;
}
}
//
// Abstract implementation-specific functions
// (to be overridden in subclass)
//
/**
* Return information about the repository.
*
* @return OAIRepository
*/
abstract public function repositoryInfo();
/**
* Check if identifier is in the valid format.
*
* @param string $identifier
*
* @return bool
*/
public function validIdentifier($identifier)
{
return false;
}
/**
* Check if identifier exists.
*
* @param string $identifier
*
* @return bool
*/
public function identifierExists($identifier)
{
return false;
}
/**
* Return OAI record for specified identifier.
*
* @param string $identifier
*
* @return OAIRecord (or false, if identifier is invalid)
*/
abstract public function record($identifier);
/**
* Return set of OAI records.
*
* @param string $metadataPrefix specified metadata prefix
* @param int $from minimum timestamp
* @param int $until maximum timestamp
* @param string $set specified set
* @param int $offset current record offset
* @param int $limit maximum number of records to return
* @param int $total output parameter, set to total number of records
*
* @return array OAIRecord
*/
public function records($metadataPrefix, $from, $until, $set, $offset, $limit, &$total)
{
return [];
}
/**
* Return set of OAI identifiers.
*
* @see getRecords
*
* @return array OAIIdentifier
*/
public function identifiers($metadataPrefix, $from, $until, $set, $offset, $limit, &$total)
{
return [];
}
/**
* Return set of OAI sets.
*
* @param int $offset current set offset
* @param int $limit Maximum number of sets to return
* @param int $total output parameter, set to total number of sets
*/
public function sets($offset, $limit, &$total)
{
return [];
}
/**
* Retrieve a resumption token.
*
* @param string $tokenId
*
* @return OAIResumptionToken|false
*/
abstract public function resumptionToken($tokenId);
/**
* Save a resumption token.
*
* @param int $offset current offset
* @param array $params request parameters
*
* @return OAIResumptionToken the saved token
*/
abstract public function saveResumptionToken($offset, $params);
/**
* Return array of supported metadata formats.
*
* @param bool $namesOnly return array of format prefix names only
* @param string $identifier return formats for specific identifier
*
* @return array
*/
public function metadataFormats($namesOnly = false, $identifier = null)
{
$formats = [];
Hook::call('OAI::metadataFormats', [$namesOnly, $identifier, &$formats]);
return $formats;
}
//
// Protocol request handlers
//
/**
* Handle OAI GetRecord request.
* Retrieves an individual record from the repository.
*/
public function GetRecord()
{
// Validate parameters
if (!$this->checkParams(['identifier', 'metadataPrefix'])) {
return;
}
$identifier = $this->getParam('identifier');
$metadataPrefix = $this->getParam('metadataPrefix');
// Check that identifier is in valid format
if ($this->validIdentifier($identifier) === false) {
$this->error('badArgument', 'Identifier is not in a valid format');
return;
}
// Get metadata for requested identifier
if (($record = $this->record($identifier)) === false) {
$this->error('idDoesNotExist', 'No matching identifier in this repository');
return;
}
// Check that the requested metadata format is supported for this identifier
if (!in_array($metadataPrefix, $this->metadataFormats(true, $identifier))) {
$this->error('cannotDisseminateFormat', 'The requested metadataPrefix is not supported by this repository');
return;
}
// Display response
$response = "\t\n" .
"\t\t\n" .
"\t\t\tstatus == self::OAIRECORD_STATUS_DELETED) ? " status=\"deleted\">\n" : ">\n") .
"\t\t\t\t" . $record->identifier . "\n" .
"\t\t\t\t" . $record->datestamp . "\n";
// Output set memberships
foreach ($record->sets as $setSpec) {
$response .= "\t\t\t\t{$setSpec}\n";
}
$response .= "\t\t\t\n";
if (!empty($record->data)) {
$response .= "\t\t\t\n";
// Output metadata
$response .= $this->formatMetadata($metadataPrefix, $record);
$response .= "\t\t\t\n";
}
$response .= "\t\t\n" .
"\t\n";
$this->response($response);
}
/**
* Handle OAI Identify request.
* Retrieves information about a repository.
*/
public function Identify()
{
// Validate parameters
if (!$this->checkParams()) {
return;
}
$info = $this->repositoryInfo();
// Format body of response
$response = "\t\n" .
"\t\t" . OAIUtils::prepOutput($info->repositoryName) . "\n" .
"\t\t" . $this->config->baseUrl . "\n" .
"\t\t" . $this->protocolVersion . "\n" .
"\t\t" . $info->adminEmail . "\n" .
"\t\t" . OAIUtils::UTCDate($info->earliestDatestamp) . "\n" .
"\t\tpersistent\n" .
"\t\t" . $this->config->granularity . "\n";
if (extension_loaded('zlib')) {
// Show compression options if server supports Zlib
$response .= "\t\tgzip\n" .
"\t\tdeflate\n";
}
$response .= "\t\t\n" .
"\t\t\t\n" .
"\t\t\t\toai\n" .
"\t\t\t\t" . $this->config->repositoryId . "\n" .
"\t\t\t\t" . $info->delimiter . "\n" .
"\t\t\t\t" . $info->sampleIdentifier . "\n" .
"\t\t\t\n" .
"\t\t\n";
$response .= "\t\t\n" .
"\t\t\t\n" .
"\t\t\t\t" . $info->toolkitTitle . "\n" .
"\t\t\t\t\n" .
"\t\t\t\t\tPublic Knowledge Project\n" .
"\t\t\t\t\tpkp.contact@gmail.com\n" .
"\t\t\t\t\n" .
"\t\t\t\t" . $info->toolkitVersion . "\n" .
"\t\t\t\t" . $info->toolkitURL . "\n" .
"\t\t\t\n" .
"\t\t\n";
$response .= "\t\n";
$this->response($response);
}
/**
* Handle OAI ListIdentifiers request.
* Retrieves headers of records from the repository.
*/
public function ListIdentifiers()
{
$offset = 0;
// Check for resumption token
if ($this->paramExists('resumptionToken')) {
// Validate parameters
if (!$this->checkParams(['resumptionToken'])) {
return;
}
// Get parameters from resumption token
if (($token = $this->resumptionToken($this->getParam('resumptionToken'))) === false) {
$this->error('badResumptionToken', 'The requested resumptionToken is invalid or has expired');
return;
}
$this->setParams($token->params);
$offset = $token->offset;
}
// Validate parameters
if (!$this->checkParams(['metadataPrefix'], ['from', 'until', 'set'])) {
return;
}
$metadataPrefix = $this->getParam('metadataPrefix');
$set = $this->getParam('set');
// Check that the requested metadata format is supported
if (!in_array($metadataPrefix, $this->metadataFormats(true))) {
$this->error('cannotDisseminateFormat', 'The requested metadataPrefix is not supported by this repository');
return;
}
// If a set was passed in check if repository supports sets
if (isset($set) && $this->config->maxSets == 0) {
$this->error('noSetHierarchy', 'This repository does not support sets');
return;
}
// Get UNIX timestamps for from and until dates, if applicable
if (!$this->extractDateParams($this->getParams(), $from, $until)) {
return;
}
// Store current offset and total records for resumption token, if needed
$cursor = $offset;
$total = 0;
// Get list of matching identifiers
$records = $this->identifiers($metadataPrefix, $from, $until, $set, $offset, $this->config->maxIdentifiers, $total);
if (empty($records)) {
$this->error('noRecordsMatch', 'No matching records in this repository');
return;
}
// Format body of response
$response = "\t\n";
// Output identifiers
for ($i = 0, $num = count($records); $i < $num; $i++) {
$record = $records[$i];
$response .= "\t\tstatus == self::OAIRECORD_STATUS_DELETED) ? " status=\"deleted\">\n" : ">\n") .
"\t\t\t" . $record->identifier . "\n" .
"\t\t\t" . $record->datestamp . "\n";
// Output set memberships
foreach ($record->sets as $setSpec) {
$response .= "\t\t\t" . OAIUtils::prepOutput($setSpec) . "\n";
}
$response .= "\t\t\n";
}
$offset += $num;
if ($offset != 0 && $offset < $total) {
// Partial result, save resumption token
$token = $this->saveResumptionToken($offset, $this->getParams());
$response .= "\t\texpire) . "\"\n" .
"\t\t\tcompleteListSize=\"{$total}\"\n" .
"\t\t\tcursor=\"{$cursor}\">" . $token->id . "\n";
} elseif (isset($token)) {
// Current request completes a previous incomplete list, add empty resumption token
$response .= "\t\t\n";
}
$response .= "\t\n";
$this->response($response);
}
/**
* Handle OAI ListMetadataFormats request.
* Retrieves metadata formats supported by the repository.
*/
public function ListMetadataFormats()
{
// Validate parameters
if (!$this->checkParams([], ['identifier'])) {
return;
}
// Get list of metadata formats for selected identifier, or all formats if no identifier was passed
if ($this->paramExists('identifier')) {
if (!$this->identifierExists($this->getParam('identifier'))) {
$this->error('idDoesNotExist', 'No matching identifier in this repository');
return;
} else {
$formats = $this->metadataFormats(false, $this->getParam('identifier'));
}
} else {
$formats = $this->metadataFormats();
}
if (empty($formats) || !is_array($formats)) {
$this->error('noMetadataFormats', 'No metadata formats are available');
return;
}
// Format body of response
$response = "\t\n";
// output metadata formats
foreach ($formats as $format) {
$response .= "\t\t\n" .
"\t\t\t" . $format->prefix . "\n" .
"\t\t\t" . $format->schema . "\n" .
"\t\t\t" . $format->namespace . "\n" .
"\t\t\n";
}
$response .= "\t\n";
$this->response($response);
}
/**
* Handle OAI ListRecords request.
* Retrieves records from the repository.
*/
public function ListRecords()
{
$offset = 0;
// Check for resumption token
if ($this->paramExists('resumptionToken')) {
// Validate parameters
if (!$this->checkParams(['resumptionToken'])) {
return;
}
// get parameters from resumption token
if (($token = $this->resumptionToken($this->getParam('resumptionToken'))) === false) {
$this->error('badResumptionToken', 'The requested resumptionToken is invalid or has expired');
return;
}
$this->setParams($token->params);
$offset = $token->offset;
}
// Validate parameters
if (!$this->checkParams(['metadataPrefix'], ['from', 'until', 'set'])) {
return;
}
$metadataPrefix = $this->getParam('metadataPrefix');
$set = $this->getParam('set');
// Check that the requested metadata format is supported
if (!in_array($metadataPrefix, $this->metadataFormats(true))) {
$this->error('cannotDisseminateFormat', 'The requested metadataPrefix is not supported by this repository');
return;
}
// If a set was passed check if repository supports sets
if (isset($set) && $this->config->maxSets == 0) {
$this->error('noSetHierarchy', 'This repository does not support sets');
return;
}
// Get UNIX timestamps for from and until dates, if applicable
if (!$this->extractDateParams($this->getParams(), $from, $until)) {
return;
}
// Store current offset and total records for resumption token, if needed
$cursor = $offset;
$total = 0;
// Get list of matching records
$records = $this->records($metadataPrefix, $from, $until, $set, $offset, $this->config->maxRecords, $total);
if (empty($records)) {
$this->error('noRecordsMatch', 'No matching records in this repository');
return;
}
// Format body of response
$response = "\t\n";
// Output records
for ($i = 0, $num = count($records); $i < $num; $i++) {
$record = $records[$i];
$response .= "\t\t\n" .
"\t\t\tstatus == self::OAIRECORD_STATUS_DELETED) ? " status=\"deleted\">\n" : ">\n") .
"\t\t\t\t" . $record->identifier . "\n" .
"\t\t\t\t" . $record->datestamp . "\n";
// Output set memberships
foreach ($record->sets as $setSpec) {
$response .= "\t\t\t\t" . OAIUtils::prepOutput($setSpec) . "\n";
}
$response .= "\t\t\t\n";
if (!empty($record->data)) {
$response .= "\t\t\t\n";
// Output metadata
$response .= $this->formatMetadata($this->getParam('metadataPrefix'), $record);
$response .= "\t\t\t\n";
}
$response .= "\t\t\n";
}
$offset += $num;
if ($offset != 0 && $offset < $total) {
// Partial result, save resumption token
$token = $this->saveResumptionToken($offset, $this->getParams());
$response .= "\t\texpire) . "\"\n" .
"\t\t\tcompleteListSize=\"{$total}\"\n" .
"\t\t\tcursor=\"{$cursor}\">" . $token->id . "\n";
} elseif (isset($token)) {
// Current request completes a previous incomplete list, add empty resumption token
$response .= "\t\t\n";
}
$response .= "\t\n";
$this->response($response);
}
/**
* Handle OAI ListSets request.
* Retrieves sets from a repository.
*/
public function ListSets()
{
$offset = 0;
// Check for resumption token
if ($this->paramExists('resumptionToken')) {
// Validate parameters
if (!$this->checkParams(['resumptionToken'])) {
return;
}
// Get parameters from resumption token
if (($token = $this->resumptionToken($this->getParam('resumptionToken'))) === false) {
$this->error('badResumptionToken', 'The requested resumptionToken is invalid or has expired');
return;
}
$this->setParams($token->params);
$offset = $token->offset;
}
// Validate parameters
if (!$this->checkParams()) {
return;
}
// Store current offset and total sets for resumption token, if needed
$cursor = $offset;
$total = 0;
// Get list of matching sets
$sets = $this->sets($offset, $this->config->maxRecords, $total);
if (empty($sets)) {
$this->error('noSetHierarchy', 'This repository does not support sets');
return;
}
// Format body of response
$response = "\t\n";
// Output sets
for ($i = 0, $num = count($sets); $i < $num; $i++) {
$set = $sets[$i];
$response .= "\t\t\n" .
"\t\t\t" . OAIUtils::prepOutput($set->spec) . "\n" .
"\t\t\t" . OAIUtils::prepOutput($set->name) . "\n";
// output set description, if applicable
if (isset($set->description)) {
$response .= "\t\t\t\n" .
"\t\t\t\t\n" .
"\t\t\t\t\t" . OAIUtils::prepOutput($set->description) . "\n" .
"\t\t\t\t\n" .
"\t\t\t\n";
}
$response .= "\t\t\n";
}
$offset += $num;
if ($offset != 0 && $offset < $total) {
// Partial result, set resumption token
$token = $this->saveResumptionToken($offset, $this->getParams());
$response .= "\t\texpire) . "\"\n" .
"\t\t\tcompleteListSize=\"{$total}\"\n" .
"\t\t\tcursor=\"{$cursor}\">" . $token->id . "\n";
} elseif (isset($token)) {
// current request completes a previous incomplete list, add empty resumption token
$response .= "\t\t\n";
}
$response .= "\t\n";
$this->response($response);
}
//
// Private helper functions
//
/**
* Display OAI error response.
*/
public function error($code, $message)
{
if (in_array($code, ['badVerb', 'badArgument'])) {
$printParams = false;
} else {
$printParams = true;
}
$this->response("\t{$message}", $printParams);
}
/**
* Output OAI response.
*
* @param string $response text of response message.
* @param bool $printParams display request parameters
*/
public function response($response, $printParams = true)
{
$request = Application::get()->getRequest();
header('Content-Type: text/xml');
echo "\n" .
'getBaseUrl() . "/lib/pkp/xml/oai2.xsl\" ?>\n" .
"\n" .
"\t" . OAIUtils::UTCDate() . "\n" .
"\tparams as $k => $v) {
echo " {$k}=\"" . OAIUtils::prepOutput($v) . '"';
}
}
echo '>' . OAIUtils::prepOutput($this->config->baseUrl) . "\n" .
$response .
"\n";
}
/**
* Returns the value of the specified parameter.
*
* @param string $name
*
* @return string
*/
public function getParam($name)
{
return $this->params[$name] ?? null;
}
/**
* Returns an associative array of all request parameters.
*
* @return array
*/
public function getParams()
{
return $this->params;
}
/**
* Set the request parameters.
*
* @param array $params
*/
public function setParams($params)
{
$this->params = $params;
}
/**
* Returns true if the requested parameter is set, false if it is not set.
*
* @param string $name
*
* @return bool
*/
public function paramExists($name)
{
return isset($this->params[$name]);
}
/**
* Check request parameters.
* Outputs error response if an invalid parameter is found.
*
* @param array $required required parameters for the current request
* @param array $optional optional parameters for the current request
*
* @return bool
*/
public function checkParams($required = [], $optional = [])
{
// Get allowed parameters for current request
$requiredParams = array_merge(['verb'], $required);
$validParams = array_merge($requiredParams, $optional);
// Check for missing or duplicate required parameters
foreach ($requiredParams as $k) {
if (!$this->paramExists($k)) {
$this->error('badArgument', "Missing {$k} parameter");
return false;
} elseif (is_array($this->getParam($k))) {
$this->error('badArgument', "Multiple values are not allowed for the {$k} parameter");
return false;
}
}
// Check for duplicate optional parameters
foreach ($optional as $k) {
if ($this->paramExists($k) && is_array($this->getParam($k))) {
$this->error('badArgument', "Multiple values are not allowed for the {$k} parameter");
return false;
}
}
// Check for illegal parameters
$request = Application::get()->getRequest();
foreach ($this->params as $k => $v) {
if (!in_array($k, $validParams)) {
$this->error('badArgument', "{$k} is an illegal parameter");
return false;
}
}
return true;
}
/**
* Returns formatted metadata response in specified format.
*
* @param string $format
*
* @return string
*/
public function formatMetadata($format, $record)
{
$formats = $this->metadataFormats();
$metadata = $formats[$format]->toXml($record);
return $metadata;
}
/**
* Checks if from and until parameters have been passed.
* If passed, validate and convert to UNIX timestamps.
*
* @param array $params request parameters
* @param int $from from timestamp (output parameter)
* @param int $until until timestamp (output parameter)
*
* @return bool
*/
public function extractDateParams($params, &$from, &$until)
{
if (isset($params['from'])) {
$from = OAIUtils::UTCtoTimestamp($params['from'], $this->config->granularity);
if ($from == 'invalid') {
$this->error('badArgument', 'Illegal from parameter');
return false;
} elseif ($from == 'invalid_granularity') {
$this->error('badArgument', 'Illegal granularity for from parameter');
return false;
}
}
if (isset($params['until'])) {
$until = OAIUtils::UTCtoTimestamp($params['until'], $this->config->granularity);
if ($until == 'invalid') {
$this->error('badArgument', 'Illegal until parameter');
return false;
} elseif ($until == 'invalid_granularity') {
$this->error('badArgument', 'Illegal granularity for until parameter');
return false;
}
// Check that until value is greater than or equal to from value
if (isset($from) && $from > $until) {
$this->error('badArgument', 'until parameter must be greater than or equal to from parameter');
return false;
}
// Check that granularities are equal
if (isset($from) && strlen($params['from']) != strlen($params['until'])) {
$this->error('badArgument', 'until and from parameters must be of the same granularity');
return false;
}
if (strlen($params['until']) == 10) {
// Until date is inclusive
$until += 86399;
}
}
return true;
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\oai\OAI', '\OAI');
define('OAIRECORD_STATUS_DELETED', OAI::OAIRECORD_STATUS_DELETED);
define('OAIRECORD_STATUS_ALIVE', OAI::OAIRECORD_STATUS_ALIVE);
}