├── public
├── robots.txt
├── api.php
├── favicon-32x32.png
├── .htaccess
└── dini-logo.svg
├── api
├── test
│ ├── xml
│ │ ├── ListRecords
│ │ │ ├── invalid.xml
│ │ │ ├── no-records.xml
│ │ │ ├── no-completelistsize.xml
│ │ │ ├── unstable-resumptiontoken-2.xml
│ │ │ ├── no-responsedate.xml
│ │ │ ├── bad-record.xml
│ │ │ ├── no-expirationdate.xml
│ │ │ ├── invalid-responsedate.xml
│ │ │ ├── invalid-expirationdate.xml
│ │ │ ├── expiring-too-soon.xml
│ │ │ ├── missing-record.xml
│ │ │ ├── invalid-identifier.xml
│ │ │ ├── no-metadata.xml
│ │ │ ├── no-oaidc.xml
│ │ │ ├── invalid-datestamps.xml
│ │ │ ├── no-dc-rights.xml
│ │ │ ├── small-repo.xml
│ │ │ ├── invalid-dc-identifier.xml
│ │ │ ├── no-dc-type.xml
│ │ │ ├── about-without-provenance.xml
│ │ │ ├── no-dc-date.xml
│ │ │ ├── no-dc-type-coar.xml
│ │ │ ├── no-dc-creator.xml
│ │ │ ├── invalid-dc-date.xml
│ │ │ ├── duplicate-dc-date.xml
│ │ │ ├── no-dc-creator-orcid.xml
│ │ │ ├── no-dc-identifier.xml
│ │ │ ├── no-dc-title.xml
│ │ │ ├── no-dc-language.xml
│ │ │ ├── no-datacite.xml
│ │ │ ├── datacite-bad-doi.xml
│ │ │ ├── datacite-bad-metadata.xml
│ │ │ ├── invalid-dc-language.xml
│ │ │ ├── no-dc-subject-ddc.xml
│ │ │ ├── datacite-no-match.xml
│ │ │ ├── good-2.xml
│ │ │ └── batchsize-too-small.xml
│ │ ├── ListIdentifiers
│ │ │ ├── empty.xml
│ │ │ └── good.xml
│ │ ├── Identify
│ │ │ ├── no-admin-email.xml
│ │ │ ├── no-description.xml
│ │ │ ├── invalid-admin-email.xml
│ │ │ ├── no-english-description.xml
│ │ │ ├── bad-protocol-version.xml
│ │ │ ├── invalid-description.xml
│ │ │ ├── no-deletedrecord.xml
│ │ │ ├── good.xml
│ │ │ └── invalid-deletedrecord.xml
│ │ ├── ListSets
│ │ │ ├── no-sets.xml
│ │ │ ├── bad.xml
│ │ │ └── good.xml
│ │ ├── ListMetadataFormats
│ │ │ ├── empty.xml
│ │ │ ├── no-datacite.xml
│ │ │ └── good.xml
│ │ ├── GetRecord
│ │ │ ├── unknown-record-id.xml
│ │ │ ├── invalid.xml
│ │ │ ├── record-2.xml
│ │ │ └── record-1.xml
│ │ └── GetRecord-datacite
│ │ │ └── bad-metadata.xml
│ ├── bootstrap.php
│ ├── helpers.php
│ └── mock-oai-api
│ │ └── index.php
├── data
│ ├── status-types.json
│ ├── mail-template.txt
│ ├── doc-types.json
│ ├── coar-resource-types-3.1.json
│ └── schemas
│ │ └── oai-identifier.xsd
├── rules
│ ├── M_9_3_2Test.php
│ ├── M_9_1.php
│ ├── M_10_1Test.php
│ ├── M_10_3Test.php
│ ├── E_10_1Test.php
│ ├── M_10_2Test.php
│ ├── E_9_3Test.php
│ ├── M_9_1Test.php
│ ├── E_9_3.php
│ ├── E_10_2Test.php
│ ├── M_9_3_1Test.php
│ ├── E_10_4Test.php
│ ├── M_10_2.php
│ ├── M_10_3.php
│ ├── M_10_1.php
│ ├── M_10_4Test.php
│ ├── E_11_11Test.php
│ ├── E_10_4.php
│ ├── E_10_2.php
│ ├── M_10_4.php
│ ├── E_10_1.php
│ ├── M_9_4.php
│ ├── M_9_3_3Test.php
│ ├── M_11_1_3Test.php
│ ├── M_11_1_4Test.php
│ ├── E_11_10Test.php
│ ├── M_11_1_2Test.php
│ ├── M_11_1_1Test.php
│ ├── M_11_5Test.php
│ ├── M_11_1_5Test.php
│ ├── M_11_6Test.php
│ ├── E_11_8Test.php
│ ├── M_11_3Test.php
│ ├── M_9_3_1.php
│ ├── M_9_3_4Test.php
│ ├── M_9_3_2.php
│ ├── E_11_11.php
│ ├── M_11_8Test.php
│ ├── E_11_5Test.php
│ ├── M_11_7Test.php
│ ├── E_11_9Test.php
│ ├── M_11_1_3.php
│ ├── M_11_1_4.php
│ ├── M_11_1_2.php
│ ├── M_11_1_1.php
│ ├── M_11_1_5.php
│ ├── E_11_10.php
│ ├── M_11_3.php
│ ├── M_9_4Test.php
│ ├── M_11_6.php
│ ├── M_9_6Test.php
│ ├── E_10_3Test.php
│ ├── M_11_5.php
│ ├── M_9_6.php
│ ├── E_11_5.php
│ ├── M_9_3_4.php
│ ├── M_11_8.php
│ ├── M_10_5Test.php
│ ├── E_11_8.php
│ ├── M_9_3_3.php
│ ├── E_11_9.php
│ ├── M_11_7.php
│ ├── M_10_5.php
│ └── M_11_9Test.php
├── index.php
├── routes
│ ├── RouteIssues.php
│ └── RouteXml.php
├── bootstrap.php
└── classes
│ └── Route.php
├── favicon.ico
├── jsconfig.json
├── frontend
├── styles
│ ├── mixins
│ │ ├── monospace.scss
│ │ ├── caps.scss
│ │ └── dotted-background.scss
│ ├── mixins.scss
│ └── settings.scss
├── components
│ ├── VLink.vue
│ ├── VTranslation.vue
│ ├── VWarning.vue
│ ├── VDiniScore.vue
│ ├── VLoading.vue
│ ├── TheFooter.vue
│ ├── VError.vue
│ ├── VModal.vue
│ └── VDecoration.vue
├── modules
│ ├── meta.js
│ ├── error.js
│ ├── fx.js
│ ├── cache.js
│ ├── format.js
│ ├── ui.js
│ └── http.js
├── views
│ ├── ValidationView.vue
│ ├── LegalView.vue
│ ├── NotFoundView.vue
│ ├── ContactView.vue
│ └── ResultView.vue
├── main.js
├── config.js
├── App.vue
└── router.js
├── phpstan.neon
├── docker-compose.yml
├── .editorconfig
├── composer.json
├── .gitignore
├── index.html
├── Dockerfile
├── .stylelintrc.js
├── README.md
├── phpunit.xml
├── .eslintrc.cjs
├── doc
└── api.md
├── vite.config.js
├── package.json
└── pint.json
/public/robots.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/api/test/xml/ListRecords/invalid.xml:
--------------------------------------------------------------------------------
1 | This is not XML.
2 |
--------------------------------------------------------------------------------
/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UB-Mannheim/dini-validator/main/favicon.ico
--------------------------------------------------------------------------------
/public/api.php:
--------------------------------------------------------------------------------
1 |
2 |
$2',
14 | $result->issues[0]->text,
15 | );
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/api/index.php:
--------------------------------------------------------------------------------
1 | setHttpHeader();
12 | $route->serve();
13 | } else {
14 | http_response_code(404);
15 | echo json_encode(['error' => 'Unknown route']);
16 | }
17 |
--------------------------------------------------------------------------------
/api/test/xml/ListSets/no-sets.xml:
--------------------------------------------------------------------------------
1 |
2 | open_access is missing in ListSets',
14 | getIssueText($result->issues[0]),
15 | );
16 |
17 | $result = runRule('ListSets/good');
18 | $this->assertEquals(0, $result->issuesCount);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/api/rules/M_10_3Test.php:
--------------------------------------------------------------------------------
1 | assertEquals(
13 | 'Sets for document types are missing in ListSets.',
14 | getIssueText($result->issues[0]),
15 | );
16 |
17 | $result = runRule('ListSets/good');
18 | $this->assertEquals(0, $result->issuesCount);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/api/test/bootstrap.php:
--------------------------------------------------------------------------------
1 | /dev/null 2>&1 & echo $!', $output);
11 | $pid = (int) $output[0];
12 |
13 | // Kill the local web server when the process ends
14 | register_shutdown_function(function () use ($pid) {
15 | exec("kill $pid");
16 | });
17 |
--------------------------------------------------------------------------------
/api/rules/E_10_1Test.php:
--------------------------------------------------------------------------------
1 | assertEquals(
13 | 'Sets for publication status are missing in ListSets.',
14 | getIssueText($result->issues[0]),
15 | );
16 |
17 | $result = runRule('ListSets/good');
18 | $this->assertEquals(0, $result->issuesCount);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/api/rules/M_10_2Test.php:
--------------------------------------------------------------------------------
1 | assertEquals(
13 | 'Sets for DDC subject groups are missing in ListSets.',
14 | getIssueText($result->issues[0]),
15 | );
16 |
17 | $result = runRule('ListSets/good');
18 | $this->assertEquals(0, $result->issuesCount);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/api/test/xml/ListRecords/bad-record.xml:
--------------------------------------------------------------------------------
1 |
2 |
about is set, but provenance is missing in about-without-provenance',
14 | getIssueText($result->issues[0]),
15 | );
16 |
17 | $result = runRule('ListRecords/good');
18 | $this->assertEquals(0, $result->issuesCount);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/api/test/xml/ListRecords/expiring-too-soon.xml:
--------------------------------------------------------------------------------
1 |
2 | $1 is set, but $2 is missing in $3',
17 | 'about',
18 | 'provenance',
19 | (string) $record->header->identifier,
20 | );
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/api/rules/E_10_2Test.php:
--------------------------------------------------------------------------------
1 | assertEquals(0, $result->issuesCount);
13 |
14 | $result = runRule('ListRecords/batchsize-too-small');
15 | $this->assertEquals(
16 | 'The batch size of ListRecords is 2.',
17 | getIssueText($result->issues[0]),
18 | );
19 |
20 | $result = runRule('ListRecords/fake-100');
21 | $this->assertEquals(0, $result->issuesCount);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/api/rules/M_9_3_1Test.php:
--------------------------------------------------------------------------------
1 | assertEquals('Identify', $result->issues[0]->verb);
13 | $this->assertEquals('Schema validation errors in $1: $2', $result->issues[0]->text);
14 | $this->assertEquals('Identify', $result->issues[0]->values[0]);
15 | $this->assertCount(2, $result->issues[0]->values);
16 |
17 | $result = runRule('Identify/good');
18 | $this->assertEquals(0, $result->issuesCount);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/api/test/xml/ListRecords/no-metadata.xml:
--------------------------------------------------------------------------------
1 |
2 | resumptionToken without completeListSize in ListRecords',
17 | getIssueText($result->issues[0]),
18 | );
19 |
20 | $result = runRule('ListRecords/fake-100');
21 | $this->assertEquals(0, $result->issuesCount);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/api/test/xml/GetRecord/invalid.xml:
--------------------------------------------------------------------------------
1 |
2 | $1 is missing in $2',
25 | 'open_access',
26 | 'ListSets',
27 | );
28 |
29 | return;
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/api/test/xml/GetRecord-datacite/bad-metadata.xml:
--------------------------------------------------------------------------------
1 |
2 | deletedRecord is missing in Identify',
14 | getIssueText($result->issues[0]),
15 | );
16 |
17 | $result = runRule('Identify/invalid-deletedrecord');
18 | $this->assertEquals(
19 | 'deletedRecord is invalid in Identify',
20 | getIssueText($result->issues[0]),
21 | );
22 |
23 | $result = runRule('Identify/good');
24 | $this->assertEquals(0, $result->issuesCount);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/api/rules/E_11_11Test.php:
--------------------------------------------------------------------------------
1 | assertEquals(
13 | 'Invalid response to ListMetadataFormats',
14 | getIssueText($result->issues[0]),
15 | );
16 |
17 | $result = runRule('ListMetadataFormats/no-datacite');
18 | $this->assertEquals(
19 | 'oai_datacite is missing in ListMetadataFormats',
20 | getIssueText($result->issues[0]),
21 | );
22 |
23 | $result = runRule('ListMetadataFormats/good');
24 | $this->assertEquals(0, $result->issuesCount);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/frontend/config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | // URL of the validator API
3 | // Protocol and domain may be omitted if same as frontend.
4 | apiUrl:
5 | process.env.NODE_ENV === 'development'
6 | ? '//localhost:8001/public/api'
7 | : '/api',
8 |
9 | // Default language
10 | // The value must match a key in "languages".
11 | defaultLang: 'en',
12 |
13 | // Available languages
14 | // Keys should have 2 characters and are used in URLs, and there must be
15 | // a translation file for every key other than the default language.
16 | languages: {
17 | en: 'English',
18 | de: 'Deutsch',
19 | },
20 |
21 | // Available options for how many records are checked, 0 means "all"
22 | // NOTE: This must also be set in the API config (api/config.php).
23 | recordsLimitOptions: [100, 500, 1000, 5000, 0],
24 | }
25 |
--------------------------------------------------------------------------------
/frontend/components/VWarning.vue:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 | .warning(role="alert")
6 | IconExclamation.icon
7 | .message
8 | slot
9 |
10 |
11 |
46 |
--------------------------------------------------------------------------------
/frontend/views/NotFoundView.vue:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 | TheHeader
6 |
7 | main
8 | h1 404
9 |
10 | VTranslation(en)
11 | p This page does not exist. Sorry for for any discombobulation.
12 | p If you followed a link leading to this page, please #[a(href="mailto:gs@dini.de") drop us an email], and we will fix it as soon as possible.
13 | VTranslation(de)
14 | p Diese Seite gibt es nicht. Tut uns leid, wenn Sie sich jetzt etwas verloren fühlen.
15 | p Wenn Sie ein Link hierher geführt hat, #[a(href="mailto:gs@dini.de") schreiben Sie uns bitte eine E-Mail], und wir werden uns schnellstmöglich darum kümmern.
16 |
17 | p.center(style="margin-top: 1.5rem")
18 | VLink.button.outline(to="home")
19 | VTranslation(en) Go Home
20 | VTranslation(de) Zur Startseite
21 |
22 | TheFooter
23 |
24 |
--------------------------------------------------------------------------------
/frontend/App.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 | Teleport(to="head")
7 | //- NOTE: Document title is set via setTitle
8 | VTranslation(en)
9 | meta(
10 | name="description"
11 | content="The DINI Validator tests an OAI-PMH API according to the requirements of the DINI Certificate for Open Access Publication Services 2022."
12 | )
13 | VTranslation(de)
14 | meta(
15 | name="description"
16 | content="Der DINI Validator prüft eine OAI-PMH-API gemäß den Anforderungen des DINI-Zertifikats für Open-Access-Publikationsdienste 2022."
17 | )
18 |
19 | RouterView
20 |
21 |
22 |
32 |
--------------------------------------------------------------------------------
/api/rules/E_10_4.php:
--------------------------------------------------------------------------------
1 | ListRecords->resumptionToken)) {
14 | $this->finish('Not relevant due to small repository size');
15 |
16 | return;
17 | }
18 |
19 | if (empty($xml->ListRecords->resumptionToken['completeListSize'])) {
20 | $this->addFatalIssue(
21 | 'ListRecords',
22 | '$1 without $2 in $3',
23 | 'resumptionToken',
24 | 'completeListSize',
25 | 'ListRecords',
26 | );
27 |
28 | return;
29 | }
30 |
31 | $this->finish();
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/api/test/xml/Identify/bad-protocol-version.xml:
--------------------------------------------------------------------------------
1 |
2 | $1 is missing in $2',
17 | 'deletedRecord',
18 | 'Identify',
19 | );
20 |
21 | return;
22 | }
23 | if (! in_array($xml->Identify->deletedRecord, ['no', 'persistent', 'transient'])) {
24 | $this->addFatalIssue(
25 | 'Identify',
26 | '$1 is invalid in $2',
27 | 'deletedRecord',
28 | 'Identify',
29 | );
30 |
31 | return;
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DINI Validator
2 |
3 | The DINI Validator is a web application for checking OAI-PMH APIs as part of the DINI certification process.
4 |
5 | Users provide the URL of an OAI-PMH API and their email address, select the number of records to check and start the validation process. The validator then thoroughly checks this API by downloading and validating multiple XML responses, during which the progress is shown. Once completed, the results are displayed and an email is sent, containing a permalink to these results.
6 |
7 | The validator consists of a PHP-based backend, doubling as a JSON REST API, and a Vue-based frontend.
8 |
9 | ## Documentation
10 |
11 | 1. [Setup](doc/setup.md) – How to set up the application locally and on a server
12 | 1. [API](doc/api.md) – How the API (backend) works
13 | 1. [Rulesets](doc/rulesets.md) – How to modify the ruleset and add new validation rules
14 | 1. [Frontend](doc/frontend.md) – How the frontend works, including translations
15 |
--------------------------------------------------------------------------------
/api/rules/E_10_1.php:
--------------------------------------------------------------------------------
1 | statusTypes = json_decode($json);
17 | }
18 |
19 | public function check($xml, $isLastBatch): void
20 | {
21 | foreach ($xml->ListSets->set as $set) {
22 | if (in_array((string) $set->setSpec, $this->statusTypes)) {
23 | $this->finish();
24 |
25 | return;
26 | }
27 | }
28 |
29 | if ($isLastBatch) {
30 | $this->addFatalIssue(
31 | 'ListSets',
32 | 'Sets for publication status are missing in ListSets.',
33 | );
34 |
35 | return;
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/api/rules/M_9_4.php:
--------------------------------------------------------------------------------
1 | header->datestamp) {
14 | $this->addIssue(
15 | "GetRecord&identifier={$record->header->identifier}",
16 | '$1 is missing in $2',
17 | 'datestamp',
18 | (string) $record->header->identifier,
19 | );
20 |
21 | return;
22 | }
23 |
24 | if (! date_create((string) $record->header->datestamp)) {
25 | $this->addIssue(
26 | "GetRecord&identifier={$record->header->identifier}",
27 | '$1 is invalid in $2',
28 | 'datestamp',
29 | (string) $record->header->identifier,
30 | );
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/api/test/xml/ListMetadataFormats/no-datacite.xml:
--------------------------------------------------------------------------------
1 |
2 | metadata is missing in record-1',
14 | getIssueText($result->issues[0]),
15 | );
16 |
17 | $result = runRule('ListRecords/no-oaidc');
18 | $this->assertEquals(
19 | 'oai_dc is missing in missing-oaidc',
20 | getIssueText($result->issues[0]),
21 | );
22 |
23 | $result = runRule('ListRecords/no-dc-date');
24 | $this->assertEquals(
25 | 'dc:date is missing in record-1',
26 | getIssueText($result->issues[0]),
27 | );
28 |
29 | $result = runRule('ListRecords/good');
30 | $this->assertEquals(0, $result->issuesCount);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/api/rules/M_11_1_4Test.php:
--------------------------------------------------------------------------------
1 | assertEquals(
13 | 'metadata is missing in record-1',
14 | getIssueText($result->issues[0]),
15 | );
16 |
17 | $result = runRule('ListRecords/no-oaidc');
18 | $this->assertEquals(
19 | 'oai_dc is missing in missing-oaidc',
20 | getIssueText($result->issues[0]),
21 | );
22 |
23 | $result = runRule('ListRecords/no-dc-type');
24 | $this->assertEquals(
25 | 'dc:type is missing in record-1',
26 | getIssueText($result->issues[0]),
27 | );
28 |
29 | $result = runRule('ListRecords/good');
30 | $this->assertEquals(0, $result->issuesCount);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/api/rules/E_11_10Test.php:
--------------------------------------------------------------------------------
1 | assertEquals(
13 | 'metadata is missing in record-1',
14 | getIssueText($result->issues[0]),
15 | );
16 |
17 | $result = runRule('ListRecords/no-oaidc');
18 | $this->assertEquals(
19 | 'oai_dc is missing in missing-oaidc',
20 | getIssueText($result->issues[0]),
21 | );
22 |
23 | $result = runRule('ListRecords/no-dc-rights');
24 | $this->assertEquals(
25 | 'dc:rights is missing in record-1',
26 | getIssueText($result->issues[0]),
27 | );
28 |
29 | $result = runRule('ListRecords/good');
30 | $this->assertEquals(0, $result->issuesCount);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/api/rules/M_11_1_2Test.php:
--------------------------------------------------------------------------------
1 | assertEquals(
13 | 'metadata is missing in record-1',
14 | getIssueText($result->issues[0]),
15 | );
16 |
17 | $result = runRule('ListRecords/no-oaidc');
18 | $this->assertEquals(
19 | 'oai_dc is missing in missing-oaidc',
20 | getIssueText($result->issues[0]),
21 | );
22 |
23 | $result = runRule('ListRecords/no-dc-title');
24 | $this->assertEquals(
25 | 'dc:title is missing in record-1',
26 | getIssueText($result->issues[0]),
27 | );
28 |
29 | $result = runRule('ListRecords/good');
30 | $this->assertEquals(0, $result->issuesCount);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/api/routes/RouteIssues.php:
--------------------------------------------------------------------------------
1 | sanitizeTaskId($_GET['taskId'] ?? '');
12 | $ruleId = $this->sanitizeRuleId($_GET['ruleId'] ?? '');
13 | $n = (int) ($_GET['n'] ?? 1);
14 |
15 | $cacheDir = Config::$cacheDir . "/$taskId";
16 |
17 | if (! $taskId || ! is_dir($cacheDir)) {
18 | $this->fail('Unknown task ID', 404);
19 | }
20 |
21 | $issuesFile = "$cacheDir/issues/$ruleId/$n.json";
22 |
23 | if (! file_exists($issuesFile)) {
24 | $this->fail('Unknown issues file', 400);
25 | }
26 |
27 | $issuesFile = "$cacheDir/issues/$ruleId/$n.json";
28 | $content = file_get_contents($issuesFile);
29 |
30 | if ($content === false) {
31 | $this->fail('Error reading issues', 500);
32 | }
33 |
34 | echo $content;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/api/rules/M_11_1_1Test.php:
--------------------------------------------------------------------------------
1 | assertEquals(
13 | 'metadata is missing in record-1',
14 | getIssueText($result->issues[0]),
15 | );
16 |
17 | $result = runRule('ListRecords/no-oaidc');
18 | $this->assertEquals(
19 | 'oai_dc is missing in missing-oaidc',
20 | getIssueText($result->issues[0]),
21 | );
22 |
23 | $result = runRule('ListRecords/no-dc-creator');
24 | $this->assertEquals(
25 | 'dc:creator is missing in record-1',
26 | getIssueText($result->issues[0]),
27 | );
28 |
29 | $result = runRule('ListRecords/good');
30 | $this->assertEquals(0, $result->issuesCount);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/api/rules/M_11_5Test.php:
--------------------------------------------------------------------------------
1 | assertEquals(
13 | 'metadata is missing in record-1',
14 | getIssueText($result->issues[0]),
15 | );
16 |
17 | $result = runRule('ListRecords/no-oaidc');
18 | $this->assertEquals(
19 | 'oai_dc is missing in missing-oaidc',
20 | getIssueText($result->issues[0]),
21 | );
22 |
23 | $result = runRule('ListRecords/no-dc-type');
24 | $this->assertEquals(
25 | 'No dc:type with valid doc-type in record-1',
26 | getIssueText($result->issues[0]),
27 | );
28 |
29 | $result = runRule('ListRecords/good');
30 | $this->assertEquals(0, $result->issuesCount);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/api/rules/M_11_1_5Test.php:
--------------------------------------------------------------------------------
1 | assertEquals(
13 | 'metadata is missing in record-1',
14 | getIssueText($result->issues[0]),
15 | );
16 |
17 | $result = runRule('ListRecords/no-oaidc');
18 | $this->assertEquals(
19 | 'oai_dc is missing in missing-oaidc',
20 | getIssueText($result->issues[0]),
21 | );
22 |
23 | $result = runRule('ListRecords/no-dc-identifier');
24 | $this->assertEquals(
25 | 'dc:identifier is missing in record-1',
26 | getIssueText($result->issues[0]),
27 | );
28 |
29 | $result = runRule('ListRecords/good');
30 | $this->assertEquals(0, $result->issuesCount);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/api/rules/M_11_6Test.php:
--------------------------------------------------------------------------------
1 | assertEquals(
13 | 'metadata is missing in record-1',
14 | getIssueText($result->issues[0]),
15 | );
16 |
17 | $result = runRule('ListRecords/no-oaidc');
18 | $this->assertEquals(
19 | 'oai_dc is missing in missing-oaidc',
20 | getIssueText($result->issues[0]),
21 | );
22 |
23 | $result = runRule('ListRecords/no-dc-subject-ddc');
24 | $this->assertEquals(
25 | 'No dc:subject with valid DDC in record-1',
26 | getIssueText($result->issues[0]),
27 | );
28 |
29 | $result = runRule('ListRecords/good');
30 | $this->assertEquals(0, $result->issuesCount);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/api/rules/E_11_8Test.php:
--------------------------------------------------------------------------------
1 | assertEquals(
13 | 'metadata is missing in record-1',
14 | getIssueText($result->issues[0]),
15 | );
16 |
17 | $result = runRule('ListRecords/no-oaidc');
18 | $this->assertEquals(
19 | 'oai_dc is missing in missing-oaidc',
20 | getIssueText($result->issues[0]),
21 | );
22 |
23 | $result = runRule('ListRecords/no-dc-type-coar');
24 | $this->assertEquals(
25 | 'No dc:type with valid COAR Resource Type in record-1',
26 | getIssueText($result->issues[0]),
27 | );
28 |
29 | $result = runRule('ListRecords/good');
30 | $this->assertEquals(0, $result->issuesCount);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/api/rules/M_11_3Test.php:
--------------------------------------------------------------------------------
1 | assertEquals(
13 | 'metadata is missing in record-1',
14 | getIssueText($result->issues[0]),
15 | );
16 |
17 | $result = runRule('ListRecords/no-oaidc');
18 | $this->assertEquals(
19 | 'oai_dc is missing in missing-oaidc',
20 | getIssueText($result->issues[0]),
21 | );
22 |
23 | $result = runRule('ListRecords/invalid-dc-identifier');
24 | $this->assertEquals(
25 | 'dc:identifier with operable URL missing in invalid-dc-identifier.',
26 | getIssueText($result->issues[0]),
27 | );
28 |
29 | $result = runRule('ListRecords/good');
30 | $this->assertEquals(0, $result->issuesCount);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/api/rules/M_9_3_1.php:
--------------------------------------------------------------------------------
1 | ownerDocument;
15 |
16 | // @codeCoverageIgnoreStart
17 | if (! $dom) {
18 | $this->addFatalIssue('Invalid XML for $1', 'Identify');
19 |
20 | return;
21 | }
22 | // @codeCoverageIgnoreEnd
23 |
24 | $dom->schemaValidate(Config::$dataDir . '/schemas/OAI-PMH.xsd');
25 | $xmlErrors = libxml_get_errors();
26 | libxml_clear_errors();
27 | $errorHtml = $this->xmlErrorsToHtml($xmlErrors);
28 |
29 | if ($errorHtml) {
30 | $this->addIssue(
31 | 'Identify',
32 | 'Schema validation errors in $1: $2',
33 | 'Identify',
34 | $errorHtml,
35 | );
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 | $1 is missing in $2',
35 | 'oai_datacite',
36 | 'ListMetadataFormats',
37 | );
38 |
39 | return;
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/api/bootstrap.php:
--------------------------------------------------------------------------------
1 | $issue->values[(int) $matches[1] - 1],
13 | $issue->text,
14 | );
15 | }
16 |
17 | /**
18 | * Run a rule isolated for unit tests.
19 | *
20 | * @param mixed[] $overriddenProperties
21 | */
22 | function runRule(string $filename, array $overriddenProperties = []): object
23 | {
24 | $backtrace = debug_backtrace();
25 | $testClassName = $backtrace[1]['class'];
26 | $testedClassName = str_replace('Test', '', $testClassName);
27 |
28 | $validator = new \Dini\Validator\Validator('http://localhost:8002');
29 | $rule = new $testedClassName('', 0, $validator);
30 | $rule->setup();
31 |
32 | foreach ($overriddenProperties as $key => $value) {
33 | $rule->{$key} = $value;
34 | }
35 |
36 | $content = file_get_contents(__DIR__ . "/xml/$filename.xml");
37 | $xml = simplexml_load_string($content);
38 |
39 | $rule->run($xml);
40 |
41 | return $rule->getResult(true);
42 | }
43 |
--------------------------------------------------------------------------------
/api/rules/M_11_8Test.php:
--------------------------------------------------------------------------------
1 | assertEquals(
13 | 'metadata is missing in record-1',
14 | getIssueText($result->issues[0]),
15 | );
16 |
17 | $result = runRule('ListRecords/no-oaidc');
18 | $this->assertEquals(
19 | 'oai_dc is missing in missing-oaidc',
20 | getIssueText($result->issues[0]),
21 | );
22 |
23 | $result = runRule('ListRecords/no-dc-date');
24 | $this->assertEquals(
25 | 'dc:date is missing in record-1',
26 | getIssueText($result->issues[0]),
27 | );
28 |
29 | $result = runRule('ListRecords/invalid-dc-date');
30 | $this->assertEquals(
31 | 'dc:date is invalid in record-1',
32 | getIssueText($result->issues[0]),
33 | );
34 |
35 | $result = runRule('ListRecords/good');
36 | $this->assertEquals(0, $result->issuesCount);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/api/test/xml/ListMetadataFormats/good.xml:
--------------------------------------------------------------------------------
1 |
2 | metadata is missing in record-1',
14 | getIssueText($result->issues[0]),
15 | );
16 |
17 | $result = runRule('ListRecords/no-oaidc');
18 | $this->assertEquals(
19 | 'oai_dc is missing in missing-oaidc',
20 | getIssueText($result->issues[0]),
21 | );
22 |
23 | $result = runRule('ListRecords/no-dc-date');
24 | $this->assertEquals(
25 | 'dc:date is missing in record-1',
26 | getIssueText($result->issues[0]),
27 | );
28 |
29 | $result = runRule('ListRecords/duplicate-dc-date');
30 | $this->assertEquals(
31 | 'dc:date is used multiple times in record-1',
32 | getIssueText($result->issues[0]),
33 | );
34 |
35 | $result = runRule('ListRecords/good');
36 | $this->assertEquals(0, $result->issuesCount);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/api/rules/M_11_7Test.php:
--------------------------------------------------------------------------------
1 | assertEquals(
13 | 'metadata is missing in record-1',
14 | getIssueText($result->issues[0]),
15 | );
16 |
17 | $result = runRule('ListRecords/no-oaidc');
18 | $this->assertEquals(
19 | 'oai_dc is missing in missing-oaidc',
20 | getIssueText($result->issues[0]),
21 | );
22 |
23 | $result = runRule('ListRecords/no-dc-language');
24 | $this->assertEquals(
25 | 'dc:language is missing in record-1',
26 | getIssueText($result->issues[0]),
27 | );
28 |
29 | $result = runRule('ListRecords/invalid-dc-language');
30 | $this->assertEquals(
31 | 'dc:language is invalid in record-1',
32 | getIssueText($result->issues[0]),
33 | );
34 |
35 | $result = runRule('ListRecords/good');
36 | $this->assertEquals(0, $result->issuesCount);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/api/rules/E_11_9Test.php:
--------------------------------------------------------------------------------
1 | assertEquals(
13 | 'metadata is missing in record-1',
14 | getIssueText($result->issues[0]),
15 | );
16 |
17 | $result = runRule('ListRecords/no-oaidc');
18 | $this->assertEquals(
19 | 'oai_dc is missing in missing-oaidc',
20 | getIssueText($result->issues[0]),
21 | );
22 |
23 | $result = runRule('ListRecords/no-dc-creator');
24 | $this->assertEquals(
25 | 'dc:creator is missing in record-1',
26 | getIssueText($result->issues[0]),
27 | );
28 |
29 | $result = runRule('ListRecords/no-dc-creator-orcid');
30 | $this->assertEquals(
31 | 'No dc:creator with valid ORCID iD in record-1',
32 | getIssueText($result->issues[0]),
33 | );
34 |
35 | $result = runRule('ListRecords/good');
36 | $this->assertEquals(0, $result->issuesCount);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/api/test/xml/ListIdentifiers/good.xml:
--------------------------------------------------------------------------------
1 |
2 | $1 is missing in $2',
17 | 'metadata',
18 | (string) $record->header->identifier,
19 | );
20 |
21 | return;
22 | }
23 |
24 | $oaiDc = $record->metadata->children('oai_dc', true);
25 |
26 | if (! $oaiDc) {
27 | $this->addIssue(
28 | "GetRecord&identifier={$record->header->identifier}",
29 | '$1 is missing in $2',
30 | 'oai_dc',
31 | (string) $record->header->identifier,
32 | );
33 | } elseif (! trim((string) $oaiDc->children('dc', true)->date)) {
34 | $this->addIssue(
35 | "GetRecord&identifier={$record->header->identifier}",
36 | '$1 is missing in $2',
37 | 'dc:date',
38 | (string) $record->header->identifier,
39 | );
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/api/rules/M_11_1_4.php:
--------------------------------------------------------------------------------
1 | metadata) {
14 | $this->addIssue(
15 | "GetRecord&identifier={$record->header->identifier}",
16 | '$1 is missing in $2',
17 | 'metadata',
18 | (string) $record->header->identifier,
19 | );
20 |
21 | return;
22 | }
23 |
24 | $oaiDc = $record->metadata->children('oai_dc', true);
25 |
26 | if (! $oaiDc) {
27 | $this->addIssue(
28 | "GetRecord&identifier={$record->header->identifier}",
29 | '$1 is missing in $2',
30 | 'oai_dc',
31 | (string) $record->header->identifier,
32 | );
33 | } elseif (! trim((string) $oaiDc->children('dc', true)->type)) {
34 | $this->addIssue(
35 | "GetRecord&identifier={$record->header->identifier}",
36 | '$1 is missing in $2',
37 | 'dc:type',
38 | (string) $record->header->identifier,
39 | );
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/api/test/xml/Identify/invalid-deletedrecord.xml:
--------------------------------------------------------------------------------
1 |
2 | $1 is missing in $2',
17 | 'metadata',
18 | (string) $record->header->identifier,
19 | );
20 |
21 | return;
22 | }
23 |
24 | $oaiDc = $record->metadata->children('oai_dc', true);
25 |
26 | if (! $oaiDc) {
27 | $this->addIssue(
28 | "GetRecord&identifier={$record->header->identifier}",
29 | '$1 is missing in $2',
30 | 'oai_dc',
31 | (string) $record->header->identifier,
32 | );
33 | } elseif (! trim((string) $oaiDc->children('dc', true)->title)) {
34 | $this->addIssue(
35 | "GetRecord&identifier={$record->header->identifier}",
36 | '$1 is missing in $2',
37 | 'dc:title',
38 | (string) $record->header->identifier,
39 | );
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/api/rules/M_11_1_1.php:
--------------------------------------------------------------------------------
1 | metadata) {
14 | $this->addIssue(
15 | "GetRecord&identifier={$record->header->identifier}",
16 | '$1 is missing in $2',
17 | 'metadata',
18 | (string) $record->header->identifier,
19 | );
20 |
21 | return;
22 | }
23 |
24 | $oaiDc = $record->metadata->children('oai_dc', true);
25 |
26 | if (! $oaiDc) {
27 | $this->addIssue(
28 | "GetRecord&identifier={$record->header->identifier}",
29 | '$1 is missing in $2',
30 | 'oai_dc',
31 | (string) $record->header->identifier,
32 | );
33 | } elseif (! trim((string) $oaiDc->children('dc', true)->creator)) {
34 | $this->addIssue(
35 | "GetRecord&identifier={$record->header->identifier}",
36 | '$1 is missing in $2',
37 | 'dc:creator',
38 | (string) $record->header->identifier,
39 | );
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/api/rules/M_11_1_5.php:
--------------------------------------------------------------------------------
1 | metadata) {
14 | $this->addIssue(
15 | "GetRecord&identifier={$record->header->identifier}",
16 | '$1 is missing in $2',
17 | 'metadata',
18 | (string) $record->header->identifier,
19 | );
20 |
21 | return;
22 | }
23 |
24 | $oaiDc = $record->metadata->children('oai_dc', true);
25 |
26 | if (! $oaiDc) {
27 | $this->addIssue(
28 | "GetRecord&identifier={$record->header->identifier}",
29 | '$1 is missing in $2',
30 | 'oai_dc',
31 | (string) $record->header->identifier,
32 | );
33 | } elseif (! trim((string) $oaiDc->children('dc', true)->identifier)) {
34 | $this->addIssue(
35 | "GetRecord&identifier={$record->header->identifier}",
36 | '$1 is missing in $2',
37 | 'dc:identifier',
38 | (string) $record->header->identifier,
39 | );
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/api/test/mock-oai-api/index.php:
--------------------------------------------------------------------------------
1 | Invalid verb';
27 | exit(400);
28 | }
29 |
30 | $xmlDir = __DIR__ . "/../xml/$verb" . ($metadataPrefix === 'oai_datacite' ? '-datacite' : '');
31 |
32 | if ($identifier) {
33 | $file = "$xmlDir/$identifier.xml";
34 |
35 | if (file_exists($file)) {
36 | echo file_get_contents($file);
37 | } else {
38 | http_response_code(404);
39 | }
40 | } elseif (! $resumptionToken) {
41 | echo file_get_contents("$xmlDir/good.xml");
42 | } else {
43 | echo file_get_contents("$xmlDir/good-$resumptionToken.xml");
44 | }
45 |
--------------------------------------------------------------------------------
/api/rules/E_11_10.php:
--------------------------------------------------------------------------------
1 | metadata) {
14 | $this->addIssue(
15 | "GetRecord&identifier={$record->header->identifier}",
16 | '$1 is missing in $2',
17 | 'metadata',
18 | (string) $record->header->identifier,
19 | );
20 |
21 | return;
22 | }
23 |
24 | $oaiDc = $record->metadata->children('oai_dc', true);
25 |
26 | if (! $oaiDc) {
27 | $this->addIssue(
28 | "GetRecord&identifier={$record->header->identifier}",
29 | '$1 is missing in $2',
30 | 'oai_dc',
31 | (string) $record->header->identifier,
32 | );
33 |
34 | return;
35 | }
36 |
37 | if (! $oaiDc->children('dc', true)->rights) {
38 | $this->addIssue(
39 | "GetRecord&identifier={$record->header->identifier}",
40 | '$1 is missing in $2',
41 | 'dc:rights',
42 | (string) $record->header->identifier,
43 | );
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/api/test/xml/ListRecords/no-dc-rights.xml:
--------------------------------------------------------------------------------
1 |
2 | $1 is missing in $2',
17 | 'metadata',
18 | (string) $record->header->identifier,
19 | );
20 |
21 | return;
22 | }
23 |
24 | $oaiDc = $record->metadata->children('oai_dc', true);
25 |
26 | if (! $oaiDc) {
27 | $this->addIssue(
28 | "GetRecord&identifier={$record->header->identifier}",
29 | '$1 is missing in $2',
30 | 'oai_dc',
31 | (string) $record->header->identifier,
32 | );
33 |
34 | return;
35 | }
36 |
37 | foreach ($oaiDc->children('dc', true)->identifier as $dcIdentifier) {
38 | if (filter_var($dcIdentifier, FILTER_VALIDATE_URL)) {
39 | return;
40 | }
41 | }
42 |
43 | $this->addIssue(
44 | "GetRecord&identifier={$record->header->identifier}",
45 | 'dc:identifier with operable URL missing in $1.',
46 | (string) $record->header->identifier,
47 | );
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/api/test/xml/ListRecords/no-dc-date.xml:
--------------------------------------------------------------------------------
1 |
2 | $1 is missing in $2',
16 | $result->issues[0]->text,
17 | );
18 | $this->assertEquals(
19 | 'datestamp',
20 | $result->issues[0]->values[0],
21 | );
22 | $this->assertEquals(
23 | 'oai:1',
24 | $result->issues[0]->values[1],
25 | );
26 |
27 | $this->assertEquals(
28 | '$1 is invalid in $2',
29 | $result->issues[1]->text,
30 | );
31 | $this->assertEquals(
32 | 'datestamp',
33 | $result->issues[1]->values[0],
34 | );
35 | $this->assertEquals(
36 | 'oai:2',
37 | $result->issues[1]->values[1],
38 | );
39 |
40 | $this->assertEquals(
41 | '$1 is invalid in $2',
42 | $result->issues[2]->text,
43 | );
44 | $this->assertEquals(
45 | 'datestamp',
46 | $result->issues[2]->values[0],
47 | );
48 | $this->assertEquals(
49 | 'oai:3',
50 | $result->issues[2]->values[1],
51 | );
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/api/test/xml/GetRecord/record-2.xml:
--------------------------------------------------------------------------------
1 |
2 | $1 is missing in $2',
17 | 'metadata',
18 | (string) $record->header->identifier,
19 | );
20 |
21 | return;
22 | }
23 |
24 | $oaiDc = $record->metadata->children('oai_dc', true);
25 |
26 | if (! $oaiDc) {
27 | $this->addIssue(
28 | "GetRecord&identifier={$record->header->identifier}",
29 | '$1 is missing in $2',
30 | 'oai_dc',
31 | (string) $record->header->identifier,
32 | );
33 |
34 | return;
35 | }
36 |
37 | foreach ($oaiDc->children('dc', true)->subject as $dcSubject) {
38 | if (preg_match('/^ddc:\d{3}(\.\d+)?$/', (string) $dcSubject)) {
39 | return;
40 | }
41 | }
42 |
43 | $this->addIssue(
44 | "GetRecord&identifier={$record->header->identifier}",
45 | 'No $1 with valid $2 in $3',
46 | 'dc:subject',
47 | 'DDC',
48 | (string) $record->header->identifier,
49 | );
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/api/test/xml/ListRecords/no-dc-type-coar.xml:
--------------------------------------------------------------------------------
1 |
2 | adminEmail in Identify does not contain a valid email address.',
16 | $result->issues[0]->text,
17 | );
18 |
19 | $result = runRule('Identify/invalid-admin-email');
20 | $this->assertEquals(1, $result->issuesCount);
21 | $this->assertEquals(
22 | 'adminEmail in Identify does not contain a valid email address.',
23 | $result->issues[0]->text,
24 | );
25 |
26 | $result = runRule('Identify/no-description');
27 | $this->assertEquals(1, $result->issuesCount);
28 | $this->assertEquals(
29 | '$1 is missing in $2',
30 | $result->issues[0]->text,
31 | );
32 | $this->assertEquals('description', $result->issues[0]->values[0]);
33 | $this->assertEquals('Identify', $result->issues[0]->values[1]);
34 |
35 | $result = runRule('Identify/no-english-description');
36 | $this->assertEquals(1, $result->issuesCount);
37 | $this->assertEquals(
38 | 'Identify does not seem to have a description in English.',
39 | $result->issues[0]->text,
40 | );
41 |
42 | $result = runRule('Identify/good');
43 | $this->assertEquals(0, $result->issuesCount);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/doc/api.md:
--------------------------------------------------------------------------------
1 | # API
2 |
3 | The backend is a JSON REST API which supplies data to the frontend.
4 |
5 | To update progress during a validation with minimal overhead, the `watch` route uses [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) with JSON data instead plain JSON.
6 |
7 | The API is English-only, translations are entirely done in the frontend.
8 |
9 | ## Directory Structure
10 |
11 | Base directory: `api`
12 |
13 | - `classes`
14 | The validator’s core classes.
15 | - `config`
16 | Global backend configuration.
17 | - `routes`
18 | Classes for available API routes. Each file corresponds to an API route.
19 | - `rules`
20 | Classes for all rules defined in the current ruleset that are validated programmatically.
21 | - `test`
22 | Additional files for running unit tests, including a mock OAI-PMH API.
23 |
24 | ## Validation Run
25 |
26 | For every validation, these steps are executed:
27 |
28 | 1. An `Identify` request is sent to the supplied OAI API URL. If the response is valid, the XML is saved. Otherwise the validation is cancelled.
29 | 2. If the first response was valid, requests are sent for each of the OAI verbs `ListIdentifiers`, `ListMetadataFormats`, `ListRecords`, and `ListSets`. The responses are saved as XML files. If any of these verbs have a `resumptionToken`, subsequent requests are sent and saved as XML, with the exception of `ListRecords`, where only so many batches are downloaded until the user-set record limit is fulfilled. The progress is displayed, so are errors, but the validation is not aborted.
30 | 3. For each downloaded file, all applicable rules are run, see [rulesets](./rulesets.md).
31 | 4. Once all files have been checked, the result is displayed.
32 |
--------------------------------------------------------------------------------
/api/rules/E_10_3Test.php:
--------------------------------------------------------------------------------
1 | assertEquals(0, $result->issuesCount);
13 |
14 | $result = runRule('ListRecords/no-responsedate');
15 | $this->assertEquals(
16 | 'responseDate is missing in ListRecords',
17 | getIssueText($result->issues[0]),
18 | );
19 |
20 | $result = runRule('ListRecords/invalid-responsedate');
21 | $this->assertEquals(
22 | 'responseDate is invalid in ListRecords',
23 | getIssueText($result->issues[0]),
24 | );
25 |
26 | $result = runRule('ListRecords/no-expirationdate');
27 | $this->assertEquals(
28 | 'resumptionToken without expirationDate in ListRecords',
29 | getIssueText($result->issues[0]),
30 | );
31 |
32 | $result = runRule('ListRecords/invalid-expirationdate');
33 | $this->assertEquals(
34 | 'resumptionToken with invalid expirationDate in ListRecords',
35 | getIssueText($result->issues[0]),
36 | );
37 |
38 | $result = runRule('ListRecords/expiring-too-soon');
39 | $this->assertEquals(
40 | 'The lifespan of the resumptionToken in ListRecords is 3 h.',
41 | getIssueText($result->issues[0]),
42 | );
43 |
44 | $result = runRule('ListRecords/fake-100');
45 | $this->assertEquals(0, $result->issuesCount);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/api/rules/M_11_5.php:
--------------------------------------------------------------------------------
1 | docTypes = json_decode($json);
17 | }
18 |
19 | public function checkRecord($record): void
20 | {
21 | if (! $record->metadata) {
22 | $this->addIssue(
23 | "GetRecord&identifier={$record->header->identifier}",
24 | '$1 is missing in $2',
25 | 'metadata',
26 | (string) $record->header->identifier,
27 | );
28 |
29 | return;
30 | }
31 |
32 | $oaiDc = $record->metadata->children('oai_dc', true);
33 |
34 | if (! $oaiDc) {
35 | $this->addIssue(
36 | "GetRecord&identifier={$record->header->identifier}",
37 | '$1 is missing in $2',
38 | 'oai_dc',
39 | (string) $record->header->identifier,
40 | );
41 |
42 | return;
43 | }
44 |
45 | foreach ($oaiDc->children('dc', true)->type as $dcType) {
46 | if (in_array($dcType, $this->docTypes)) {
47 | return;
48 | }
49 | }
50 |
51 | $this->addIssue(
52 | "GetRecord&identifier={$record->header->identifier}",
53 | 'No $1 with valid $2 in $3',
54 | 'dc:type',
55 | 'doc-type',
56 | (string) $record->header->identifier,
57 | );
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/api/test/xml/ListRecords/no-datacite.xml:
--------------------------------------------------------------------------------
1 |
2 | adminEmail in Identify does not contain a valid email address.',
24 | );
25 |
26 | return;
27 | }
28 |
29 | if (! $xml->Identify->description) {
30 | $this->addIssue(
31 | 'Identify',
32 | '$1 is missing in $2',
33 | 'description',
34 | 'Identify',
35 | );
36 |
37 | return;
38 | }
39 |
40 | foreach ($xml->Identify->description as $description) {
41 | // NOTE: description can contain XML, see
42 | // https://www.openarchives.org/OAI/openarchivesprotocol.html#Identify
43 | $text = @dom_import_simplexml($description)->textContent;
44 |
45 | if ($text) {
46 | $language = (string) $this->languageDetector->detect($text);
47 |
48 | if ($language === 'en') {
49 | return;
50 | }
51 | }
52 | }
53 |
54 | $this->addIssue(
55 | 'Identify',
56 | 'Identify does not seem to have a description in English.',
57 | );
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/api/test/xml/ListRecords/invalid-dc-language.xml:
--------------------------------------------------------------------------------
1 |
2 | $1 is missing in $2',
17 | 'metadata',
18 | (string) $record->header->identifier,
19 | );
20 |
21 | return;
22 | }
23 |
24 | $oaiDc = $record->metadata->children('oai_dc', true);
25 |
26 | if (! $oaiDc) {
27 | $this->addIssue(
28 | "GetRecord&identifier={$record->header->identifier}",
29 | '$1 is missing in $2',
30 | 'oai_dc',
31 | (string) $record->header->identifier,
32 | );
33 |
34 | return;
35 | }
36 |
37 | $dates = $oaiDc->children('dc', true)->date;
38 |
39 | if (! $dates) {
40 | $this->addIssue(
41 | "GetRecord&identifier={$record->header->identifier}",
42 | '$1 is missing in $2',
43 | 'dc:date',
44 | (string) $record->header->identifier,
45 | );
46 |
47 | return;
48 | }
49 |
50 | if (count($dates) > 1) {
51 | $this->addIssue(
52 | "GetRecord&identifier={$record->header->identifier}",
53 | '$1 is used multiple times in $2',
54 | 'dc:date',
55 | (string) $record->header->identifier,
56 | );
57 |
58 | return;
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/api/rules/M_9_3_4.php:
--------------------------------------------------------------------------------
1 | validator->downloadOaiXml('GetRecord', '', $this->unknownRecordId);
19 |
20 | if (! $result || ! $result->content) {
21 | $this->addFatalIssue(
22 | "GetRecord&identifier=$this->unknownRecordId",
23 | 'Invalid response to $1',
24 | 'GetRecord',
25 | );
26 |
27 | return;
28 | }
29 |
30 | $dom = new DOMDocument();
31 | $domCreated = @$dom->loadXML($result->content);
32 |
33 | // @codeCoverageIgnoreStart
34 | if (! $domCreated) {
35 | $this->addFatalIssue(
36 | "GetRecord&identifier=$this->unknownRecordId",
37 | 'Invalid response to $1',
38 | 'GetRecord',
39 | );
40 |
41 | return;
42 | }
43 | // @codeCoverageIgnoreEnd
44 |
45 | $dom->schemaValidate(Config::$dataDir . '/schemas/OAI-PMH.xsd');
46 | $xmlErrors = libxml_get_errors();
47 | libxml_clear_errors();
48 | $errorHtml = $this->xmlErrorsToHtml($xmlErrors);
49 |
50 | if ($errorHtml) {
51 | $this->addIssue(
52 | "GetRecord&identifier=$this->unknownRecordId",
53 | 'Schema validation errors in $1:$1 is missing in $2',
17 | 'metadata',
18 | (string) $record->header->identifier,
19 | );
20 |
21 | return;
22 | }
23 |
24 | $oaiDc = $record->metadata->children('oai_dc', true);
25 |
26 | if (! $oaiDc) {
27 | $this->addIssue(
28 | "GetRecord&identifier={$record->header->identifier}",
29 | '$1 is missing in $2',
30 | 'oai_dc',
31 | (string) $record->header->identifier,
32 | );
33 |
34 | return;
35 | }
36 |
37 | $date = (string) $oaiDc->children('dc', true)->date;
38 |
39 | if (! $date) {
40 | $this->addIssue(
41 | "GetRecord&identifier={$record->header->identifier}",
42 | '$1 is missing in $2',
43 | 'dc:date',
44 | (string) $record->header->identifier,
45 | );
46 |
47 | return;
48 | }
49 |
50 | $parts = explode('-', $date);
51 |
52 | if (! checkdate((int) ($parts[1] ?? 1), (int) ($parts[2] ?? 1), (int) ($parts[0]))) {
53 | $this->addIssue(
54 | "GetRecord&identifier={$record->header->identifier}",
55 | '$1 is invalid in $2',
56 | 'dc:date',
57 | (string) $record->header->identifier,
58 | );
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/api/rules/M_10_5Test.php:
--------------------------------------------------------------------------------
1 | assertEquals(0, $result->issuesCount);
13 |
14 | $result = runRule(
15 | 'ListRecords/good',
16 | ['secondListRecordsFile' => ''],
17 | );
18 | $this->assertEquals(
19 | 'XML file not found',
20 | getIssueText($result->issues[0]),
21 | );
22 |
23 | $result = runRule(
24 | 'ListRecords/good',
25 | ['savedResultFile' => __DIR__ . '/../test/xml/ListRecords/invalid.xml'],
26 | );
27 | $this->assertEquals(
28 | 'Invalid response to ListRecords (1)',
29 | getIssueText($result->issues[0]),
30 | );
31 |
32 | $result = runRule(
33 | 'ListRecords/bad-resumptiontoken',
34 | ['savedResultFile' => __DIR__ . '/../test/xml/ListRecords/bad-resumptiontoken.xml'],
35 | );
36 | $this->assertEquals(
37 | 'Invalid response to ListRecords (2)',
38 | getIssueText($result->issues[0]),
39 | );
40 |
41 | $result = runRule(
42 | 'ListRecords/unstable-resumptiontoken',
43 | ['savedResultFile' => __DIR__ . '/../test/xml/ListRecords/unstable-resumptiontoken.xml'],
44 | );
45 | $this->assertEquals(
46 | 'Consecutive ListRecords requests do not match.',
47 | getIssueText($result->issues[0]),
48 | );
49 |
50 | $result = runRule(
51 | 'ListRecords/good',
52 | ['savedResultFile' => __DIR__ . '/../test/xml/ListRecords/good-2.xml'],
53 | );
54 | $this->assertEquals(0, $result->issuesCount);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/api/rules/E_11_8.php:
--------------------------------------------------------------------------------
1 | coarResourceTypes = json_decode($json);
17 |
18 | foreach ($this->coarResourceTypes as &$type) {
19 | $type = "http://purl.org/coar/resource_type/$type";
20 | }
21 | }
22 |
23 | public function checkRecord($record): void
24 | {
25 | if (! $record->metadata) {
26 | $this->addIssue(
27 | "GetRecord&identifier={$record->header->identifier}",
28 | '$1 is missing in $2',
29 | 'metadata',
30 | (string) $record->header->identifier,
31 | );
32 |
33 | return;
34 | }
35 |
36 | $oaiDc = $record->metadata->children('oai_dc', true);
37 |
38 | if (! $oaiDc) {
39 | $this->addIssue(
40 | "GetRecord&identifier={$record->header->identifier}",
41 | '$1 is missing in $2',
42 | 'oai_dc',
43 | (string) $record->header->identifier,
44 | );
45 | } else {
46 | foreach ($oaiDc->children('dc', true)->type as $dcType) {
47 | if (in_array($dcType, $this->coarResourceTypes)) {
48 | return;
49 | }
50 | }
51 |
52 | $this->addIssue(
53 | "GetRecord&identifier={$record->header->identifier}",
54 | 'No $1 with valid $2 in $3',
55 | 'dc:type',
56 | 'COAR Resource Type',
57 | (string) $record->header->identifier,
58 | );
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/frontend/modules/http.js:
--------------------------------------------------------------------------------
1 | import { ref, onUnmounted } from 'vue'
2 |
3 | import { cache } from './cache'
4 | import { errorHandler } from './error'
5 |
6 | export const loading = ref(false)
7 |
8 | export function createEventSource (url) {
9 | const eventSource = new EventSource(url)
10 |
11 | loading.value = true
12 |
13 | eventSource.addEventListener('message', async () => {
14 | loading.value = false
15 | })
16 |
17 | eventSource.addEventListener('error', () => {
18 | eventSource.close()
19 | loading.value = false
20 | errorHandler.set('Server error')
21 | })
22 |
23 | // Without this, EventSource throws an error on reload
24 | window.addEventListener('beforeunload', closeEventSource)
25 |
26 | onUnmounted(() => {
27 | eventSource.close()
28 | loading.value = false
29 | window.removeEventListener('beforeunload', closeEventSource)
30 | })
31 |
32 | function closeEventSource () {
33 | eventSource.close()
34 | }
35 |
36 | return eventSource
37 | }
38 |
39 | export async function fetchJson (url, options = {}, useCache = true) {
40 | const cacheKey = url + JSON.stringify(options)
41 |
42 | if (useCache && cache.get(cacheKey)) {
43 | return cache.get(cacheKey)
44 | }
45 |
46 | loading.value = true
47 |
48 | const response = await fetch(url, options).catch(() => {
49 | errorHandler.set('Network error')
50 | })
51 |
52 | loading.value = false
53 |
54 | if (response?.ok) {
55 | try {
56 | const content = await response.json()
57 |
58 | if (useCache) {
59 | cache.set(cacheKey, content)
60 | }
61 |
62 | return content
63 | } catch {
64 | errorHandler.set('Received invalid JSON')
65 |
66 | return false
67 | }
68 | } else if (response?.body) {
69 | try {
70 | const error = await response.json()
71 | errorHandler.set(error.error || error)
72 | } catch {
73 | errorHandler.set('Server error')
74 | }
75 | }
76 |
77 | return false
78 | }
79 |
--------------------------------------------------------------------------------
/api/rules/M_9_3_3.php:
--------------------------------------------------------------------------------
1 | ListRecords->record[0]?->header?->identifier ?? null;
16 |
17 | if (! $recordId) {
18 | $this->addFatalIssue(
19 | 'ListRecords',
20 | 'Invalid response to $1',
21 | 'ListRecords',
22 | );
23 |
24 | return;
25 | }
26 |
27 | $result = $this->validator->downloadOaiXml('GetRecord', '', $recordId);
28 |
29 | if (! $result || ! $result->content) {
30 | $this->addFatalIssue(
31 | "GetRecord&identifier=$recordId",
32 | 'Invalid response to $1',
33 | $recordId,
34 | );
35 |
36 | return;
37 | }
38 |
39 | $dom = new DOMDocument();
40 | $domCreated = @$dom->loadXML($result->content);
41 |
42 | // @codeCoverageIgnoreStart
43 | if (! $domCreated) {
44 | $this->addFatalIssue(
45 | "GetRecord&identifier=$recordId",
46 | 'Invalid response to $1',
47 | $recordId,
48 | );
49 |
50 | return;
51 | }
52 | // @codeCoverageIgnoreEnd
53 |
54 | $dom->schemaValidate(Config::$dataDir . '/schemas/OAI-PMH.xsd');
55 | $xmlErrors = libxml_get_errors();
56 | libxml_clear_errors();
57 | $errorHtml = $this->xmlErrorsToHtml($xmlErrors);
58 |
59 | if ($errorHtml) {
60 | $this->addIssue(
61 | "GetRecord&identifier=$recordId",
62 | 'Schema validation errors in $1:$1 is missing in $2',
17 | 'metadata',
18 | (string) $record->header->identifier,
19 | );
20 |
21 | return;
22 | }
23 |
24 | $oaiDc = $record->metadata->children('oai_dc', true);
25 |
26 | if (! $oaiDc) {
27 | $this->addIssue(
28 | "GetRecord&identifier={$record->header->identifier}",
29 | '$1 is missing in $2',
30 | 'oai_dc',
31 | (string) $record->header->identifier,
32 | );
33 |
34 | return;
35 | }
36 |
37 | $creators = $oaiDc->children('dc', true)->creator;
38 |
39 | if (! $creators) {
40 | $this->addIssue(
41 | "GetRecord&identifier={$record->header->identifier}",
42 | '$1 is missing in $2',
43 | 'dc:creator',
44 | (string) $record->header->identifier,
45 | );
46 |
47 | return;
48 | }
49 |
50 | $creatorsWithOrchidId = array_filter(
51 | (array) $creators,
52 | fn ($creator) => preg_match('#https://orcid.org/\d{4}-\d{4}-\d{4}-\d{3}[0-9X]#', (string) $creator),
53 | );
54 |
55 | if (! $creatorsWithOrchidId) {
56 | $this->addIssue(
57 | "GetRecord&identifier={$record->header->identifier}",
58 | 'No $1 with valid $2 in $3',
59 | 'dc:creator',
60 | 'ORCID iD',
61 | (string) $record->header->identifier,
62 | );
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/frontend/components/VModal.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
30 | section.modal(
31 | :id="id"
32 | aria-modal="true"
33 | :aria-describedby="`${id}-title`"
34 | @click.stop
35 | )
36 | header.header
37 | .header-content(:id="`${id}-title`")
38 | slot(name="header")
39 | button.button.outline(
40 | :aria-label="$t('Close')"
41 | @click="$emit('close')"
42 | )
43 | IconClose
44 | slot
45 |
46 |
47 |
92 |
--------------------------------------------------------------------------------
/frontend/styles/settings.scss:
--------------------------------------------------------------------------------
1 | // Base colors
2 | $blue: #344999;
3 | $green: #68ba80;
4 | $orange: #f0821d;
5 | $red: #d72f89;
6 | $black: #000;
7 | $white: #fff;
8 |
9 | // Shades of gray
10 | $gray-1: #eee;
11 | $gray-2: #ccc;
12 | $gray-3: #888;
13 | $gray-4: #555;
14 | $gray-5: #333;
15 | $gray-6: #222;
16 | $shade-1: var(--shade-1, rgba(#000, .05));
17 | $shade-2: var(--shade-2, rgba(#000, .1));
18 | $shade-3: var(--shade-3, rgba(#000, .2));
19 | $shade-4: var(--shade-4, rgba(#000, .3));
20 | $shine: var(--shine, rgba(#fff, .8));
21 |
22 | // Semantic colors
23 | $bg-color: var(--bg-color, $white);
24 | $bg-muted-color: var(--bg-muted-color, $gray-1);
25 | $border-color: $shade-3;
26 | $button-hover-bg: var(--link-light-color, color.mix(#fff, $blue, 10%));
27 | $border-shade: $shade-2;
28 | $button-shade: var(--button-shade, rgba($blue, 20%));
29 | $error-color: var(--error-color, color.mix(#000, $orange, 30%));
30 | $link-color: var(--link-color, $blue);
31 | $link-shade: var(--link-shade, rgba($blue, 10%));
32 | $text-color: var(--text-color, $black);
33 | $text-muted-color: var(--text-muted-color, $gray-4);
34 | $text-invert-color: var(--text-invert-color, $white);
35 |
36 | // Dimensions
37 | $grid: 24px;
38 | $font-size-small: .882em;
39 |
40 | // Timings
41 | $td: .2s; // transition duration
42 |
43 | // Breakpoints for @media queries
44 | $small: '(min-width: 360px)';
45 | $medium: '(min-width: 576px)';
46 | $large: '(min-width: 1200px)';
47 |
48 | // Combined values
49 | $drop-shadow: 0 0 .5rem rgba(#000, .2);
50 |
51 | .dark {
52 | --shade-1: #{rgba(#fff, .05)};
53 | --shade-2: #{rgba(#fff, .1)};
54 | --shade-3: #{rgba(#fff, .2)};
55 | --shade-4: #{rgba(#fff, .3)};
56 | --shine: #{rgba(#fff, .1)};
57 | --bg-color: #{$gray-6};
58 | --bg-muted-color: #{$gray-5};
59 | --button-shade: #{rgba($green, 20%)};
60 | --error-color: #{$orange};
61 | --link-color: #{$green};
62 | --link-light-color: #{color.mix(#fff, $green, 10%)};
63 | --link-shade: #{rgba($green, 10%)};
64 | --text-color: #{$white};
65 | --text-invert-color: #{$black};
66 | --text-muted-color: #{$gray-2};
67 | }
68 |
--------------------------------------------------------------------------------
/api/rules/M_11_7.php:
--------------------------------------------------------------------------------
1 | languageCodes = json_decode($json);
17 | }
18 |
19 | public function checkRecord($record): void
20 | {
21 | if (! $record->metadata) {
22 | $this->addIssue(
23 | "GetRecord&identifier={$record->header->identifier}",
24 | '$1 is missing in $2',
25 | 'metadata',
26 | (string) $record->header->identifier,
27 | );
28 |
29 | return;
30 | }
31 |
32 | $oaiDc = $record->metadata->children('oai_dc', true);
33 |
34 | if (! $oaiDc) {
35 | $this->addIssue(
36 | "GetRecord&identifier={$record->header->identifier}",
37 | '$1 is missing in $2',
38 | 'oai_dc',
39 | (string) $record->header->identifier,
40 | );
41 |
42 | return;
43 | }
44 |
45 | $languageCode = (string) $oaiDc->children('dc', true)->language;
46 |
47 | if (! $languageCode) {
48 | $this->addIssue(
49 | "GetRecord&identifier={$record->header->identifier}",
50 | '$1 is missing in $2',
51 | 'dc:language',
52 | (string) $record->header->identifier,
53 | );
54 |
55 | return;
56 | }
57 |
58 | if (! in_array($languageCode, $this->languageCodes)) {
59 | $this->addIssue(
60 | "GetRecord&identifier={$record->header->identifier}",
61 | '$1 is invalid in $2',
62 | 'dc:language',
63 | (string) $record->header->identifier,
64 | );
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/frontend/views/ResultView.vue:
--------------------------------------------------------------------------------
1 |
41 |
42 |
43 | TheHeader(
44 | :sidebarOpen="sidebarOpen"
45 | :score="result.score"
46 | @toggleSidebar="() => sidebarOpen = !sidebarOpen"
47 | )
48 | template(#title)
49 | //- h1 overlays sidebar and thus is hidden when sidebar is open
50 | h1(:class="{ 'sr-only': sidebarOpen }" style="font-weight: 300")
51 | span(v-html="$t('Result for $1', removeProtocol(result.oaiApiUrl))")
52 | //- For setting document title from h1
53 | span(hidden) · {{ formatTimestamp(result.timestamp) }}
54 |
55 | main.main
56 | ValidationSidebar#sidebar(
57 | :open="sidebarOpen"
58 | :rules="result.rules"
59 | @close="sidebarOpen = false"
60 | )
61 | ValidationResult(
62 | :result="result"
63 | )
64 |
65 | TheFooter.footer(:decorated="false")
66 |
67 |
68 |
84 |
--------------------------------------------------------------------------------
/api/routes/RouteXml.php:
--------------------------------------------------------------------------------
1 | sanitizeTaskId($_GET['taskId'] ?? '');
12 | $url = @base64_decode($_GET['url'] ?? '');
13 |
14 | $cacheDir = Config::$cacheDir . "/$taskId";
15 |
16 | if (! $taskId || ! is_dir($cacheDir)) {
17 | $this->fail('Invalid request', 400);
18 | }
19 |
20 | if (! $url || ! filter_var($url, FILTER_VALIDATE_URL)) {
21 | $this->fail('Invalid request', 400);
22 | }
23 |
24 | // Only accept requests if we got a finished validation for this OAI URL
25 | $status = $this->readJson("$cacheDir/status.json");
26 |
27 | if (! $status
28 | || $status->stage !== 'finished'
29 | || ! str_starts_with($url, $status->oaiApiUrl)
30 | ) {
31 | $this->fail('Invalid request', 400);
32 | }
33 |
34 | $streamContext = stream_context_create([
35 | 'http' => [
36 | 'header' => 'User-Agent: ' . Config::$userAgent,
37 | 'timeout' => Config::$oaiApiTimeout,
38 | ],
39 | ]);
40 |
41 | $xml = @file_get_contents($url, false, $streamContext);
42 |
43 | if (! $xml) {
44 | $this->fail('Error loading remote XML', 502);
45 | }
46 |
47 | // Prettify XML
48 | $dom = new \DOMDocument();
49 | $dom->preserveWhiteSpace = false;
50 | $dom->formatOutput = true;
51 |
52 | $domCreated = @$dom->loadXML($xml);
53 |
54 | if ($domCreated) {
55 | $prettyXml = $dom->saveXML();
56 | $resumptionToken = $dom->getElementsByTagName('resumptionToken')->item(0)?->textContent;
57 | } else {
58 | // This is probably not actual XML, still return what we got from the server
59 | $prettyXml = $xml;
60 | }
61 |
62 | $this->sendJson([
63 | 'xml' => $prettyXml,
64 | 'resumptionToken' => $resumptionToken ?? '',
65 | ]);
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/frontend/components/VDecoration.vue:
--------------------------------------------------------------------------------
1 |
31 |
32 |
33 | //- eslint-disable vuejs-accessibility/click-events-have-key-events
34 | //- eslint-disable vuejs-accessibility/no-static-element-interactions
35 | .container(role="none")
36 | .drawing(
37 | v-for="style, index in styles"
38 | :key="index"
39 | :class="`color-${index % 4 + 1}`"
40 | :style="style"
41 | @click="updateStyle(index)"
42 | )
43 |
44 |
45 |
90 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dini-validator",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "vite",
7 | "build": "vite build",
8 | "preview": "vite preview",
9 | "lint": "npm run lint:php; npm run lint:js; npm run lint:scss",
10 | "lint:js": "eslint . --ext .vue,.js --ignore-path .gitignore",
11 | "lint:php": "vendor/bin/phpstan; vendor/bin/pint --test -v",
12 | "lint:scss": "stylelint 'frontend/**/*.{scss,vue}' --rd --risd",
13 | "lint-fix": "npm run lint-fix:php; npm run lint-fix:js; npm run lint-fix:scss",
14 | "lint-fix:js": "eslint . --ext .vue,.js --fix --ignore-path .gitignore",
15 | "lint-fix:php": "vendor/bin/phpstan; vendor/bin/pint -v",
16 | "lint-fix:scss": "stylelint 'frontend/**/*.{scss,vue}' --fix --rd --risd",
17 | "test": "php -dpcov.enabled=1 -dpcov.directory=api vendor/bin/phpunit --color --testdox --coverage-text"
18 | },
19 | "dependencies": {
20 | "@fontsource/barlow": "^5.0.8",
21 | "@fontsource/noto-sans-mono": "^5.0.12",
22 | "@highlightjs/vue-plugin": "^2.1.0",
23 | "highlight.js": "^11.8.0",
24 | "js-confetti": "^0.11.0",
25 | "vue": "^3.3.4",
26 | "vue-material-design-icons": "^5.2.0",
27 | "vue-router": "^4.2.4"
28 | },
29 | "devDependencies": {
30 | "@rushstack/eslint-patch": "^1.4.0",
31 | "@vitejs/plugin-vue": "^4.3.4",
32 | "@volar/vue-language-plugin-pug": "^1.6.5",
33 | "@vue/eslint-config-standard": "^8.0.1",
34 | "@vue/language-plugin-pug": "^1.8.11",
35 | "eslint": "^8.49.0",
36 | "eslint-plugin-unicorn": "^48.0.1",
37 | "eslint-plugin-vue": "^9.17.0",
38 | "eslint-plugin-vue-pug": "^0.6.0",
39 | "eslint-plugin-vuejs-accessibility": "^2.2.0",
40 | "jsdom": "^22.1.0",
41 | "pug": "^3.0.2",
42 | "stylelint": "^15.10.3",
43 | "stylelint-config-recommended-vue": "^1.5.0",
44 | "stylelint-config-standard-scss": "^11.0.0",
45 | "stylelint-config-standard-vue": "^1.0.0",
46 | "stylelint-order": "^6.0.3",
47 | "stylelint-stylistic": "^0.4.3",
48 | "unplugin-fonts": "^1.0.3",
49 | "unplugin-vue-components": "^0.25.2",
50 | "vite": "^4.4.9",
51 | "vite-plugin-eslint": "^1.8.1",
52 | "vite-plugin-rewrite-all": "^1.0.1",
53 | "vite-plugin-stylelint": "^5.1.1"
54 | },
55 | "engines" : {
56 | "node" : ">=18",
57 | "npm" : ">=9"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/pint.json:
--------------------------------------------------------------------------------
1 |
2 | {
3 | "preset": "laravel",
4 | "exclude": [
5 | "cache"
6 | ],
7 | "rules": {
8 | "align_multiline_comment": true,
9 | "array_indentation": true,
10 | "array_push": true,
11 | "array_syntax": true,
12 | "assign_null_coalescing_to_coalesce_equal": true,
13 | "blank_line_after_namespace": true,
14 | "blank_line_after_opening_tag": true,
15 | "combine_consecutive_issets": true,
16 | "concat_space": {
17 | "spacing": "one"
18 | },
19 | "declare_parentheses": true,
20 | "declare_strict_types": true,
21 | "explicit_indirect_variable": true,
22 | "fully_qualified_strict_types": true,
23 | "is_null": true,
24 | "lambda_not_used_import": true,
25 | "logical_operators": true,
26 | "method_argument_space": {
27 | "on_multiline": "ensure_fully_multiline"
28 | },
29 | "method_chaining_indentation": true,
30 | "modernize_strpos": true,
31 | "modernize_types_casting": true,
32 | "new_with_braces": true,
33 | "no_empty_comment": true,
34 | "no_multiple_statements_per_line": true,
35 | "no_superfluous_elseif": true,
36 | "no_useless_else": true,
37 | "nullable_type_declaration_for_default_null_value": true,
38 | "ordered_class_elements": {
39 | "order": [
40 | "use_trait",
41 | "case",
42 | "constant",
43 | "constant_public",
44 | "constant_protected",
45 | "constant_private",
46 | "property_public",
47 | "property_protected",
48 | "property_private",
49 | "construct",
50 | "destruct",
51 | "magic",
52 | "phpunit",
53 | "method_abstract",
54 | "method_public_static",
55 | "method_public",
56 | "method_protected_static",
57 | "method_protected",
58 | "method_private_static",
59 | "method_private"
60 | ],
61 | "sort_algorithm": "none"
62 | },
63 | "ordered_imports": {
64 | "sort_algorithm": "alpha"
65 | },
66 | "ordered_traits": true,
67 | "psr_autoloading": true,
68 | "simplified_if_return": true,
69 | "strict_comparison": true,
70 | "ternary_to_null_coalescing": true,
71 | "trailing_comma_in_multiline": {
72 | "elements": ["arguments", "arrays", "match", "parameters"]
73 | },
74 | "trim_array_spaces": true,
75 | "use_arrow_functions": true
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/api/rules/M_10_5.php:
--------------------------------------------------------------------------------
1 | savedResultFile = "{$this->validator->cacheDir}/xml/ListRecords/2.xml";
16 | }
17 |
18 | public function check($xml, $isLastBatch): void
19 | {
20 | if (empty($xml->ListRecords->resumptionToken)) {
21 | $this->finish('Not relevant due to small repository size');
22 |
23 | return;
24 | }
25 |
26 | if (! file_exists($this->savedResultFile)) {
27 | $this->addFatalIssue('ListRecords', 'XML file not found');
28 |
29 | return;
30 | }
31 |
32 | $savedContents = file_get_contents($this->savedResultFile);
33 | $savedXml = @simplexml_load_string((string) $savedContents);
34 |
35 | $newResult = $this->validator->downloadOaiXml('ListRecords', (string) $xml->ListRecords->resumptionToken);
36 | $newXml = @simplexml_load_string((string) $newResult->content);
37 |
38 | if (! $savedXml) {
39 | $this->addIssue(
40 | "ListRecords&resumptionToken={$xml->ListRecords->resumptionToken}",
41 | 'Invalid response to $1',
42 | 'ListRecords (1)',
43 | );
44 | } elseif (! $newXml) {
45 | $this->addIssue(
46 | "ListRecords&resumptionToken={$xml->ListRecords->resumptionToken}",
47 | 'Invalid response to $1',
48 | 'ListRecords (2)',
49 | );
50 | } else {
51 | $index = 0;
52 | foreach ($savedXml->ListRecords->record as $record) {
53 | if (
54 | ! $newXml->ListRecords?->record
55 | || json_encode($record) !== json_encode($newXml->ListRecords->record[$index])
56 | ) {
57 | $this->addIssue(
58 | "ListRecords&resumptionToken={$xml->ListRecords->resumptionToken}",
59 | 'Consecutive ListRecords requests do not match.',
60 | );
61 | break;
62 | }
63 |
64 | $index++;
65 | }
66 | }
67 |
68 | $this->finish();
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/api/test/xml/ListRecords/good-2.xml:
--------------------------------------------------------------------------------
1 |
2 | metadata is invalid in record-1',
23 | getIssueText($result->issues[0]),
24 | );
25 |
26 | $result = runRule('ListRecords/datacite-bad-metadata');
27 | $this->assertEquals(
28 | 'metadata is invalid in bad-metadata (DataCite)',
29 | getIssueText($result->issues[0]),
30 | );
31 |
32 | $result = runRule('ListRecords/datacite-bad-doi');
33 | $this->assertEquals(
34 | 'DataCite identifier is not a valid DOI in bad-doi (DataCite)',
35 | getIssueText($result->issues[0]),
36 | );
37 |
38 | $result = runRule('ListRecords/datacite-no-match');
39 | $this->assertEquals(
40 | 'creator does not match dc:creator in record-1 (DataCite)',
41 | getIssueText($result->issues[0]),
42 | );
43 | $this->assertEquals(
44 | 'title does not match dc:title in record-1 (DataCite)',
45 | getIssueText($result->issues[1]),
46 | );
47 | $this->assertEquals(
48 | 'publisher does not match dc:publisher in record-1 (DataCite)',
49 | getIssueText($result->issues[2]),
50 | );
51 | $this->assertEquals(
52 | 'publicationYear does not match dc:issued in record-1 (DataCite)',
53 | getIssueText($result->issues[3]),
54 | );
55 | $this->assertEquals(
56 | 'resourceType does not match dc:type in record-1 (DataCite)',
57 | getIssueText($result->issues[4]),
58 | );
59 |
60 | $result = runRule('ListRecords-datacite/good');
61 | $this->assertEquals(0, $result->issuesCount);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/api/classes/Route.php:
--------------------------------------------------------------------------------
1 | method");
27 | header("Content-Type: $this->contentType");
28 | }
29 |
30 | /**
31 | * Fail the request, i.e. send an HTTP error code and an error message,
32 | * and terminate the script.
33 | */
34 | protected function fail(string $message, int $statusCode = 500): never
35 | {
36 | http_response_code($statusCode);
37 | $this->sendJson(['error' => $message]);
38 | exit;
39 | }
40 |
41 | /**
42 | * Read and decode a JSON file.
43 | */
44 | protected function readJson(string $filename): mixed
45 | {
46 | $string = file_get_contents($filename);
47 |
48 | if ($string === false) {
49 | return false;
50 | }
51 |
52 | $result = json_decode($string);
53 |
54 | return $result;
55 | }
56 |
57 | /**
58 | * Sanitize a rule ID to be used as class name.
59 | */
60 | protected function sanitizeRuleId(string $ruleId): ?string
61 | {
62 | return preg_replace('#[^a-zA-Z0-9_]#', '', $ruleId);
63 | }
64 |
65 | /**
66 | * Sanitize a task ID to be used as directory name.
67 | */
68 | protected function sanitizeTaskId(string $taskId): ?string
69 | {
70 | return preg_replace('#[^\p{L}0-9.,;!=\[\]@%~+-]#', '', $taskId);
71 | }
72 |
73 | /**
74 | * Output payload to the browser as JSON.
75 | */
76 | protected function sendJson(mixed $payload): void
77 | {
78 | $json = json_encode($payload, Config::$jsonFlags);
79 |
80 | if ($json) {
81 | echo $json;
82 | } else {
83 | $this->fail('Encoding error: ' . json_last_error_msg());
84 | }
85 | }
86 |
87 | /**
88 | * Serve route
89 | *
90 | * For being overridden in child routes.
91 | */
92 | protected function serve(): void
93 | {
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/public/dini-logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------