diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b9ab33e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..deb86b8 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,121 @@ +### +# https://github.com/gitattributes/gitattributes/blob/master/Common.gitattributes +### + +# Auto detect text files and perform LF normalization +* text=auto + +# +# The above will handle all files NOT found below +# + +# Documents +*.bibtex text diff=bibtex +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain +*.md text diff=markdown +*.mdx text diff=markdown +*.tex text diff=tex +*.adoc text +*.textile text +*.mustache text +*.csv text eol=crlf +*.tab text +*.tsv text +*.txt text +*.sql text +*.epub diff=astextplain + +# Graphics +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.tif binary +*.tiff binary +*.ico binary +# SVG treated as text by default. +*.svg text +# If you want to treat it as binary, +# use the following line instead. +# *.svg binary +*.eps binary + +# Scripts +*.bash text eol=lf +*.fish text eol=lf +*.ksh text eol=lf +*.sh text eol=lf +*.zsh text eol=lf +# These are explicitly windows files and should use crlf +*.bat text eol=crlf +*.cmd text eol=crlf +*.ps1 text eol=crlf + +# Serialisation +*.json text +*.toml text +*.xml text +*.yaml text +*.yml text + +# Archives +*.7z binary +*.gz binary +*.tar binary +*.tgz binary +*.zip binary + +# Text files where line endings should be preserved +*.patch -text + +# +# Exclude files from exporting +# + +.gitattributes export-ignore +.gitignore export-ignore +.gitkeep export-ignore + +### +# https://github.com/gitattributes/gitattributes/blob/master/PHP.gitattributes +### + +# PHP files +*.php text eol=lf diff=php +*.phpt text eol=lf diff=php +*.phtml text eol=lf diff=html +*.twig text eol=lf +*.phar binary + +# Configuration +phpcs.xml text eol=lf +phpunit.xml text eol=lf +phpstan.neon text eol=lf +psalm.xml text eol=lf + +### +# Open Culture Consulting custom additions +### + +# Configuration +.editorconfig text eol=lf +*.dist.xml text eol=lf +*.xml.dist text eol=lf +*.neon text eol=lf + +# Generated documentation +doc/* linguist-generated=true + +# Exclude files from exporting +.github/* export-ignore +.phpdoc/* export-ignore +phpdoc.dist.xml export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1f17302 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/.phpdoc/cache/ +/.vscode/ +/vendor/ +.php-cs-fixer.php +phpcs.xml +phpdoc.xml +phpstan.neon +psalm.xml +TODO diff --git a/Classes/Exception.php b/Classes/Exception.php index c4d5b58..5d0d1a2 100644 --- a/Classes/Exception.php +++ b/Classes/Exception.php @@ -24,33 +24,34 @@ namespace OCC\OAI2; class Exception extends \Exception { + private array $errorTable = [ + 'badArgument' => [ + 'text' => 'The request includes illegal arguments, is missing required arguments, includes a repeated argument, or values for arguments have an illegal syntax.', + ], + 'badResumptionToken' => [ + 'text' => 'The value of the resumptionToken argument is invalid or expired.', + ], + 'badVerb' => [ + 'text' => 'Value of the verb argument is not a legal OAI-PMH verb, the verb argument is missing, or the verb argument is repeated.', + ], + 'cannotDisseminateFormat' => [ + 'text' => 'The metadata format identified by the value given for the metadataPrefix argument is not supported by the item or by the repository.', + ], + 'idDoesNotExist' => [ + 'text' => 'The value of the identifier argument is unknown or illegal in this repository.', + ], + 'noRecordsMatch' => [ + 'text' => 'The combination of the values of the from, until, set and metadataPrefix arguments results in an empty list.', + ], + 'noMetadataFormats' => [ + 'text' => 'There are no metadata formats available for the specified item.', + ], + 'noSetHierarchy' => [ + 'text' => 'The repository does not support sets.', + ] + ]; + public function __construct($code) { - $this->errorTable = [ - 'badArgument' => [ - 'text' => 'The request includes illegal arguments, is missing required arguments, includes a repeated argument, or values for arguments have an illegal syntax.', - ], - 'badResumptionToken' => [ - 'text' => 'The value of the resumptionToken argument is invalid or expired.', - ], - 'badVerb' => [ - 'text' => 'Value of the verb argument is not a legal OAI-PMH verb, the verb argument is missing, or the verb argument is repeated.', - ], - 'cannotDisseminateFormat' => [ - 'text' => 'The metadata format identified by the value given for the metadataPrefix argument is not supported by the item or by the repository.', - ], - 'idDoesNotExist' => [ - 'text' => 'The value of the identifier argument is unknown or illegal in this repository.', - ], - 'noRecordsMatch' => [ - 'text' => 'The combination of the values of the from, until, set and metadataPrefix arguments results in an empty list.', - ], - 'noMetadataFormats' => [ - 'text' => 'There are no metadata formats available for the specified item.', - ], - 'noSetHierarchy' => [ - 'text' => 'The repository does not support sets.', - ], - ]; parent::__construct($this->errorTable[$code]['text']); $this->code = $code; } diff --git a/Classes/Response.php b/Classes/Response.php index d2d3c1f..6a7a37c 100644 --- a/Classes/Response.php +++ b/Classes/Response.php @@ -24,7 +24,11 @@ namespace OCC\OAI2; class Response { - public $doc; // DOMDocument. Handle of current XML Document object + public \DOMDocument $doc; // DOMDocument. Handle of current XML Document object + + private string $verb = ''; + + private \DOMElement $verbNode; public function __construct($uri, $verb, $request_args) { if (substr($uri, -1, 1) == '/') { @@ -74,7 +78,7 @@ class Response { * @param string $nodeName The name of appending node. * @param string $value The content of appending node. */ - public function addToVerbNode($nodeName, $value = null) { + public function addToVerbNode($nodeName, $value = '') { if (!isset($this->verbNode) && !empty($this->verb)) { $this->verbNode = $this->addChild($this->doc->documentElement, $this->verb); } diff --git a/Classes/Server.php b/Classes/Server.php index 0c93570..61b6f9d 100644 --- a/Classes/Server.php +++ b/Classes/Server.php @@ -34,6 +34,12 @@ class Server { private $max_records = 100; private $token_prefix = '/tmp/oai2-'; private $token_valid = 86400; + private $uri = ''; + private $identifyResponse; + private $listMetadataFormatsCallback; + private $listRecordsCallback; + private $getRecordCallback; + private Response $response; public function __construct($uri, $args, $identifyResponse, $callbacks, $config) { $this->uri = $uri; @@ -242,7 +248,7 @@ class Server { if ($records_count - $deliveredRecords > $maxItems) { $deliveredRecords += $maxItems; $restoken = $this->createResumptionToken($deliveredRecords, $metadataPrefix, $from, $until); - $expirationDatetime = gmstrftime('%Y-%m-%dT%TZ', time()+$this->token_valid); + $expirationDatetime = date('Y-m-d\TH:i:s\Z', time()+$this->token_valid); } elseif (isset($this->args['resumptionToken'])) { // Last delivery, return empty resumptionToken $restoken = null; @@ -297,10 +303,14 @@ class Server { } private function formatTimestamp($datestamp) { - if (is_array($time = strptime($datestamp, '%Y-%m-%dT%H:%M:%SZ')) || is_array($time = strptime($datestamp, '%Y-%m-%d'))) { - return gmmktime($time['tm_hour'], $time['tm_min'], $time['tm_sec'], $time['tm_mon'] + 1, $time['tm_mday'], $time['tm_year']+1900); - } else { + $time = date_parse_from_format('Y-m-d\TH:i:s\Z', $datestamp); + if ($time['error_count'] > 0) { + $time = date_parse_from_format('Y-m-d', $datestamp); + } + if ($time['error_count'] > 0) { return null; + } else { + return gmmktime($time['hour'], $time['minute'], $time['second'], $time['month'] + 1, $time['day'], $time['year']); } } @@ -309,7 +319,7 @@ class Server { if ($datetime === false) { $datetime = \DateTime::createFromFormat('Y-m-d', $date); } - return ($datetime !== false) && !array_sum($datetime->getLastErrors()); + return ($datetime !== false); } } diff --git a/README.md b/README.md index 288ddee..84fc173 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,20 @@ # Simple OAI-PMH 2.0 Data Provider -This is a stand-alone and easy to install data provider implementing the [Open Archives Initiative's Protocol for Metadata Harvesting (OAI-PMH)](https://openarchives.org/pmh/). It serves records in any metadata format from directories of XML files using the directory name as `metadataPrefix`, the filename as `identifier` and the filemtime as datestamp. 0-byte files are considered deleted records and handled accordingly. Resumption tokens are managed using files. Sets are currently not supported. +This is a stand-alone and easy to install data provider implementing the [Open Archives Initiative's Protocol for Metadata Harvesting (OAI-PMH)](https://openarchives.org/pmh/). It serves records in any metadata format from directories of XML files using the directory name as `metadataPrefix`, the filename as `identifier` and the filemtime as timestamp. 0-byte files are considered deleted records and handled accordingly. Resumption tokens are managed using files. Sets are currently not supported. Just put the records as XML files in the data directory, adjust a few configuration settings and you are ready to go! A demo installation can be found [here](https://demo.opencultureconsulting.com/oai_pmh/?verb=Identify). -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/7a12022611d047ad9ef9a0c3aadb986a)](https://www.codacy.com/gh/opencultureconsulting/oai_pmh) +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/7a12022611d047ad9ef9a0c3aadb986a)](https://www.codacy.com/gh/opencultureconsulting/simple-oai-pmh) ## Installation -1. Run `composer create-project opencultureconsulting/oai_pmh `. +1. Run `composer create-project opencultureconsulting/simple-oai-pmh `. -2. Create a data directory in a not publicly accessible location outside of ``. Create a subdirectory inside the specified data directory for every format (i. e. `metadataPrefix`) you want to provide. +2. Create a data directory in a location not publicly accessible (i. e. outside of ``). Create a subdirectory inside the specified data directory for every format (i. e. `metadataPrefix`) you want to provide. -3. Copy `Configuration/Main.template.php` to `Configuration/Main.php` and adjust the settings according to your preferences. Don't forget pointing `$config['dataDirectory']` to your newly created data directory. +3. Copy `Configuration/Main.template.php` to `Configuration/Main.php` and edit the settings according to your preferences. Don't forget pointing `$config['dataDirectory']` to your newly created data directory. 4. Put the records into the respective directories according to their format. Each record has to be a separate XML file with its `identifier` as filename (e. g. the file *12345678.xml* can be adressed using the `identifier` *12345678*). Optionally you can maintain deletions by keeping 0-byte files for deleted records. @@ -24,7 +24,7 @@ A demo installation can be found [here](https://demo.opencultureconsulting.com/o 1. Backup `Configuration/Main.php`! -2. Delete `` and re-install by running `composer create-project opencultureconsulting/oai_pmh `. +2. Delete `` and re-install by running `composer create-project opencultureconsulting/simple-oai-pmh `. 3. Move your configuration back into `Configuration/Main.php`. diff --git a/Resources/Stylesheet.xsl b/Resources/Stylesheet.xsl index 7e10d8b..a48dbc1 100644 --- a/Resources/Stylesheet.xsl +++ b/Resources/Stylesheet.xsl @@ -165,7 +165,7 @@

You are viewing an HTML version of the XML OAI-PMH response. To see the underlying XML as it appears to any OAI-PMH harvester use your web browser's view source option or disable XSLT processing.

-

This XSL script was originally written by Christopher Gutteridge at University of Southampton for the EPrints project and was later adapted by Sebastian Meyer at Open Culture Consulting to be more generally applicable to other OAI-PMH interfaces. It is available on GitHub for free!

+

This XSL script was originally written by Christopher Gutteridge at University of Southampton for the EPrints project and was later adapted by Sebastian Meyer at Open Culture Consulting to be more generally applicable to other OAI-PMH interfaces. It is available on GitHub for free!

diff --git a/composer.json b/composer.json index ade45fb..ef9aab7 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "opencultureconsulting/oai_pmh", + "name": "opencultureconsulting/simple-oai-pmh", "description": "This is a stand-alone and easy to install data provider implementing the Open Archives Initiative's Protocol for Metadata Harvesting (OAI-PMH).", "type": "project", "keywords": [ @@ -9,7 +9,7 @@ "code4lib", "repository" ], - "homepage": "https://github.com/opencultureconsulting/oai_pmh", + "homepage": "https://github.com/opencultureconsulting/simple-oai-pmh", "readme": "README.md", "license": ["GPL-3.0-or-later"], "authors": [ @@ -41,14 +41,17 @@ } ], "support": { - "issues": "https://github.com/opencultureconsulting/oai_pmh/issues", - "source": "https://github.com/opencultureconsulting/oai_pmh", - "docs": "https://github.com/opencultureconsulting/oai_pmh/blob/master/README.md" + "issues": "https://github.com/opencultureconsulting/simple-oai-pmh/issues", + "source": "https://github.com/opencultureconsulting/simple-oai-pmh", + "docs": "https://github.com/opencultureconsulting/simple-oai-pmh/blob/master/README.md" }, "require": { - "php": "^7.0", + "php": "7.4.*|8.0.*|8.1.*|8.2.*", "ext-xml": "*" }, + "replace": { + "opencultureconsulting/oai_pmh": "self.version" + }, "autoload": { "psr-4": { "OCC\\OAI2\\": "Classes/" diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..7639c60 --- /dev/null +++ b/composer.lock @@ -0,0 +1,21 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "af0a24a6f06b7a8dc4ecd3084647403e", + "packages": [], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "7.4.*|8.0.*|8.1.*|8.2.*", + "ext-xml": "*" + }, + "platform-dev": [], + "plugin-api-version": "2.6.0" +}