diff --git a/index.php b/index.php index c2c2ca5..46588b5 100644 --- a/index.php +++ b/index.php @@ -9,33 +9,33 @@ * the support of on-the-fly output compression which may significantly * reduce the amount of data being transfered. * - * This package has been inspired by PHP OAI Data Provider developed by Heinrich Stamerjohanns at University of Oldenburg. + * This package has been inspired by PHP OAI Data Provider developed by Heinrich Stamerjohanns at University of Oldenburg. * Some of the functions and algorithms used in this code were transplanted from his implementation at http://physnet.uni-oldenburg.de/oai/. * - * Database support is supported through PDO (PHP Data Objects included in - * the PHP distribution), so almost any popular SQL-database can be + * Database support is supported through PDO (PHP Data Objects included in + * the PHP distribution), so almost any popular SQL-database can be * used without any change in the code. Only thing need to do is to configure * database connection and define a suitable data structure. * - * The repository can be quite easily configured by just editing + * The repository can be quite easily configured by just editing * oaidp-config.php, most possible values and options are explained. * * \section req_sec Requirements * - A running web server + PHP version 5.0 or above. - * - A databse can be connected by PDO. + * - A databse can be connected by PDO. * * \section install_sec Installation * *- Copy the the files in source package to a location under your * document root of your web server. The directory structure should be - * preserved. + * preserved. *- Change to that directory (e.g. cd /var/www/html/oai). *- Allow your webserver to write to the token directory. * The default token directory is /tmp which does not need any attention. - *- Edit oaidp-config.php. Almost all possible options are - * explained. It is assumed that basic elements of a record are stored in + *- Edit oaidp-config.php. Almost all possible options are + * explained. It is assumed that basic elements of a record are stored in * one simple table. You can find sql examples of table definition in doc folder. - * If your data is organized differently, you have to adjust the Query functions + * If your data is organized differently, you have to adjust the Query functions * to reflect it and even develop your own code. *- Check your oai site through a web browser. e.g. : \code http://localhost/oai/ \endcode *- SELinux needs special treatments for database connection and other permission. @@ -91,14 +91,14 @@ $MY_URI = substr($MY_URI, 0, $pos). '/oai2.php';

This implementation completely complies to OAI-PMH 2.0, including the support of on-the-fly output compression which may significantly reduce the amount of data being transfered.

-

This package has been inspired by PHP OAI Data Provider developed by Heinrich Stamerjohanns at University of Oldenburg. +

This package has been inspired by PHP OAI Data Provider developed by Heinrich Stamerjohanns at University of Oldenburg. Some of the functions and algorithms used in this code were transplanted from his implementation.

-

Database is supported through PDO, so almost any popular SQL-database which has PDO driver can be used without any change in the code.

+

Database is supported through PDO, so almost any popular SQL-database which has PDO driver can be used without any change in the code.

It uses DOM extension,an extension included in every PHP installation since version 5, for generating XML files. With PHP 5 or above no extra extension is needed.

-

The repository can be quite easily configured by just editing oai2/oaidp-config.php, most possible values and options are explained. +

The repository can be quite easily configured by just editing oai2/oaidp-config.php, most possible values and options are explained. For requirements and instructions to install and configure, please reference the documentation.

Once you have setup your Data Provider, you can the easiliy check the generated answers (it will be XML) of your Data Provider @@ -106,7 +106,7 @@ by clicking on the test links below.

For simple visual tests set $SHOW_QUERY_ERROR to TRUE and $CONTENT_TYPE to text/plain, so you can easily read the generated XML responses in your browser.

-

Remember, GetRecord needs identifier to work. +

Remember, GetRecord needs identifier to work. So please change it use your own or you should see a response with error message.

diff --git a/oai2.php b/oai2.php index 9f5acd7..a00015d 100644 --- a/oai2.php +++ b/oai2.php @@ -1,18 +1,5 @@ "The value '{$value}' of attribute '{$argument}' on element 'request' is not valid with respect to its type, 'UTCdatetimeType'.", 'code' => 'badArgument', ), - 'badResumptionToken' => array( 'text' => "The resumptionToken '{$value}' does not exist or has already expired.", ), @@ -32,12 +31,6 @@ class OAI2Exception extends Exception { ), 'idDoesNotExist' => array( 'text' => "The value '{$value}' of the identifier does not exist in this repository.", - /* - if (!is_valid_uri($value)) { - 'code' = 'badArgument', - 'text' .= ' Invalidated URI has been detected.', - } - */ ), 'missingArgument' => array( 'text' => "The required argument '{$argument}' is missing in the request.", diff --git a/oai2server.php b/oai2server.php index ef0ef8c..ee918ba 100644 --- a/oai2server.php +++ b/oai2server.php @@ -1,195 +1,141 @@ errors[] = new OAI2Exception('noVerb'); - $this->errorResponse(); - } + } else { + $verbs = array('Identify', 'ListMetadataFormats', 'ListSets', 'ListIdentifiers', 'ListRecords', 'GetRecord'); + if (in_array($args['verb'], $verbs)) { - $this->verb = $args['verb']; - unset($args['verb']); - $this->args = $args; + $this->verb = $args['verb']; - $this->uri = $uri; + unset($args['verb']); - $this->identifyResponse = $identifyResponse; + $this->args = $args; - $this->listMetadataFormatsCallback = $callbacks['ListMetadataFormats']; - $this->listSetsCallback = $callbacks['ListSets']; - $this->listRecordsCallback = $callbacks['ListRecords']; - $this->getRecordCallback = $callbacks['GetRecord']; + $this->uri = $uri; - $this->response = new OAI2XMLResponse($this->uri, $this->verb, $this->args); + $this->identifyResponse = $identifyResponse; - $this->respond(); - } + $this->listMetadataFormatsCallback = $callbacks['ListMetadataFormats']; + $this->listSetsCallback = $callbacks['ListSets']; + $this->listRecordsCallback = $callbacks['ListRecords']; + $this->getRecordCallback = $callbacks['GetRecord']; - private function respond() { + $this->response = new OAI2XMLResponse($this->uri, $this->verb, $this->args); - switch ($this->verb) { + call_user_func(array($this, $this->verb)); - case 'Identify': $this->identify(); break; - - case 'ListMetadataFormats': $this->listMetadataFormats(); break; - - case 'ListSets': $this->listSets(); break; - - case 'ListIdentifiers': - case 'ListRecords': $this->listRecords(); break; - - case 'GetRecord': $this->getRecord(); break; - - default: $this->errors[] = new OAI2Exception('badVerb', $this->args['verb']); + } else { + $this->errors[] = new OAI2Exception('badVerb', $args['verb']); + } } if (empty($this->errors)) { - header(CONTENT_TYPE); $this->response->display(); } else { - $this->errorResponse(); + $errorResponse = new OAI2XMLResponse($this->uri, $this->verb, $this->args); + $oai_node = $errorResponse->doc->documentElement; + foreach($this->errors as $e) { + $node = $errorResponse->addChild($oai_node,"error",$e->getMessage()); + $node->setAttribute("code",$e->getOAI2Code()); + } + $errorResponse->display(); } } - private function errorResponse() { - $errorResponse = new OAI2XMLResponse($this->uri, $this->verb, $this->args); - $oai_node = $errorResponse->doc->documentElement; - foreach($this->errors as $e) { - $node = $errorResponse->addChild($oai_node,"error",$e->getMessage()); - $node->setAttribute("code",$e->getOAI2Code()); - } - header(CONTENT_TYPE); - $errorResponse->display(); - exit(); - } - - /** - * 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() { + public function Identify() { if (count($this->args) > 0) { - foreach($args as $key => $val) { + foreach($this->args as $key => $val) { $this->errors[] = new OAI2Exception('badArgument', $key, $val); } - $this->errorResponse(); - } - - foreach($this->identifyResponse as $key => $val) { - $this->response->addToVerbNode($key, $val); + } else { + foreach($this->identifyResponse as $key => $val) { + $this->response->addToVerbNode($key, $val); + } } } - /** - * Response to Verb ListMetadataFormats - * - * The information of supported metadata formats - */ - public function listMetadataFormats() { + public function ListMetadataFormats() { foreach ($this->args as $argument => $value) { if ($argument != 'identifier') { $this->errors[] = new OAI2Exception('badArgument', $argument, $value); } } - if (!empty($this->errors)) { - $this->errorResponse(); - } - - try { - if ($formats = call_user_func($this->listMetadataFormatsCallback, $this->args['identifier'])) { - foreach($formats as $key => $val) { - $cmf = $this->response->addToVerbNode("metadataFormat"); - $this->response->addChild($cmf,'metadataPrefix',$key); - $this->response->addChild($cmf,'schema',$val['schema']); - $this->response->addChild($cmf,'metadataNamespace',$val['metadataNamespace']); + if (empty($this->errors)) { + try { + if ($formats = call_user_func($this->listMetadataFormatsCallback, $this->args['identifier'])) { + foreach($formats as $key => $val) { + $cmf = $this->response->addToVerbNode("metadataFormat"); + $this->response->addChild($cmf,'metadataPrefix',$key); + $this->response->addChild($cmf,'schema',$val['schema']); + $this->response->addChild($cmf,'metadataNamespace',$val['metadataNamespace']); + } + } else { + $this->errors[] = new OAI2Exception('noMetadataFormats'); } - } else { - $this->errors[] = new OAI2Exception('noMetadataFormats'); + } catch (OAI2Exception $e) { + $this->errors[] = $e; } - } catch (OAI2Exception $e) { - $this->errors[] = $e; } } - /** - * Response to Verb ListSets - * - * Lists what sets are available to records in the system. - * This variable is filled in config-sets.php - */ - public function listSets() { + public function ListSets() { if (isset($this->args['resumptionToken'])) { if (count($this->args) > 1) { $this->errors[] = new OAI2Exception('exclusiveArgument'); } else { - if ((int)$val+TOKEN_VALID < time()) { + if ((int)$val+$this->token_valid < time()) { $this->errors[] = new OAI2Exception('badResumptionToken'); } } + $resumptionToken = $this->args['resumptionToken']; + } else { + $resumptionToken = null; } if (!empty($this->errors)) { - $this->errorResponse(); - } + if ($sets = call_user_func($this->listSetsCallback, $resumptionToken)) { - if ($sets = call_user_func($this->listSetsCallback)) { + foreach($sets as $set) { - foreach($sets as $set) { + $setNode = $this->response->addToVerbNode("set"); - $setNode = $this->response->addToVerbNode("set"); - - foreach($set as $key => $val) { - if($key=='setDescription') { - $desNode = $this->response->addChild($setNode,$key); - $des = $this->response->doc->createDocumentFragment(); - $des->appendXML($val); - $desNode->appendChild($des); - } else { - $this->response->addChild($setNode,$key,$val); + foreach($set as $key => $val) { + if($key=='setDescription') { + $desNode = $this->response->addChild($setNode,$key); + $des = $this->response->doc->createDocumentFragment(); + $des->appendXML($val); + $desNode->appendChild($des); + } else { + $this->response->addChild($setNode,$key,$val); + } } } + } else { + $this->errors[] = new OAI2Exception('noSetHierarchy'); } - } else { - $this->errors[] = new OAI2Exception('noSetHierarchy'); } } - /** - * Response to Verb GetRecord - * - * Retrieve a record based its identifier. - * - * Local variables $metadataPrefix and $identifier need to be provided through global array variable $args - * by their indexes 'metadataPrefix' and 'identifier'. - * The reset of information will be extracted from database based those two parameters. - */ - public function getRecord() { + public function GetRecord() { if (!isset($this->args['metadataPrefix'])) { $this->errors[] = new OAI2Exception('missingArgument', 'metadataPrefix'); @@ -202,52 +148,66 @@ class OAI2Server { if (!isset($this->args['identifier'])) { $this->errors[] = new OAI2Exception('missingArgument', 'identifier'); } - if (!empty($this->errors)) { - $this->errorResponse(); - } - try { - if ($record = call_user_func($this->getRecordCallback, $this->args['identifier'], $this->args['metadataPrefix'])) { + if (empty($this->errors)) { + try { + if ($record = call_user_func($this->getRecordCallback, $this->args['identifier'], $this->args['metadataPrefix'])) { - $identifier = $record['identifier']; + $identifier = $record['identifier']; - $datestamp = formatDatestamp($record['datestamp']); + $datestamp = $this->formatDatestamp($record['datestamp']); - $set = $record['set']; + $set = $record['set']; - $status_deleted = (isset($record['deleted']) && ($record['deleted'] == 'true') && - (($this->identifyResponse['deletedRecord'] == 'transient') || - ($this->identifyResponse['deletedRecord'] == 'persistent'))); + $status_deleted = (isset($record['deleted']) && ($record['deleted'] == 'true') && + (($this->identifyResponse['deletedRecord'] == 'transient') || + ($this->identifyResponse['deletedRecord'] == 'persistent'))); - $cur_record = $this->response->addToVerbNode('record'); - $cur_header = $this->response->createHeader($identifier, $datestamp, $set, $cur_record); - if ($status_deleted) { - $cur_header->setAttribute("status","deleted"); + $cur_record = $this->response->addToVerbNode('record'); + $cur_header = $this->response->createHeader($identifier, $datestamp, $set, $cur_record); + if ($status_deleted) { + $cur_header->setAttribute("status","deleted"); + } else { + $this->add_metadata($cur_record, $record); + } } else { - $this->add_metadata($cur_record, $record); + $this->errors[] = new OAI2Exception('idDoesNotExist', 'identifier', $identifier); } + } catch (OAI2Exception $e) { + $this->errors[] = $e; } - } catch (OAI2Exception $e) { - $this->errors[] = $e; } } - /** - * 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 $args by keywords. - */ - public function listRecords() { + public function ListIdentifiers() { + return $this->ListRecords(); + } + + public function ListRecords() { + + $maxItems = 1000; + $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'] : ''; if (isset($this->args['resumptionToken'])) { if (count($this->args) > 1) { $this->errors[] = new OAI2Exception('exclusiveArgument'); } else { - if ((int)$val+TOKEN_VALID < time()) { + if ((int)$val+$this->token_valid < time()) { $this->errors[] = new OAI2Exception('badResumptionToken'); + } else { + if (!file_exists($this->token_prefix.$this->args['resumptionToken'])) { + $this->errors[] = new OAI2Exception('badResumptionToken', '', $this->args['resumptionToken']); + } else { + if ($readings = $this->readResumptionToken($this->token_prefix.$this->args['resumptionToken'])) { + list($deliveredRecords, $metadataPrefix, $from, $until, $set) = $readings; + } else { + $this->errors[] = new OAI2Exception('badResumptionToken', '', $this->args['resumptionToken']); + } + } } } } else { @@ -260,99 +220,69 @@ class OAI2Server { } } if (isset($this->args['from'])) { - if(!checkDateFormat($this->args['from'])) { - $this->errors[] = new OAI2Exception('badGranularity', 'from', $this->args['from']); + if(!$this->checkDateFormat($this->args['from'])) { + $this->errors[] = new OAI2Exception('badGranularity', 'from', $this->args['from']); } } if (isset($this->args['until'])) { - if(!checkDateFormat($this->args['until'])) { - $this->errors[] = new OAI2Exception('badGranularity', 'until', $this->args['until']); + if(!$this->checkDateFormat($this->args['until'])) { + $this->errors[] = new OAI2Exception('badGranularity', 'until', $this->args['until']); } } } if (!empty($this->errors)) { - $this->errorResponse(); - } + try { - // Resume previous session? - if (isset($this->args['resumptionToken'])) { + $records_count = call_user_func($this->listRecordsCallback, $metadataPrefix, $from, $until, $set, true); - if (!file_exists(TOKEN_PREFIX.$this->args['resumptionToken'])) { - $this->errors[] = new OAI2Exception('badResumptionToken', '', $this->args['resumptionToken']); - } else { + $records = call_user_func($this->listRecordsCallback, $metadataPrefix, $from, $until, $set, false, $deliveredRecords, $maxItems); - if ($readings = $this->readResumptionToken(TOKEN_PREFIX.$this->args['resumptionToken'])) { - list($deliveredRecords, $metadataPrefix, $from, $until, $set) = $readings; - } else { - $this->errors[] = new OAI2Exception('badResumptionToken', '', $this->args['resumptionToken']); + foreach ($records as $record) { + + $identifier = $record['identifier']; + $datestamp = $this->formatDatestamp($record['datestamp']); + $setspec = $record['set']; + + $status_deleted = (isset($record['deleted']) && ($record['deleted'] === true) && + (($this->identifyResponse['deletedRecord'] == 'transient') || + ($this->identifyResponse['deletedRecord'] == 'persistent'))); + + if($this->args['verb'] == 'ListRecords') { + $cur_record = $this->response->createToVerNode('record'); + $cur_header = $this->response->createHeader($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->response->createHeader($identifier, $datestamp,$setspec); + } + if ($status_deleted) { + $cur_header->setAttribute("status","deleted"); + } } - } + // Will we need a new ResumptionToken? + if ($records_count - $deliveredRecords > $maxItems) { - if (!empty($this->errors)) { - $this->errorResponse(); - } + $deliveredRecords += $maxItems; + $restoken = $this->createResumptionToken($deliveredRecords); - } 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'] : ''; - } + $expirationDatetime = gmstrftime('%Y-%m-%dT%TZ', time()+$this->token_valid); - $maxItems = 1000; - try { - - $records_count = call_user_func($this->listRecordsCallback, $metadataPrefix, $from, $until, $set, true); - - $records = call_user_func($this->listRecordsCallback, $metadataPrefix, $from, $until, $set, false, $deliveredRecords, $maxItems); - - foreach ($records as $record) { - - $identifier = $record['identifier']; - $datestamp = formatDatestamp($record['datestamp']); - $setspec = $record['set']; - - $status_deleted = (isset($record['deleted']) && ($record['deleted'] === true) && - (($this->identifyResponse['deletedRecord'] == 'transient') || - ($this->identifyResponse['deletedRecord'] == 'persistent'))); - - if($this->args['verb'] == 'ListRecords') { - $cur_record = $this->response->createToVerNode('record'); - $cur_header = $this->response->createHeader($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->response->createHeader($identifier, $datestamp,$setspec); + } elseif (isset($args['resumptionToken'])) { + // Last delivery, return empty ResumptionToken + $restoken = null; + $expirationDatetime = null; } - if ($status_deleted) { - $cur_header->setAttribute("status","deleted"); + + if (isset($restoken)) { + $this->response->createResumptionToken($restoken,$expirationDatetime,$records_count,$deliveredRecords); } + + } catch (OAI2Exception $e) { + $this->errors[] = $e; } - - // Will we need a new ResumptionToken? - if ($records_count - $deliveredRecords > $maxItems) { - - $deliveredRecords += $maxItems; - $restoken = $this->createResumptionToken($deliveredRecords); - - $expirationDatetime = gmstrftime('%Y-%m-%dT%TZ', time()+TOKEN_VALID); - - } elseif (isset($args['resumptionToken'])) { - // Last delivery, return empty ResumptionToken - $restoken = null; - $expirationDatetime = null; - } - - if (isset($restoken)) { - $this->response->createResumptionToken($restoken,$expirationDatetime,$records_count,$deliveredRecords); - } - - } catch (OAI2Exception $e) { - $this->errors[] = $e; } } @@ -374,17 +304,17 @@ class OAI2Server { list($usec, $sec) = explode(" ", microtime()); $token = ((int)($usec*1000) + (int)($sec*1000)); - $fp = fopen (TOKEN_PREFIX.$token, 'w'); - if($fp==false) { + $fp = fopen ($this->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']}#"); + 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; + return $token; } private function readResumptionToken($resumptionToken) { @@ -393,10 +323,33 @@ class OAI2Server { if ($fp != false) { $filetext = fgets($fp, 255); $textparts = explode('#', $filetext); - fclose($fp); + fclose($fp); unlink($resumptionToken); $rtVal = array_values($textparts); - } - return $rtVal; + } + return $rtVal; + } + + /** + * All datestamps used in this system are GMT even + * return value from database has no TZ information + */ + private function formatDatestamp($datestamp) { + return date("Y-m-d\TH:i:s\Z",strtotime($datestamp)); + } + + /** + * The database uses datastamp without time-zone information. + * It needs to clean all time-zone informaion from time string and reformat it + */ + private function checkDateFormat($date) { + $date = str_replace(array("T","Z")," ",$date); + $time_val = strtotime($date); + if(!$time_val) return false; + if(strstr($date,":")) { + return date("Y-m-d H:i:s",$time_val); + } else { + return date("Y-m-d",$time_val); + } } } diff --git a/oai2xml.php b/oai2xml.php index 523e7d1..68da71d 100644 --- a/oai2xml.php +++ b/oai2xml.php @@ -27,6 +27,7 @@ class OAI2XMLResponse { function display() { $this->doc->formatOutput = true; $this->doc->preserveWhiteSpace = false; + header('Content-Type: text/xml'); echo $this->doc->saveXML(); } diff --git a/oaidp-util.php b/oaidp-util.php deleted file mode 100644 index 057196f..0000000 --- a/oaidp-util.php +++ /dev/null @@ -1,46 +0,0 @@ -oai_error code idDoesNotExist. - * \param $url Type: string - */ -function is_valid_uri($url) { - return((bool)preg_match('/^[-a-z\.0-9]+$/i', $url)); -} - -/** Validates attributes come with the query. - * It accepts letters, numbers, ':', '_', '.' and -. - * Here there are few more match patterns than is_valid_uri(): ':_'. - * \param $attrb Type: string - */ -function is_valid_attrb($attrb) { - return preg_match("/^[_a-zA-Z0-9\-\:\.]+$/",$attrb); -} - -/** All datestamps used in this system are GMT even - * return value from database has no TZ information - */ -function formatDatestamp($datestamp) { - return date("Y-m-d\TH:i:s\Z",strtotime($datestamp)); -} - -/** The database uses datastamp without time-zone information. - * It needs to clean all time-zone informaion from time string and reformat it - */ -function checkDateFormat($date) { - $date = str_replace(array("T","Z")," ",$date); - $time_val = strtotime($date); - if(!$time_val) return false; - if(strstr($date,":")) { - return date("Y-m-d H:i:s",$time_val); - } else { - return date("Y-m-d",$time_val); - } -}