simple-oai-pmh/oai2server.php

425 lines
16 KiB
PHP
Raw Normal View History

2013-05-12 01:06:17 +02:00
<?php
require_once('oai2exception.php');
require_once('oai2xml.php');
/**
* The content-type the WWW-server delivers back. For debug-puposes, "text/plain"
* is easier to view. On a production site you should use "text/xml".
2013-05-12 02:18:36 +02:00
*/
define('CONTENT_TYPE', 'Content-Type: text/xml');
/** After 24 hours resumptionTokens become invalid. Unit is second. */
define('TOKEN_VALID',24*3600);
/** Where token is saved and path is included */
define('TOKEN_PREFIX','/tmp/oai_pmh-');
2013-05-12 01:06:17 +02:00
class OAI2Server {
2013-05-12 02:18:36 +02:00
public $errors = array();
function __construct($args, $identifyResponse, $callbacks) {
2013-05-12 01:06:17 +02:00
$this->args = $args;
2013-05-12 02:18:36 +02:00
$this->identifyResponse = $identifyResponse;
$this->listMetadataFormatsCallback = $callbacks['ListMetadataFormats'];
$this->listSetsCallback = $callbacks['ListSets'];
$this->listRecordsCallback = $callbacks['ListRecords'];
$this->getRecordCallback = $callbacks['GetRecord'];
2013-05-12 02:18:36 +02:00
$this->respond();
}
private function respond() {
if (!isset($this->args['verb']) || empty($this->args['verb'])) {
$this->errors[] = new OAI2Exception('noVerb');
2013-05-12 02:18:36 +02:00
} else {
switch ($this->args['verb']) {
case 'Identify': $this->identify(); break;
2013-05-12 02:18:36 +02:00
case 'ListMetadataFormats': $this->listMetadataFormats(); break;
2013-05-12 02:18:36 +02:00
case 'ListSets': $this->listSets(); break;
2013-05-12 02:18:36 +02:00
case 'ListIdentifiers':
case 'ListRecords': $this->listRecords(); break;
2013-05-12 02:18:36 +02:00
case 'GetRecord': $this->getRecord(); break;
2013-05-12 02:18:36 +02:00
default: $this->errors[] = new OAI2Exception('badVerb', $this->args['verb']);
2013-05-12 02:18:36 +02:00
}
}
if (empty($this->errors)) {
if (isset($this->outputObj)) {
header(CONTENT_TYPE);
$this->outputObj->display();
} else {
exit("Nothing to output. May be a bug.");
}
2013-05-12 02:18:36 +02:00
} else {
$this->errorResponse();
}
}
private function errorResponse() {
$e = new OAI2XMLError($this->args,$this->errors);
2013-05-12 02:18:36 +02:00
header(CONTENT_TYPE);
$e->display();
exit();
}
2013-05-12 01:06:17 +02:00
/**
* Response to Verb Identify
*
* Tell the world what the data provider is. Usually it is static once the provider has been set up.
*
* http://www.openarchives.org/OAI/2.0/guidelines-oai-identifier.htm for details
*/
public function identify() {
2013-05-12 02:18:36 +02:00
if (count($this->args) > 1) {
foreach($args as $key => $val) {
if(strcmp($key,"verb")!=0) {
$this->errors[] = new OAI2Exception('badArgument', $key, $val);
2013-05-12 02:18:36 +02:00
}
}
}
2013-05-12 01:06:17 +02:00
$this->outputObj = new ANDS_Response_XML($this->args);
2013-05-12 02:18:36 +02:00
foreach($this->identifyResponse as $key => $val) {
$this->outputObj->add2_verbNode($key, $val);
2013-05-12 01:06:17 +02:00
}
}
/**
* Response to Verb ListMetadataFormats
*
* The information of supported metadata formats
2013-05-12 01:06:17 +02:00
*/
public function listMetadataFormats() {
2013-05-12 02:18:36 +02:00
$checkList = array("ops"=>array("identifier"));
$this->checkArgs($checkList);
2013-05-12 01:06:17 +02:00
try {
if ($formats = call_user_func($this->listMetadataFormatsCallback, $this->args['identifier'])) {
$this->outputObj = new ANDS_Response_XML($this->args);
foreach($formats as $key => $val) {
$cmf = $this->outputObj->add2_verbNode("metadataFormat");
$this->outputObj->addChild($cmf,'metadataPrefix',$key);
$this->outputObj->addChild($cmf,'schema',$val['schema']);
$this->outputObj->addChild($cmf,'metadataNamespace',$val['metadataNamespace']);
2013-05-12 01:06:17 +02:00
}
return $outputObj;
2013-05-12 01:06:17 +02:00
}
$this->errors[] = new OAI2Exception('noMetadataFormats');
} catch (OAI2Exception $e) {
$this->errors[] = $e;
2013-05-12 01:06:17 +02:00
}
}
/**
* Response to Verb ListSets
*
* Lists what sets are available to records in the system.
* This variable is filled in config-sets.php
*/
2013-05-12 02:18:36 +02:00
public function listSets() {
if (isset($this->args['resumptionToken']) && count($this->args) > 2) {
$this->errors[] = new OAI2Exception('exclusiveArgument');
2013-05-12 02:18:36 +02:00
}
$checkList = array("ops"=>array("resumptionToken"));
$this->checkArgs($checkList);
2013-05-12 01:06:17 +02:00
if ($sets = call_user_func($this->listSetsCallback)) {
$this->outputObj = new ANDS_Response_XML($this->args);
2013-05-12 01:06:17 +02:00
foreach($sets as $set) {
$setNode = $this->outputObj->add2_verbNode("set");
2013-05-12 01:06:17 +02:00
foreach($set as $key => $val) {
if($key=='setDescription') {
$desNode = $this->outputObj->addChild($setNode,$key);
$des = $this->outputObj->doc->createDocumentFragment();
2013-05-12 01:06:17 +02:00
$des->appendXML($val);
$desNode->appendChild($des);
} else {
$this->outputObj->addChild($setNode,$key,$val);
2013-05-12 01:06:17 +02:00
}
}
}
} else {
$this->errors[] = new OAI2Exception('noSetHierarchy');
2013-05-12 01:06:17 +02:00
}
}
/**
* Response to Verb GetRecord
*
* Retrieve a record based its identifier.
*
* Local variables <B>$metadataPrefix</B> and <B>$identifier</B> need to be provided through global array variable <B>$args</B>
* by their indexes 'metadataPrefix' and 'identifier'.
* The reset of information will be extracted from database based those two parameters.
*/
public function getRecord() {
2013-05-12 02:18:36 +02:00
$checkList = array("required"=>array("metadataPrefix","identifier"));
$this->checkArgs($checkList);
2013-05-12 01:06:17 +02:00
$metadataPrefix = $this->args['metadataPrefix'];
$metadataFormats = call_user_func($this->listMetadataFormatsCallback);
if (!isset($metadataFormats[$metadataPrefix])) {
$this->errors[] = new OAI2Exception('cannotDisseminateFormat', 'metadataPrefix', $metadataPrefix);
2013-05-12 01:06:17 +02:00
}
try {
if ($record = call_user_func($this->getRecordCallback, $this->args['identifier'], $metadataPrefix)) {
2013-05-12 01:06:17 +02:00
$identifier = $record['identifier'];
2013-05-12 01:06:17 +02:00
$datestamp = formatDatestamp($record['datestamp']);
2013-05-12 01:06:17 +02:00
$set = $record['set'];
2013-05-12 01:06:17 +02:00
$status_deleted = (isset($record['deleted']) && ($record['deleted'] == 'true') &&
(($this->identifyResponse['deletedRecord'] == 'transient') ||
($this->identifyResponse['deletedRecord'] == 'persistent')));
2013-05-12 01:06:17 +02:00
$this->outputObj = new ANDS_Response_XML($this->args);
$cur_record = $this->outputObj->create_record();
$cur_header = $this->outputObj->create_header($identifier, $datestamp, $set, $cur_record);
if ($status_deleted) {
$cur_header->setAttribute("status","deleted");
} else {
$this->add_metadata($cur_record, $record);
2013-05-12 01:06:17 +02:00
}
}
} catch (OAI2Exception $e) {
$this->errors[] = $e;
2013-05-12 01:06:17 +02:00
}
}
/**
* Response to Verb ListRecords
*
* Lists records according to conditions. If there are too many, a resumptionToken is generated.
* - If a request comes with a resumptionToken and is still valid, read it and send back records.
* - Otherwise, set up a query with conditions such as: 'metadataPrefix', 'from', 'until', 'set'.
* Only 'metadataPrefix' is compulsory. All conditions are accessible through global array variable <B>$args</B> by keywords.
*/
2013-05-12 02:18:36 +02:00
public function listRecords() {
if (isset($this->args['resumptionToken'])) {
2013-05-12 02:18:36 +02:00
if (count($this->args) > 2) {
$this->errors[] = new OAI2Exception('exclusiveArgument');
2013-05-12 02:18:36 +02:00
}
$checkList = array("ops"=>array("resumptionToken"));
} else {
$checkList = array("required"=>array("metadataPrefix"),"ops"=>array("from","until","set"));
}
$this->checkArgs($checkList);
2013-05-12 01:06:17 +02:00
$metadataFormats = call_user_func($this->listMetadataFormatsCallback, $this->args);
if (!isset($metadataFormats[$this->args['metadataPrefix']])) {
$this->errors[] = new OAI2Exception('cannotDisseminateFormat', 'metadataPrefix', $this->args['metadataPrefix']);
}
2013-05-12 01:06:17 +02:00
// Resume previous session?
if (isset($this->args['resumptionToken'])) {
2013-05-12 01:06:17 +02:00
if (!file_exists(TOKEN_PREFIX.$this->args['resumptionToken'])) {
$this->errors[] = new OAI2Exception('badResumptionToken', '', $this->args['resumptionToken']);
2013-05-12 01:06:17 +02:00
} else {
if ($readings = $this->readResumptionToken(TOKEN_PREFIX.$this->args['resumptionToken'])) {
list($deliveredRecords, $metadataPrefix, $from, $until, $set) = $readings;
2013-05-12 01:06:17 +02:00
} else {
$this->errors[] = new OAI2Exception('badResumptionToken', '', $this->args['resumptionToken']);
2013-05-12 01:06:17 +02:00
}
2013-05-12 01:06:17 +02:00
}
} else {
$deliveredRecords = 0;
$metadataPrefix = $this->args['metadataPrefix'];
$from = isset($this->args['from']) ? $this->args['from'] : '';
$until = isset($this->args['until']) ? $this->args['until'] : '';
$set = isset($this->args['set']) ? $this->args['set'] : '';
2013-05-12 01:06:17 +02:00
}
2013-05-12 02:18:36 +02:00
if (!empty($this->errors)) {
$this->errorResponse();
}
2013-05-12 01:06:17 +02:00
$maxItems = 1000;
2013-05-12 02:18:36 +02:00
try {
2013-05-12 01:06:17 +02:00
$records_count = call_user_func($this->listRecordsCallback, $metadataPrefix, $from, $until, $set, true);
2013-05-12 01:06:17 +02:00
$records = call_user_func($this->listRecordsCallback, $metadataPrefix, $from, $until, $set, false, $deliveredRecords, $maxItems);
2013-05-12 01:06:17 +02:00
$this->outputObj = new ANDS_Response_XML($this->args);
foreach ($records as $record) {
2013-05-12 01:06:17 +02:00
$identifier = $record['identifier'];
$datestamp = formatDatestamp($record['datestamp']);
$setspec = $record[$SQL['set']];
2013-05-12 01:06:17 +02:00
$status_deleted = (isset($record['deleted']) && ($record['deleted'] === true) &&
(($this->identifyResponse['deletedRecord'] == 'transient') ||
($this->identifyResponse['deletedRecord'] == 'persistent')));
if($this->args['verb'] == 'ListRecords') {
$cur_record = $this->outputObj->create_record();
$cur_header = $this->outputObj->create_header($identifier, $datestamp,$setspec,$cur_record);
if (!$status_deleted) {
$this->add_metadata($cur_record, $record);
}
} else { // for ListIdentifiers, only identifiers will be returned.
$cur_header = $this->outputObj->create_header($identifier, $datestamp,$setspec);
}
if ($status_deleted) {
$cur_header->setAttribute("status","deleted");
2013-05-12 01:06:17 +02:00
}
}
// Will we need a new ResumptionToken?
if ($records_count - $deliveredRecords > $maxItems) {
2013-05-12 01:06:17 +02:00
$deliveredRecords += $maxItems;
$restoken = $this->createResumptionToken($deliveredRecords);
2013-05-12 01:06:17 +02:00
$expirationDatetime = gmstrftime('%Y-%m-%dT%TZ', time()+TOKEN_VALID);
} elseif (isset($args['resumptionToken'])) {
// Last delivery, return empty ResumptionToken
$restoken = null;
$expirationDatetime = null;
2013-05-12 01:06:17 +02:00
}
if (isset($restoken)) {
$this->outputObj->create_resumpToken($restoken,$expirationDatetime,$records_count,$deliveredRecords);
2013-05-12 01:06:17 +02:00
}
} catch (OAI2Exception $e) {
$this->errors[] = $e;
2013-05-12 01:06:17 +02:00
}
}
private function add_metadata($cur_record, $record) {
2013-05-12 01:06:17 +02:00
$meta_node = $this->outputObj->addChild($cur_record ,"metadata");
2013-05-12 01:06:17 +02:00
$schema_node = $this->outputObj->addChild($meta_node, $record['metadata']['container_name']);
foreach ($record['metadata']['container_attributes'] as $name => $value) {
$schema_node->setAttribute($name, $value);
2013-05-12 01:06:17 +02:00
}
foreach ($record['metadata']['fields'] as $name => $value) {
$this->outputObj->addChild($schema_node, $name, $value);
2013-05-12 01:06:17 +02:00
}
}
2013-05-12 02:18:36 +02:00
/** Check if provided correct arguments for a request.
*
* Only number of parameters is checked.
* metadataPrefix has to be checked before it is used.
* set has to be checked before it is used.
* resumptionToken has to be checked before it is used.
* from and until can easily checked here because no extra information
* is needed.
*/
private function checkArgs($checkList) {
$metadataFormats = call_user_func($this->listMetadataFormatsCallback);
2013-05-12 02:18:36 +02:00
// "verb" has been checked before, no further check is needed
$verb = $this->args["verb"];
$test_args = $this->args;
unset($test_args["verb"]);
if(isset($checkList['required'])) {
for($i = 0; $i < count($checkList["required"]); $i++) {
if (isset($test_args[$checkList['required'][$i]]) == false) {
$this->errors[] = new OAI2Exception('missingArgument', $checkList["required"][$i]);
2013-05-12 02:18:36 +02:00
} else {
// if metadataPrefix is set, it is in required section
if(isset($test_args['metadataPrefix'])) {
$metadataPrefix = $test_args['metadataPrefix'];
if (!isset($metadataFormats[$metadataPrefix])) {
$this->errors[] = new OAI2Exception('cannotDisseminateFormat', 'metadataPrefix', $metadataPrefix);
2013-05-12 02:18:36 +02:00
}
}
unset($test_args[$checkList["required"][$i]]);
}
}
}
// check to see if there is unwanted
foreach($test_args as $key => $val) {
if(!in_array($key, $checkList["ops"])) {
$this->errors[] = new OAI2Exception('badArgument', $key, $val);
2013-05-12 02:18:36 +02:00
}
switch ($key) {
case 'from':
case 'until':
if(!checkDateFormat($val)) {
$this->errors[] = new OAI2Exception('badGranularity', $key, $val);
2013-05-12 02:18:36 +02:00
}
break;
case 'resumptionToken':
// only check for expiration
2013-05-12 02:18:36 +02:00
if((int)$val+TOKEN_VALID < time())
$this->errors[] = new OAI2Exception('badResumptionToken');
2013-05-12 02:18:36 +02:00
break;
}
}
if (!empty($this->errors)) {
$this->errorResponse();
}
}
private function createResumptionToken($delivered_records) {
list($usec, $sec) = explode(" ", microtime());
$token = ((int)($usec*1000) + (int)($sec*1000));
$fp = fopen (TOKEN_PREFIX.$token, 'w');
if($fp==false) {
exit("Cannot write. Writer permission needs to be changed.");
}
fputs($fp, "$delivered_records#");
fputs($fp, "$metadataPrefix#");
fputs($fp, "{$this->args['from']}#");
fputs($fp, "{$this->args['until']}#");
fputs($fp, "{$this->args['set']}#");
fclose($fp);
return $token;
}
private function readResumptionToken($resumptionToken) {
$rtVal = false;
$fp = fopen($resumptionToken, 'r');
if ($fp != false) {
$filetext = fgets($fp, 255);
$textparts = explode('#', $filetext);
fclose($fp);
unlink($resumptionToken);
$rtVal = array_values($textparts);
}
return $rtVal;
2013-05-12 02:18:36 +02:00
}
2013-05-12 01:06:17 +02:00
}