diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 000000000..288ac405c --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,21 @@ +name: Unit Testing + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + test: + name: TYPO3 + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install dependencies + run: Build/Test/runTests.sh -s composerInstall -t 11.5 + + - name: Run unit tests + run: Build/Test/runTests.sh -s unit diff --git a/.gitignore b/.gitignore index 4c5f4df22..2714fd7d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ +.cache .idea/ .vscode/ .DS_Store node_modules *.css.map +Build/Test/.env Documentation-GENERATED-temp/ public/ vendor/ diff --git a/Build/Test/UnitTests.xml b/Build/Test/UnitTests.xml new file mode 100644 index 000000000..3f6bfeceb --- /dev/null +++ b/Build/Test/UnitTests.xml @@ -0,0 +1,31 @@ + + + + + ../../Tests/Unit + + + + + + + diff --git a/Build/Test/docker-compose.yml b/Build/Test/docker-compose.yml new file mode 100644 index 000000000..43b7a1fd3 --- /dev/null +++ b/Build/Test/docker-compose.yml @@ -0,0 +1,147 @@ +# Adopted/reduced from https://github.com/TYPO3/typo3/blob/608f238a8b7696a49a47e1e73ce8e2845455f0f5/Build/testing-docker/local/docker-compose.yml + +services: + mysql: + image: docker.io/mysql:${MYSQL_VERSION} + environment: + MYSQL_ROOT_PASSWORD: funcp + tmpfs: + - /var/lib/mysql/:rw,noexec,nosuid + + mariadb: + image: docker.io/mariadb:${MARIADB_VERSION} + environment: + MYSQL_ROOT_PASSWORD: funcp + tmpfs: + - /var/lib/mysql/:rw,noexec,nosuid + + web: + image: docker.io/typo3/core-testing-${DOCKER_PHP_IMAGE}:latest + user: "${HOST_UID}:${HOST_GID}" + stop_grace_period: 1s + volumes: + - ${EXTENSIONS_ROOT}:/var/www/extensions/ + - ${DFGVIEWER_ROOT}:${DFGVIEWER_ROOT} + extra_hosts: + - "host.docker.internal:host-gateway" + ports: + - "${SERVER_PORT}:${SERVER_PORT}" + # NOTE: For testing PageViewProxy, we need another web server web:8001 that + # can be requested from TYPO3 running on web:${SERVER_PORT}. + # Setting PHP_CLI_SERVER_WORKERS wouldn't seem to work consistently. + command: > + /bin/sh -c " + if [ ${PHP_XDEBUG_ON} -eq 0 ]; then + XDEBUG_MODE=\"off\" \ + php -S web:${SERVER_PORT} -t ${DFGVIEWER_ROOT} ${DFGVIEWER_ROOT}/Tests/routeFunctionalInstance.php & + php -S web:8001 -t ${DFGVIEWER_ROOT} + else + XDEBUG_MODE=\"debug,develop\" \ + XDEBUG_TRIGGER=\"foo\" \ + XDEBUG_CONFIG=\"client_port=${PHP_XDEBUG_PORT} client_host=host.docker.internal\" \ + php -S web:${SERVER_PORT} -t ${DFGVIEWER_ROOT} ${DFGVIEWER_ROOT}/Tests/routeFunctionalInstance.php & + php -S web:8001 -t ${DFGVIEWER_ROOT} + fi + " + composer_install: + image: docker.io/typo3/core-testing-${DOCKER_PHP_IMAGE}:latest + user: "${HOST_UID}:${HOST_GID}" + volumes: + - ${EXTENSIONS_ROOT}:/var/www/extensions/ + - ${DFGVIEWER_ROOT}:${DFGVIEWER_ROOT} + working_dir: ${DFGVIEWER_ROOT} + environment: + COMPOSER_CACHE_DIR: ".cache/composer" + command: > + /bin/sh -c " + if [ ${SCRIPT_VERBOSE} -eq 1 ]; then + set -x + fi + if [ -z ${TYPO3_VERSION} ]; then + composer install --no-progress --no-interaction; + else + composer update --with=typo3/cms-core:^${TYPO3_VERSION} --no-progress --no-interaction; + fi + " + functional: + image: docker.io/typo3/core-testing-${DOCKER_PHP_IMAGE}:latest + links: + - ${DBMS} + - web + user: "${HOST_UID}:${HOST_GID}" + volumes: + - ${EXTENSIONS_ROOT}:/var/www/extensions/ + - ${DFGVIEWER_ROOT}:${DFGVIEWER_ROOT} + environment: + typo3DatabaseDriver: "${DATABASE_DRIVER}" + typo3DatabaseName: func_test + typo3DatabaseUsername: root + typo3DatabasePassword: funcp + typo3DatabaseHost: ${DBMS} + working_dir: ${DFGVIEWER_ROOT} + extra_hosts: + - "host.docker.internal:host-gateway" + command: > + /bin/sh -c " + if [ ${SCRIPT_VERBOSE} -eq 1 ]; then + set -x + fi + echo Waiting for database start...; + while ! nc -z ${DBMS} 3306; do + sleep 1; + done; + echo Database is up; + echo Waiting for Solr start...; + while ! nc -z solr 8983; do + sleep 1; + done; + echo Solr is up; + if [ ${SCRIPT_VERBOSE} -eq 1 ]; then + set -x + fi + php -v | grep '^PHP' + if [ ${PHPUNIT_WATCH} -eq 0 ]; then + PHPUNIT_BIN=\"vendor/bin/phpunit\" + else + PHPUNIT_BIN=\"vendor/bin/phpunit-watcher watch\" + fi + COMMAND=\"$${PHPUNIT_BIN} -c Build/Test/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} ${TEST_FILE}\" + if [ ${PHP_XDEBUG_ON} -eq 0 ]; then + XDEBUG_MODE=\"off\" $${COMMAND}; + else + XDEBUG_MODE=\"debug,develop\" \ + XDEBUG_TRIGGER=\"foo\" \ + XDEBUG_CONFIG=\"client_port=${PHP_XDEBUG_PORT} client_host=host.docker.internal\" \ + $${COMMAND}; + fi + " + unit: + image: docker.io/typo3/core-testing-${DOCKER_PHP_IMAGE}:latest + user: "${HOST_UID}:${HOST_GID}" + volumes: + - ${EXTENSIONS_ROOT}:/var/www/extensions/ + - ${DFGVIEWER_ROOT}:${DFGVIEWER_ROOT} + working_dir: ${DFGVIEWER_ROOT} + extra_hosts: + - "host.docker.internal:host-gateway" + command: > + /bin/sh -c " + if [ ${SCRIPT_VERBOSE} -eq 1 ]; then + set -x + fi + php -v | grep '^PHP' + if [ ${PHPUNIT_WATCH} -eq 0 ]; then + PHPUNIT_BIN=\"vendor/bin/phpunit\" + else + PHPUNIT_BIN=\"vendor/bin/phpunit-watcher watch\" + fi + if [ ${PHP_XDEBUG_ON} -eq 0 ]; then + XDEBUG_MODE=\"off\" \ + $${PHPUNIT_BIN} -c Build/Test/UnitTests.xml ${EXTRA_TEST_OPTIONS} ${TEST_FILE}; + else + XDEBUG_MODE=\"debug,develop\" \ + XDEBUG_TRIGGER=\"foo\" \ + XDEBUG_CONFIG=\"client_port=${PHP_XDEBUG_PORT} client_host=host.docker.internal\" \ + $${PHPUNIT_BIN} -c Build/Test/UnitTests.xml ${EXTRA_TEST_OPTIONS} ${TEST_FILE}; + fi + " diff --git a/Build/Test/runTests.sh b/Build/Test/runTests.sh new file mode 100755 index 000000000..d3f109f44 --- /dev/null +++ b/Build/Test/runTests.sh @@ -0,0 +1,367 @@ +#!/usr/bin/env bash + +# Adopted/reduced from https://github.com/TYPO3/typo3/blob/f6d73fea5a8f3a5cd8537e29308f18bec65a0c92/Build/Scripts/runTests.sh + +# Function to write a .env file in Build/Test +# This is read by docker compose and vars defined here are +# used in Build/Test/docker-compose.yml +setUpDockerComposeDotEnv() { + # Delete possibly existing local .env file if exists + [ -e .env ] && rm .env + # Set up a new .env file for docker compose + { + echo "COMPOSE_PROJECT_NAME=dfgviewer_testing" + # To prevent access rights of files created by the testing, the docker image later + # runs with the same user that is currently executing the script. docker compose can't + # use $UID directly itself since it is a shell variable and not an env variable, so + # we have to set it explicitly here. + echo "HOST_UID=$(id -u)" + echo "HOST_GID=$(id -g)" + # Your local user + echo "DFGVIEWER_ROOT=${DFGVIEWER_ROOT}" + echo "EXTENSIONS_ROOT=$(dirname "$DFGVIEWER_ROOT")" + echo "HOST_USER=${USER}" + echo "TYPO3_VERSION=${TYPO3_VERSION}" + echo "TEST_FILE=${TEST_FILE}" + echo "PHP_XDEBUG_ON=${PHP_XDEBUG_ON}" + echo "PHP_XDEBUG_PORT=${PHP_XDEBUG_PORT}" + echo "SERVER_PORT=${SERVER_PORT}" + echo "DOCKER_PHP_IMAGE=${DOCKER_PHP_IMAGE}" + echo "EXTRA_TEST_OPTIONS=${EXTRA_TEST_OPTIONS}" + echo "SCRIPT_VERBOSE=${SCRIPT_VERBOSE}" + echo "PHPUNIT_WATCH=${PHPUNIT_WATCH}" + echo "DBMS=${DBMS}" + echo "DATABASE_DRIVER=${DATABASE_DRIVER}" + echo "MARIADB_VERSION=${MARIADB_VERSION}" + echo "MYSQL_VERSION=${MYSQL_VERSION}" + echo "PHP_VERSION=${PHP_VERSION}" + } > .env +} + +# Options -a and -d depend on each other. The function +# validates input combinations and sets defaults. +handleDbmsAndDriverOptions() { + case ${DBMS} in + mysql|mariadb) + [ -z "${DATABASE_DRIVER}" ] && DATABASE_DRIVER="mysqli" + if [ "${DATABASE_DRIVER}" != "mysqli" ] && [ "${DATABASE_DRIVER}" != "pdo_mysql" ]; then + echo "Invalid option -a ${DATABASE_DRIVER} with -d ${DBMS}" >&2 + echo >&2 + echo "call \".Build/Test/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + ;; + esac +} + +# Load help text into $HELP +read -r -d '' HELP <=20.10 for xdebug break pointing to work reliably, and +a recent docker compose (tested >=2.27.1) is needed. + +Usage: $0 [options] [file] + +No arguments: Run all unit tests with PHP 7.4 + +Options: + -s <...> + Specifies which test suite to run + - composerInstall: "composer install" + - functional: PHP functional tests + - unit (default): PHP unit tests + + -t <|10.4|11.5> + Only with -s composerInstall + Specifies which TYPO3 version to install. When unset, installs either the packages from + composer.lock, or the latest version otherwise (default behavior of "composer install"). + - 10.4 + - 11.5 + + -a + Only with -s functional + Specifies to use another driver, following combinations are available: + - mysql + - mysqli (default) + - pdo_mysql + - mariadb + - mysqli (default) + - pdo_mysql + + -d + Only with -s functional + Specifies on which DBMS tests are performed + - mariadb (default): use mariadb + - mysql: use MySQL server + + -i <10.1|10.2|10.3|10.4|10.5|10.6|10.7|10.8|10.9|10.10> + Only with -d mariadb + Specifies on which version of mariadb tests are performed + - 10.1 + - 10.2 + - 10.3 (default) + - 10.4 + - 10.5 + - 10.6 + - 10.7 + - 10.8 + - 10.9 + - 10.10 + + -j <5.5|5.6|5.7|8.0> + Only with -d mysql + Specifies on which version of mysql tests are performed + - 5.5 (default) + - 5.6 + - 5.7 + - 8.0 + + -p <7.4|8.0|8.1|8.2> + Specifies the PHP minor version to be used + - 7.4: (default) use PHP 7.4 + - 8.0: use PHP 8.0 + - 8.1: use PHP 8.1 + - 8.2: use PHP 8.2 (note that xdebug is currently not available for PHP8.2) + + -e "" + Only with -s functional|functionalDeprecated|unit|unitDeprecated|unitRandom|acceptance + Additional options to send to phpunit (unit & functional tests) or codeception (acceptance + tests). For phpunit, options starting with "--" must be added after options starting with "-". + Example -e "-v --filter canRetrieveValueWithGP" to enable verbose output AND filter tests + named "canRetrieveValueWithGP" + + -x + Only with -s functional|functionalDeprecated|unit|unitDeprecated|unitRandom|acceptance|acceptanceInstall + Send information to host instance for test or system under test break points. This is especially + useful if a local PhpStorm instance is listening on default xdebug port 9003. A different port + can be selected with -y + + -y + Send xdebug information to a different port than default 9003 if an IDE like PhpStorm + is not listening on default port. + + -w + Only with -s functional|unit + Run tests in watch mode. + + -u + Update existing typo3/core-testing-*:latest docker images and remove dangling local docker volumes. + Maintenance call to docker pull latest versions of the main php images. The images are updated once + in a while and only the latest ones are supported by core testing. Use this if weird test errors occur. + Also removes obsolete image versions of typo3/core-testing-*. + + -v + Enable verbose script output. Shows variables and docker commands. + + -h + Show this help. +EOF + +# Go to the directory this script is located, so everything else is relative +# to this dir, no matter from where this script is called. +THIS_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" +cd "$THIS_SCRIPT_DIR" || exit 1 + +# Go to directory that contains the local docker-compose.yml file +# cd ../testing-docker/local || exit 1 + +# Set root path by checking whether realpath exists +if ! command -v realpath &> /dev/null; then + echo "Consider installing realpath for properly resolving symlinks" >&2 + DFGVIEWER_ROOT="${PWD}/../../" +else + DFGVIEWER_ROOT=$(realpath "${PWD}/../../") +fi + +# Option defaults +TEST_SUITE="unit" +TYPO3_VERSION="" +DBMS="mariadb" +PHP_VERSION="7.4" +PHP_XDEBUG_ON=0 +PHP_XDEBUG_PORT=9003 +SERVER_PORT=8000 +EXTRA_TEST_OPTIONS="" +SCRIPT_VERBOSE=0 +PHPUNIT_WATCH=0 +DATABASE_DRIVER="" +MARIADB_VERSION="10.3" +MYSQL_VERSION="5.5" + +# Option parsing +# Reset in case getopts has been used previously in the shell +OPTIND=1 +# Array for invalid options +INVALID_OPTIONS=(); +# Simple option parsing based on getopts (! not getopt) +while getopts ":a:s:t:d:i:j:p:e:xy:whuv" OPT; do + case ${OPT} in + s) + TEST_SUITE=${OPTARG} + ;; + t) + TYPO3_VERSION=${OPTARG} + ;; + a) + DATABASE_DRIVER=${OPTARG} + ;; + d) + DBMS=${OPTARG} + ;; + i) + MARIADB_VERSION=${OPTARG} + if ! [[ ${MARIADB_VERSION} =~ ^(10.2|10.3|10.4|10.5|10.6|10.11)$ ]]; then + INVALID_OPTIONS+=("${OPTARG}") + fi + ;; + j) + MYSQL_VERSION=${OPTARG} + if ! [[ ${MYSQL_VERSION} =~ ^(5.7|8.0)$ ]]; then + INVALID_OPTIONS+=("${OPTARG}") + fi + ;; + p) + PHP_VERSION=${OPTARG} + if ! [[ ${PHP_VERSION} =~ ^(7.4|8.0|8.1|8.2|8.3)$ ]]; then + INVALID_OPTIONS+=("${OPTARG}") + fi + ;; + e) + EXTRA_TEST_OPTIONS=${OPTARG} + ;; + x) + PHP_XDEBUG_ON=1 + ;; + y) + PHP_XDEBUG_PORT=${OPTARG} + ;; + w) + PHPUNIT_WATCH=1 + ;; + h) + echo "${HELP}" + exit 0 + ;; + u) + TEST_SUITE=update + ;; + v) + SCRIPT_VERBOSE=1 + ;; + \?) + INVALID_OPTIONS+=("${OPTARG}") + ;; + :) + INVALID_OPTIONS+=("${OPTARG}") + ;; + esac +done + +# Exit on invalid options +if [ ${#INVALID_OPTIONS[@]} -ne 0 ]; then + echo "Invalid option(s):" >&2 + for I in "${INVALID_OPTIONS[@]}"; do + echo "-${I}" >&2 + done + echo >&2 + echo "call \".Build/Test/runTests.sh -h\" to display help and valid options" + exit 1 +fi + +# Move "7.4" to "php74", the latter is the docker container name +DOCKER_PHP_IMAGE=$(echo "php${PHP_VERSION}" | sed -e 's/\.//') + +# Set $1 to first mass argument, this is the optional test file or test directory to execute +shift $((OPTIND - 1)) +TEST_FILE=${1} + +if [ ${SCRIPT_VERBOSE} -eq 1 ]; then + set -x +fi + +# Suite execution +case ${TEST_SUITE} in + composerInstall) + setUpDockerComposeDotEnv + docker compose run composer_install + SUITE_EXIT_CODE=$? + docker compose down + ;; + functional) + handleDbmsAndDriverOptions + setUpDockerComposeDotEnv + case ${DBMS} in + mariadb|mysql) + echo "Using driver: ${DATABASE_DRIVER}" + docker compose run functional + SUITE_EXIT_CODE=$? + ;; + *) + echo "Functional tests don't run with DBMS ${DBMS}" >&2 + echo >&2 + echo "call \".Build/Test/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + esac + docker compose down + ;; + unit) + setUpDockerComposeDotEnv + docker compose run unit + SUITE_EXIT_CODE=$? + docker compose down + ;; + update) + # pull typo3/core-testing-*:latest versions of those ones that exist locally + docker images typo3/core-testing-*:latest --format "{{.Repository}}:latest" | xargs -I {} docker pull {} + # remove "dangling" typo3/core-testing-* images (those tagged as ) + docker images typo3/core-testing-* --filter "dangling=true" --format "{{.ID}}" | xargs -I {} docker rmi {} + ;; + *) + echo "Invalid -s option argument ${TEST_SUITE}" >&2 + echo >&2 + echo "${HELP}" >&2 + exit 1 +esac + +case ${DBMS} in + mariadb) + DBMS_OUTPUT="DBMS: ${DBMS} version ${MARIADB_VERSION} driver ${DATABASE_DRIVER}" + ;; + mysql) + DBMS_OUTPUT="DBMS: ${DBMS} version ${MYSQL_VERSION} driver ${DATABASE_DRIVER}" + ;; + *) + DBMS_OUTPUT="DBMS not recognized: $DBMS" + exit 1 +esac + +# Print summary +if [ ${SCRIPT_VERBOSE} -eq 1 ]; then + # Turn off verbose mode for the script summary + set +x +fi +echo "" >&2 +echo "###########################################################################" >&2 +if [[ ${TEST_SUITE} =~ ^(functional)$ ]]; then + echo "Result of ${TEST_SUITE}" >&2 + echo "PHP: ${PHP_VERSION}" >&2 + echo "${DBMS_OUTPUT}" >&2 +else + echo "Result of ${TEST_SUITE}" >&2 + echo "PHP: ${PHP_VERSION}" >&2 +fi + +if [[ ${SUITE_EXIT_CODE} -eq 0 ]]; then + echo "SUCCESS" >&2 +else + echo "FAILURE" >&2 +fi +echo "###########################################################################" >&2 +echo "" >&2 + +# Exit with code of test suite - This script return non-zero if the executed test failed. +exit "$SUITE_EXIT_CODE" diff --git a/Classes/Common/ValidationHelper.php b/Classes/Common/ValidationHelper.php new file mode 100644 index 000000000..8b4b97f21 --- /dev/null +++ b/Classes/Common/ValidationHelper.php @@ -0,0 +1,91 @@ + + * All rights reserved + * + * This script is part of the TYPO3 project. The TYPO3 project is + * free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * The GNU General Public License can be found at + * http://www.gnu.org/copyleft/gpl.html. + * + * This script is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * This copyright notice MUST APPEAR in all copies of the script! + */ + +/** + * The validator helper contains constants and functions to support the validation process. + * + * @package TYPO3 + * @subpackage dfg-viewer + * + * @access public + */ +class ValidationHelper +{ + const NAMESPACE_DV = 'http://dfg-viewer.de/'; + + const NAMESPACE_METS = 'http://www.loc.gov/METS/'; + + const STRUCTURE_DATASET = [ + 'section', 'file', 'album', 'register', 'annotation', 'address', 'article', 'atlas', 'issue', 'bachelor_thesis', 'volume', 'contained_work', 'additional', 'report', 'official_notification', 'provenance', 'inventory', 'image', 'collation', 'ornament', 'letter', 'cover', 'cover_front', 'cover_back', 'diploma_thesis', 'doctoral_thesis', 'document', 'printers_mark', 'printed_archives', 'binding', 'entry', 'corrigenda', 'bookplate', 'fascicle', 'leaflet', 'research_paper', 'photograph', 'fragment', 'land_register', 'ground_plan', 'habilitation_thesis', 'manuscript', 'illustration', 'imprint', 'contents', 'initial_decoration', 'year', 'chapter', 'map', 'cartulary', 'colophon', 'ephemera', 'engraved_titlepage', 'magister_thesis', 'folder', 'master_thesis', 'multivolume_work', 'month', 'monograph', 'musical_notation', 'periodical', 'poster', 'plan', 'privileges', 'index', 'spine', 'scheme', 'edge', 'seal', 'paste down', 'stamp', 'study', 'table', 'day', 'proceeding', 'text', 'title_page', 'subinventory', 'act', 'judgement', 'verse', 'note', 'preprint', 'dossier', 'lecture', 'endsheet', 'paper', 'preface', 'dedication', 'newspaper' + ]; + + const XPATH_METS = '//mets:mets'; + + const XPATH_ADMINISTRATIVE_METADATA = self::XPATH_METS . '/mets:amdSec'; + + const XPATH_ADMINISTRATIVE_TECHNICAL_METADATA = self::XPATH_ADMINISTRATIVE_METADATA . '/mets:techMD'; + + const XPATH_ADMINISTRATIVE_RIGHTS_METADATA = self::XPATH_ADMINISTRATIVE_METADATA . '/mets:rightsMD'; + + const XPATH_ADMINISTRATIVE_DIGIPROV_METADATA = self::XPATH_ADMINISTRATIVE_METADATA . '/mets:digiprovMD'; + + const XPATH_DESCRIPTIVE_METADATA_SECTIONS = self::XPATH_METS . '/mets:dmdSec'; + + const XPATH_FILE_SECTIONS = self::XPATH_METS . '/mets:fileSec'; + + const XPATH_FILE_SECTION_GROUPS = self::XPATH_FILE_SECTIONS . '/mets:fileGrp'; + + const XPATH_FILE_SECTION_FILES = self::XPATH_FILE_SECTION_GROUPS . '/mets:file'; + + const XPATH_STRUCT_LINK = self::XPATH_METS . '/mets:structLink'; + + const XPATH_STRUCT_LINK_ELEMENTS = self::XPATH_STRUCT_LINK . '/mets:smLink'; + + const XPATH_LOGICAL_STRUCTURES = self::XPATH_METS . '/mets:structMap[@TYPE="LOGICAL"]'; + + const XPATH_LOGICAL_STRUCTURAL_ELEMENTS = self::XPATH_LOGICAL_STRUCTURES . '/mets:div'; + + const XPATH_LOGICAL_EXTERNAL_REFERENCES = self::XPATH_LOGICAL_STRUCTURAL_ELEMENTS . '/mets:mptr'; + + const XPATH_PHYSICAL_STRUCTURES = self::XPATH_METS . '/mets:structMap[@TYPE="PHYSICAL"]'; + + const XPATH_PHYSICAL_STRUCTURAL_ELEMENT_SEQUENCE = self::XPATH_PHYSICAL_STRUCTURES . '/mets:div'; + + const XPATH_PHYSICAL_STRUCTURAL_ELEMENTS = self::XPATH_PHYSICAL_STRUCTURAL_ELEMENT_SEQUENCE . '/mets:div'; + + const XPATH_DVRIGHTS = self::XPATH_ADMINISTRATIVE_RIGHTS_METADATA . '/mets:mdWrap[@MDTYPE="OTHER" and @OTHERMDTYPE="DVRIGHTS"]/mets:xmlData/dv:rights'; + + const XPATH_DVLINKS = self::XPATH_ADMINISTRATIVE_DIGIPROV_METADATA . '/mets:mdWrap[@MDTYPE="OTHER" and @OTHERMDTYPE="DVLINKS"]/mets:xmlData/dv:links'; + + public static function trimDoubleSlash(string $value): string + { + if (str_starts_with($value, '//')) { + return substr($value, 1); + } + return $value; + } +} diff --git a/Classes/Validation/AbstactDomDocumentValidator.php b/Classes/Validation/AbstactDomDocumentValidator.php new file mode 100644 index 000000000..f5c85b993 --- /dev/null +++ b/Classes/Validation/AbstactDomDocumentValidator.php @@ -0,0 +1,65 @@ + + * All rights reserved + * + * This script is part of the TYPO3 project. The TYPO3 project is + * free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * The GNU General Public License can be found at + * http://www.gnu.org/copyleft/gpl.html. + * + * This script is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * This copyright notice MUST APPEAR in all copies of the script! + */ + +use DOMDocument; +use DOMNode; +use DOMXPath; +use Kitodo\Dlf\Validation\AbstractDlfValidator; +use Slub\Dfgviewer\Validation\Dom\DomNodeListValidator; +use Slub\Dfgviewer\Validation\Dom\DomNodeValidator; + +abstract class AbstactDomDocumentValidator extends AbstractDlfValidator +{ + + /** + * @var DOMXPath The XPath of DOMDocument value. + */ + protected DOMXpath $xpath; + + public function __construct() + { + parent::__construct(DOMDocument::class); + } + + abstract public function isValidDocument(); + + protected function isValid($value): void + { + $this->xpath = new DOMXPath($value); + $this->isValidDocument(); + } + + protected function createNodeListValidator(string $expression, ?DOMNode $contextNode=null): DomNodeListValidator + { + return new DomNodeListValidator($this->xpath, $this->result, $expression, $contextNode); + } + + protected function createNodeValidator(?DOMNode $node): DomNodeValidator + { + return new DomNodeValidator($this->xpath, $this->result, $node); + } +} diff --git a/Classes/Validation/ApplicationProfileValidationStack.php b/Classes/Validation/ApplicationProfileValidationStack.php new file mode 100644 index 000000000..9ab3c46a7 --- /dev/null +++ b/Classes/Validation/ApplicationProfileValidationStack.php @@ -0,0 +1,51 @@ + + * All rights reserved + * + * This script is part of the TYPO3 project. The TYPO3 project is + * free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * The GNU General Public License can be found at + * http://www.gnu.org/copyleft/gpl.html. + * + * This script is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * This copyright notice MUST APPEAR in all copies of the script! + */ + +use Kitodo\Dlf\Validation\AbstractDlfValidationStack; +use Slub\Dfgviewer\Validation\Mets\AdministrativeMetadataValidator; +use Slub\Dfgviewer\Validation\Mets\DescriptiveMetadataValidator; +use Slub\Dfgviewer\Validation\Mets\DigitalRepresentationValidator; +use Slub\Dfgviewer\Validation\Mets\LinkingLogicalPhysicalStructureValidator; +use Slub\Dfgviewer\Validation\Mets\LogicalStructureValidator; +use Slub\Dfgviewer\Validation\Mets\PhysicalStructureValidator; + +class ApplicationProfileValidationStack extends AbstractDlfValidationStack +{ + public function __construct() + { + parent::__construct(\DOMDocument::class); + $this->addValidator(LogicalStructureValidator::class, "Validation of the logical document structure", false); + $this->addValidator(PhysicalStructureValidator::class, "Validation of the physical document structure", false); + $this->addValidator(LinkingLogicalPhysicalStructureValidator::class, "Validation of linking between logical and physical structure", false); + $this->addValidator(DigitalRepresentationValidator::class, "Validation of the digital representation", false); + $this->addValidator(DescriptiveMetadataValidator::class, "Validation of the descriptive metadata", false); + $this->addValidator(AdministrativeMetadataValidator::class, "Validation of the administrative metadata", false); + $this->addValidator(DvMetadataValidator::class, "Validation of the DFG-Viewer specific details", false); + } +} diff --git a/Classes/Validation/Dom/DomNodeListValidator.php b/Classes/Validation/Dom/DomNodeListValidator.php new file mode 100644 index 000000000..479add2f9 --- /dev/null +++ b/Classes/Validation/Dom/DomNodeListValidator.php @@ -0,0 +1,151 @@ + + * All rights reserved + * + * This script is part of the TYPO3 project. The TYPO3 project is + * free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * The GNU General Public License can be found at + * http://www.gnu.org/copyleft/gpl.html. + * + * This script is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * This copyright notice MUST APPEAR in all copies of the script! + */ + +use DOMNode; +use DOMNodeList; +use DOMXPath; +use TYPO3\CMS\Extbase\Error\Error; +use TYPO3\CMS\Extbase\Error\Result; + +/** + * The validator contains functions to validate a DOMNodeList. + * + * @package TYPO3 + * @subpackage dfg-viewer + * + * @access public + */ +class DomNodeListValidator +{ + + /** + * @var string The expression of XPath query + */ + private string $expression; + + /** + * @var DOMNode|null The context node of XPath query + */ + private ?DOMNode $contextNode; + + /** + * @var DOMNodeList The node list result of XPath query + */ + private DOMNodeList $nodeList; + + /** + * @var Result The result containing errors of validation + */ + private Result $result; + + public function __construct(DOMXPath $xpath, Result $result, string $expression, ?DOMNode $contextNode=null) + { + $this->expression = $expression; + $this->contextNode = $contextNode; + $this->nodeList = $xpath->query($expression, $contextNode); + $this->result = $result; + } + + /** + * Get the first node from the node list. + * + * @return DOMNode|null + */ + public function getFirstNode(): ?DOMNode + { + return $this->getNode(0); + } + + /** + * Get a node from the node list at a specific index. + * + * @param int $index The index to retrieve the node + * @return DOMNode|null + */ + public function getNode(int $index): ?DOMNode + { + return $this->nodeList->item($index); + } + + /** + * Get the node list. + * + * @return DOMNodeList + */ + public function getNodeList(): DOMNodeList + { + return $this->nodeList; + } + + /** + * Validates the node list has any node. + * + * @return $this + */ + public function validateHasAny(): DomNodeListValidator + { + if (!$this->nodeList->length > 0) { + $this->addError('There must be at least one element', 1736504345); + } + return $this; + } + + /** + * Validates the node list has one node. + * + * @return $this + */ + public function validateHasOne(): DomNodeListValidator + { + if ($this->nodeList->length != 1) { + $this->addError('There must be an element', 1736504354); + } + return $this; + } + + /** + * Validates the node list has none or one node. + * + * @return $this + */ + public function validateHasNoneOrOne(): DomNodeListValidator + { + if (!($this->nodeList->length == 0 || $this->nodeList->length == 1)) { + $this->addError('There must be no more than one element', 1736504361); + } + return $this; + } + + private function addError(string $prefix, int $code): void + { + $message = $prefix . ' that matches the XPath expression "' . $this->expression . '"'; + if ($this->contextNode) { + $message .= ' under "' . $this->contextNode->getNodePath() . '"'; + } + $this->result->addError(new Error($message, $code)); + } +} diff --git a/Classes/Validation/Dom/DomNodeValidator.php b/Classes/Validation/Dom/DomNodeValidator.php new file mode 100644 index 000000000..7f27d0646 --- /dev/null +++ b/Classes/Validation/Dom/DomNodeValidator.php @@ -0,0 +1,251 @@ + + * All rights reserved + * + * This script is part of the TYPO3 project. The TYPO3 project is + * free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * The GNU General Public License can be found at + * http://www.gnu.org/copyleft/gpl.html. + * + * This script is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * This copyright notice MUST APPEAR in all copies of the script! + */ + +use DOMNode; +use DOMXPath; +use TYPO3\CMS\Extbase\Error\Error; +use TYPO3\CMS\Extbase\Error\Result; + +/** + * The validator contains functions to validate a DOMNode. + * + * @package TYPO3 + * @subpackage dfg-viewer + * + * @access public + */ +class DomNodeValidator +{ + + /** + * @var DOMXPath The XPath of document to validate + */ + private DOMXPath $xpath; + + /** + * @var DOMNode|null The node to validate + */ + private ?DOMNode $node; + + /** + * @var Result The result containing errors of validation + */ + private Result $result; + + public function __construct(DOMXPath $xpath, Result $result, ?DOMNode $node) + { + $this->xpath = $xpath; + $this->result = $result; + $this->node = $node; + } + + /** + * Validate that the node's content contains an Email. + * + * @return $this + */ + public function validateHasContentWithEmail(): DomNodeValidator + { + if (!isset($this->node) || !$this->node->nodeValue) { + return $this; + } + + $email = $this->node->nodeValue; + + if (str_starts_with(strtolower($email), 'mailto:')) { + $email = substr($email, 7); + } + + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + $this->result->addError(new Error('Email "' . $this->node->nodeValue . '" in the content of "' . $this->node->getNodePath() . '" is not valid.', 1736504169)); + } + + return $this; + } + + /** + * Validate that the node's content contains a URL. + * + * @return $this + */ + public function validateHasContentWithUrl(): DomNodeValidator + { + if (!isset($this->node) || !$this->node->nodeValue) { + return $this; + } + + if (!filter_var($this->node->nodeValue, FILTER_VALIDATE_URL)) { + $this->result->addError(new Error('URL "' . $this->node->nodeValue . '" in the content of "' . $this->node->getNodePath() . '" is not valid.', 1736504177)); + } + + return $this; + } + + /** + * Validate that the node has an attribute with a URL value. + * + * @param string $name The attribute name + * @return $this + */ + public function validateHasAttributeWithUrl(string $name): DomNodeValidator + { + if (!isset($this->node)) { + return $this; + } + + // @phpstan-ignore-next-line + if (!$this->node->hasAttribute($name)) { + return $this->validateHasAttribute($name); + } + + // @phpstan-ignore-next-line + $value = $this->node->getAttribute($name); + if (!filter_var($value, FILTER_VALIDATE_URL)) { + $this->result->addError(new Error('URL "' . $value . '" in the "' . $name . '" attribute of "' . $this->node->getNodePath() . '" is not valid.', 1736504189)); + } + + return $this; + } + + /** + * Validate that the node has an attribute with a specific value. + * + * @param string $name The attribute name + * @param array $values The allowed values + * @return $this + */ + public function validateHasAttributeWithValue(string $name, array $values): DomNodeValidator + { + if (!isset($this->node)) { + return $this; + } + + // @phpstan-ignore-next-line + if (!$this->node->hasAttribute($name)) { + return $this->validateHasAttribute($name); + } + + // @phpstan-ignore-next-line + $value = $this->node->getAttribute($name); + if (!in_array($value, $values)) { + $this->result->addError(new Error('Value "' . $value . '" in the "' . $name . '" attribute of "' . $this->node->getNodePath() . '" is not permissible.', 1736504197)); + } + + return $this; + } + + /** + * Validate that the node has a unique attribute with name. + * + * @param string $name The attribute name + * @param string $contextExpression The context expression to determine uniqueness. + * @return $this + */ + public function validateHasUniqueAttribute(string $name, string $contextExpression): DomNodeValidator + { + if (!isset($this->node)) { + return $this; + } + + // @phpstan-ignore-next-line + if (!$this->node->hasAttribute($name)) { + return $this->validateHasAttribute($name); + } + + // @phpstan-ignore-next-line + $value = $this->node->getAttribute($name); + if ($this->xpath->query($contextExpression . '[@' . $name . '="' . $value . '"]')->length > 1) { + $this->result->addError(new Error('"' . $name . '" attribute with value "' . $value . '" of "' . $this->node->getNodePath() . '" already exists.', 1736504203)); + } + + return $this; + } + + /** + * Validate that the node has a unique identifier. + * + * @return $this + */ + public function validateHasUniqueId(): DomNodeValidator + { + $this->validateHasUniqueAttribute("ID", "//*"); + return $this; + } + + /** + * Validate that the node has attribute with name. + * + * @param string $name The attribute name + * @return $this + */ + public function validateHasAttribute(string $name): DomNodeValidator + { + if (!isset($this->node)) { + return $this; + } + + // @phpstan-ignore-next-line + if (!$this->node->hasAttribute($name)) { + $this->result->addError(new Error('Mandatory "' . $name . '" attribute of "' . $this->node->getNodePath() . '" is missing.', 1736504217)); + } + return $this; + } + + /** + * Validate that the node's resolvable identifier attribute points to a target with the specified "ID" attribute. + * + * @param string $name The attribute name containing the reference id as value + * @param string $targetExpression The context expression to the target reference + * @return $this + */ + public function validateHasReferenceToId(string $name, string $targetExpression): DomNodeValidator + { + if (!isset($this->node)) { + return $this; + } + + // @phpstan-ignore-next-line + if (!$this->node->hasAttribute($name)) { + return $this->validateHasAttribute($name); + } + + $targetNodes = $this->xpath->query($targetExpression); + // @phpstan-ignore-next-line + $identifier = $this->node->getAttribute($name); + + $foundElements = 0; + foreach ($targetNodes as $targetNode) { + $foundElements += $this->xpath->query('//*[@ID="' . $identifier . '"]', $targetNode)->length; + } + + if ($foundElements !== 1) { + $this->result->addError(new Error('Value "' . $identifier . '" in the "' . $name . '" attribute of "' . $this->node->getNodePath() . '" must reference one element under XPath expression "' . $targetExpression, 1736504228)); + } + + return $this; + } +} diff --git a/Classes/Validation/DvMetadataValidator.php b/Classes/Validation/DvMetadataValidator.php new file mode 100644 index 000000000..f722a7c42 --- /dev/null +++ b/Classes/Validation/DvMetadataValidator.php @@ -0,0 +1,155 @@ + + * All rights reserved + * + * This script is part of the TYPO3 project. The TYPO3 project is + * free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * The GNU General Public License can be found at + * http://www.gnu.org/copyleft/gpl.html. + * + * This script is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * This copyright notice MUST APPEAR in all copies of the script! + */ + +use Slub\Dfgviewer\Common\ValidationHelper as VH; + +/** + * The validator validates against the rules outlined in chapter 2.7 of the METS application profile 2.3.1. + * + * @package TYPO3 + * @subpackage dfg-viewer + * + * @access public + */ +class DvMetadataValidator extends AbstactDomDocumentValidator +{ + public function isValidDocument(): void + { + // Validates against the rules of chapter "2.7.1 Rechteangaben – dv:rights" + $this->createNodeListValidator(VH::XPATH_DVRIGHTS) + ->validateHasOne(); + + $this->validateDvRights(); + + // Validates against the rules of chapter "2.7.3 Verweise – dv:links" + $this->createNodeListValidator(VH::XPATH_DVLINKS) + ->validateHasOne(); + + $this->validateDvLinks(); + } + + /** + * Validates the DFG-Viewer links. + * + * Validates against the rules of chapter "2.7.4 Unterelemente zu dv:links" + * + * @return void + */ + protected function validateDvLinks(): void + { + $references = $this->createNodeListValidator(VH::XPATH_DVLINKS . '/dv:reference') + ->validateHasAny() + ->getNodeList(); + foreach ($references as $reference) { + $this->validateReference($reference); + } + + $this->createNodeListValidator(VH::XPATH_DVLINKS . '/dv:presentation') + ->validateHasNoneOrOne(); + + $sruNode = $this->createNodeListValidator(VH::XPATH_DVLINKS . '/dv:sru') + ->validateHasNoneOrOne()->getFirstNode(); + $this->createNodeValidator($sruNode)->validateHasContentWithUrl(); + + $iiifNode = $this->createNodeListValidator(VH::XPATH_DVLINKS . '/dv:iiif') + ->validateHasNoneOrOne()->getFirstNode(); + $this->createNodeValidator($iiifNode)->validateHasContentWithUrl(); + } + + /** + * Validates the DFG-Viewer rights. + * + * Validates against the rules of chapter "2.7.2 Unterelemente zu dv:rights" + * + * @return void + */ + protected function validateDvRights(): void + { + $this->createNodeListValidator(VH::XPATH_DVRIGHTS . '/dv:owner') + ->validateHasOne(); + + $this->validateNodeContent(VH::XPATH_DVRIGHTS . '/dv:ownerLogo'); + $this->validateNodeContent(VH::XPATH_DVRIGHTS . '/dv:ownerSiteURL'); + $this->validateNodeContent(VH::XPATH_DVRIGHTS . '/dv:ownerContact'); + + $this->createNodeListValidator(VH::XPATH_DVRIGHTS . '/dv:aggregator')->validateHasNoneOrOne(); + $this->validateNodeContent(VH::XPATH_DVRIGHTS . '/dv:aggregatorLogo', true); + $this->validateNodeContent(VH::XPATH_DVRIGHTS . '/dv:aggregatorSiteURL', true); + + $this->createNodeListValidator(VH::XPATH_DVRIGHTS . '/dv:sponsor')->validateHasNoneOrOne(); + $this->validateNodeContent(VH::XPATH_DVRIGHTS . '/dv:sponsorLogo', true); + $this->validateNodeContent(VH::XPATH_DVRIGHTS . '/dv:sponsorSiteURL', true); + + $licenseNode = $this->createNodeListValidator(VH::XPATH_DVRIGHTS . '/dv:license') + ->validateHasNoneOrOne() + ->getFirstNode(); + if ($licenseNode && !in_array($licenseNode->nodeValue, ['pdm', 'cc0', 'cc-by', 'cc-by-sa', 'cc-by-nd', 'cc-by-nc', 'cc-by-nc-sa', 'cc-by-nc-nd', 'reserved'])) { + $this->createNodeValidator($licenseNode)->validateHasContentWithUrl(); + } + } + + /** + * Validates the reference. + * + * Validates against the rules of chapter "2.7.4.1 Katalog- bzw. Findbuchnachweis – dv:reference" + * + * @param \DOMNode $reference + * @return void + */ + protected function validateReference(\DOMNode $reference): void + { + if ($this->xpath->query('dv:reference', $reference->parentNode)->length > 1) { + $this->createNodeValidator($reference) + ->validateHasAttribute('linktext'); + } + } + + private function validateNodeContent(string $expression, bool $optional=false): void + { + $nodeListValidator = $this->createNodeListValidator($expression); + + if ($optional) { + $nodeListValidator + ->validateHasNoneOrOne(); + } else { + $nodeListValidator + ->validateHasOne(); + } + + $node = $nodeListValidator->getFirstNode(); + if (!isset($node)) { + return; + } + + $nodeValidator = $this->createNodeValidator($node); + if (str_starts_with(strtolower($node->nodeValue), 'mailto:')) { + $nodeValidator->validateHasContentWithEmail(); + } else { + $nodeValidator->validateHasContentWithUrl(); + } + } +} diff --git a/Classes/Validation/Mets/AdministrativeMetadataValidator.php b/Classes/Validation/Mets/AdministrativeMetadataValidator.php new file mode 100644 index 000000000..9175b90ad --- /dev/null +++ b/Classes/Validation/Mets/AdministrativeMetadataValidator.php @@ -0,0 +1,164 @@ + + * All rights reserved + * + * This script is part of the TYPO3 project. The TYPO3 project is + * free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * The GNU General Public License can be found at + * http://www.gnu.org/copyleft/gpl.html. + * + * This script is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * This copyright notice MUST APPEAR in all copies of the script! + */ + +use Slub\Dfgviewer\Common\ValidationHelper as VH; +use Slub\Dfgviewer\Validation\AbstactDomDocumentValidator; + +/** + * The validator validates against the rules outlined in chapter 2.6 of the METS application profile 2.3.1. + * + * @package TYPO3 + * @subpackage dfg-viewer + * + * @access public + */ +class AdministrativeMetadataValidator extends AbstactDomDocumentValidator +{ + public function isValidDocument(): void + { + // Validates against the rules of chapter "2.6.1 Metadatensektion – mets:amdSec" + $amdSections = $this->createNodeListValidator(VH::XPATH_ADMINISTRATIVE_METADATA) + ->validateHasAny() + ->getNodeList(); + foreach ($amdSections as $amdSection) { + $this->validateAdministrativMetadataNode($amdSection); + } + + // Check if one administrative metadata exist with "mets:rightsMD" and "mets:digiprovMD" as children + $this->createNodeListValidator(VH::XPATH_ADMINISTRATIVE_METADATA . '[mets:rightsMD and mets:digiprovMD]') + ->validateHasOne(); + + $this->validateTechnicalMetadata(); + $this->validateRightsMetadata(); + $this->validateDigitalProvenanceMetadata(); + } + + protected function validateAdministrativMetadataNode(\DOMNode $amdSection): void + { + $this->createNodeValidator($amdSection) + ->validateHasUniqueId(); + } + + /** + * Validates the digital provenance metadata. + * + * Validates against the rules of chapters "2.6.2.5 Herstellung – mets:digiprovMD" and "2.6.2.6 Eingebettete Verweise – mets:digiprovMD/mets:mdWrap" + * + * @return void + */ + protected function validateDigitalProvenanceMetadata(): void + { + $digiprovs = $this->createNodeListValidator(VH::XPATH_ADMINISTRATIVE_DIGIPROV_METADATA) + ->getNodeList(); + foreach ($digiprovs as $digiprov) { + $this->validateDigitalProvenanceMetadataNode($digiprov); + } + } + + protected function validateDigitalProvenanceMetadataNode(\DOMNode $digiprov): void + { + $this->createNodeValidator($digiprov) + ->validateHasUniqueId(); + + $mdWrap = $this->createNodeListValidator('mets:mdWrap', $digiprov) + ->validateHasOne() + ->getFirstNode(); + + $this->createNodeValidator($mdWrap) + ->validateHasAttributeWithValue('MDTYPE', ['OTHER']) + ->validateHasAttributeWithValue('OTHERMDTYPE', ['DVLINKS']); + + $this->createNodeListValidator('mets:xmlData[dv:links]', $mdWrap) + ->validateHasOne(); + } + + /** + * Validates the rights metadata. + * + * Validates against the rules of chapters "2.6.2.4 Rechtedeklaration – mets:rightsMD" and "2.6.2.4 Eingebettete Rechteangaben – mets:rightsMD/mets:mdWrap" + * + * @return void + */ + protected function validateRightsMetadata(): void + { + $rightsMetadata = $this->createNodeListValidator(VH::XPATH_ADMINISTRATIVE_RIGHTS_METADATA) + ->getNodeList(); + foreach ($rightsMetadata as $rightsMetadataNode) { + $this->validateRightsMetadataNode($rightsMetadataNode); + } + } + + protected function validateRightsMetadataNode(\DOMNode $rightsMetadata): void + { + $this->createNodeValidator($rightsMetadata) + ->validateHasUniqueId(); + + $mpWrap = $this->createNodeListValidator('mets:mdWrap', $rightsMetadata) + ->validateHasOne() + ->getFirstNode(); + + $this->createNodeValidator($mpWrap) + ->validateHasAttributeWithValue('MDTYPE', ['OTHER']) + ->validateHasAttributeWithValue('OTHERMDTYPE', ['DVRIGHTS']); + + $this->createNodeListValidator('mets:xmlData[dv:rights]', $mpWrap) + ->validateHasOne(); + } + + /** + * Validates the technical metadata. + * + * Validates against the rules of chapters "2.6.2.1 Technische Metadaten – mets:techMD" and "2.6.2.2 Eingebettete technische Daten – mets:techMD/mets:mdWrap" + * + * @return void + */ + protected function validateTechnicalMetadata(): void + { + $technicalMd = $this->createNodeListValidator(VH::XPATH_ADMINISTRATIVE_TECHNICAL_METADATA) + ->getNodeList(); + foreach ($technicalMd as $technicalMdNode) { + $this->validateTechnicalMetadataNode($technicalMdNode); + } + } + + protected function validateTechnicalMetadataNode(\DOMNode $technicalMd): void + { + $this->createNodeValidator($technicalMd) + ->validateHasUniqueId(); + + $mdWrap = $this->createNodeListValidator('mets:mdWrap', $technicalMd) + ->validateHasOne() + ->getFirstNode(); + + $this->createNodeValidator($mdWrap) + ->validateHasAttribute("MDTYPE") + ->validateHasAttribute("OTHERMDTYPE"); + + $this->createNodeListValidator('mets:xmlData', $mdWrap) + ->validateHasOne(); + } +} diff --git a/Classes/Validation/Mets/DescriptiveMetadataValidator.php b/Classes/Validation/Mets/DescriptiveMetadataValidator.php new file mode 100644 index 000000000..787a503e7 --- /dev/null +++ b/Classes/Validation/Mets/DescriptiveMetadataValidator.php @@ -0,0 +1,91 @@ + + * All rights reserved + * + * This script is part of the TYPO3 project. The TYPO3 project is + * free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * The GNU General Public License can be found at + * http://www.gnu.org/copyleft/gpl.html. + * + * This script is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * This copyright notice MUST APPEAR in all copies of the script! + */ + +use Slub\Dfgviewer\Common\ValidationHelper as VH; +use Slub\Dfgviewer\Validation\AbstactDomDocumentValidator; + +/** + * The validator validates against the rules outlined in chapter 2.5 of the METS application profile 2.3.1. + * + * @package TYPO3 + * @subpackage dfg-viewer + * + * @access public + */ +class DescriptiveMetadataValidator extends AbstactDomDocumentValidator +{ + public function isValidDocument(): void + { + // Validates against the rules of chapter "2.5.1 Metadatensektion – mets:dmdSec" + $descriptiveSections = $this->createNodeListValidator(VH::XPATH_DESCRIPTIVE_METADATA_SECTIONS) + ->validateHasAny() + ->getNodeList(); + foreach ($descriptiveSections as $descriptiveSection) { + $this->validateDescriptiveMetadataSection($descriptiveSection); + } + + // there must be one primary structural element + $structureElement = $this->createNodeListValidator(VH::XPATH_LOGICAL_STRUCTURAL_ELEMENTS) + ->validateHasOne() + ->getFirstNode(); + + $this->createNodeValidator($structureElement) + ->validateHasReferenceToId('DMDID', VH::XPATH_DESCRIPTIVE_METADATA_SECTIONS); + } + + /** + * Validates the embedded metadata. + * + * Validates against the rules of chapter "2.5.2.1 Eingebettete Metadaten – mets:mdWrap" + * + * @return void + */ + protected function validateDescriptiveMetadataSection(\DOMNode $descriptiveSection): void + { + $mdWrap = $this->createNodeListValidator('mets:mdWrap', $descriptiveSection) + ->validateHasOne() + ->getFirstNode(); + + $this->createNodeValidator($mdWrap) + ->validateHasAttributeWithValue('MDTYPE', ['MODS', 'TEIHDR']); + + if (!$mdWrap) { + return; + } + + // @phpstan-ignore-next-line + $mdType = $mdWrap->getAttribute('MDTYPE'); + if ($mdType == 'TEIHDR' || $mdType == 'MODS') { + $childNode = 'mods:mods'; + if ($mdType == 'TEIHDR') { + $childNode = 'tei:teiHeader'; + } + $this->createNodeListValidator('mets:xmlData[' . $childNode . ']', $mdWrap) + ->validateHasOne(); + } + } +} diff --git a/Classes/Validation/Mets/DigitalRepresentationValidator.php b/Classes/Validation/Mets/DigitalRepresentationValidator.php new file mode 100644 index 000000000..d8a51934b --- /dev/null +++ b/Classes/Validation/Mets/DigitalRepresentationValidator.php @@ -0,0 +1,116 @@ + + * All rights reserved + * + * This script is part of the TYPO3 project. The TYPO3 project is + * free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * The GNU General Public License can be found at + * http://www.gnu.org/copyleft/gpl.html. + * + * This script is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * This copyright notice MUST APPEAR in all copies of the script! + */ + +use Slub\Dfgviewer\Common\ValidationHelper as VH; +use Slub\Dfgviewer\Validation\AbstactDomDocumentValidator; + +/** + * The validator validates against the rules outlined in chapter 2.4 of the METS application profile 2.3.1. + * + * @package TYPO3 + * @subpackage dfg-viewer + * + * @access public + */ +class DigitalRepresentationValidator extends AbstactDomDocumentValidator +{ + public function isValidDocument(): void + { + // Validates against the rules of chapter "2.4.1 Dateisektion – mets:fileSec" + $this->createNodeListValidator(VH::XPATH_FILE_SECTIONS) + ->validateHasNoneOrOne(); + + // If a physical structure is present, there must be one file section. + if ($this->xpath->query(VH::XPATH_PHYSICAL_STRUCTURES)->length > 0) { + $this->createNodeListValidator(VH::XPATH_FILE_SECTIONS) + ->validateHasOne(); + } + + if ($this->xpath->query(VH::XPATH_FILE_SECTIONS)->length > 0) { + $this->validateFileGroups(); + $this->validateFiles(); + } + } + + /** + * Validates the file groups. + * + * Validates against the rules of chapter "2.4.2.1 Dateigruppen – mets:fileGrp" + * + * @return void + */ + protected function validateFileGroups(): void + { + $fileSectionGroups = $this->createNodeListValidator(VH::XPATH_FILE_SECTION_GROUPS) + ->validateHasAny() + ->getNodeList(); + foreach ($fileSectionGroups as $fileSectionGroup) { + $this->validateFileGroup($fileSectionGroup); + } + + $this->createNodeListValidator(VH::XPATH_FILE_SECTION_GROUPS . '[@USE="DEFAULT"]') + ->validateHasOne(); + } + + protected function validateFileGroup(\DOMNode $fileGroup): void + { + $this->createNodeValidator($fileGroup) + ->validateHasUniqueAttribute("USE", VH::XPATH_FILE_SECTION_GROUPS); + } + + /** + * Validates the files. + * + * Validates against the rules of chapters "2.4.2.2 Datei – mets:fileGrp/mets:file" and "2.4.2.3 Dateilink – mets:fileGrp/mets:file/mets:FLocat" + * + * @return void + */ + protected function validateFiles(): void + { + $files = $this->createNodeListValidator(VH::XPATH_FILE_SECTION_FILES) + ->validateHasAny() + ->getNodeList(); + foreach ($files as $file) { + $this->validateFile($file); + } + } + + protected function validateFile(\DOMNode $file): void + { + $this->createNodeValidator($file) + ->validateHasUniqueId() + ->validateHasAttribute('MIMETYPE'); + + $fLocat = $this->createNodeListValidator('mets:FLocat', $file) + ->validateHasOne() + ->getFirstNode(); + + $this->createNodeValidator($fLocat) + ->validateHasAttributeWithValue('LOCTYPE', ['URL', 'PURL']) + ->validateHasAttributeWithUrl('xlink:href'); + } +} diff --git a/Classes/Validation/Mets/LinkingLogicalPhysicalStructureValidator.php b/Classes/Validation/Mets/LinkingLogicalPhysicalStructureValidator.php new file mode 100644 index 000000000..ae50de37a --- /dev/null +++ b/Classes/Validation/Mets/LinkingLogicalPhysicalStructureValidator.php @@ -0,0 +1,73 @@ + + * All rights reserved + * + * This script is part of the TYPO3 project. The TYPO3 project is + * free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * The GNU General Public License can be found at + * http://www.gnu.org/copyleft/gpl.html. + * + * This script is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * This copyright notice MUST APPEAR in all copies of the script! + */ + +use Slub\Dfgviewer\Common\ValidationHelper as VH; +use Slub\Dfgviewer\Validation\AbstactDomDocumentValidator; + +/** + * The validator validates against the rules outlined in chapter 2.3 of the METS application profile 2.3.1. + * + * @package TYPO3 + * @subpackage dfg-viewer + * + * @access public + */ +class LinkingLogicalPhysicalStructureValidator extends AbstactDomDocumentValidator +{ + public function isValidDocument(): void + { + // Validates against the rules of chapter "2.3.1 Structure links - mets:structLink" + $this->createNodeListValidator(VH::XPATH_STRUCT_LINK) + ->validateHasNoneOrOne(); + + $this->validateLinkElements(); + } + + /** + * Validates the linking elements. + * + * Validates against the rules of chapter "2.3.2.1 Linking – mets:smLink" + * + * @return void + */ + protected function validateLinkElements(): void + { + $linkElements = $this->createNodeListValidator(VH::XPATH_STRUCT_LINK_ELEMENTS) + ->validateHasAny() + ->getNodeList(); + foreach ($linkElements as $linkElement) { + $this->validateLinkElement($linkElement); + } + } + + protected function validateLinkElement(\DOMNode $linkElement): void + { + $this->createNodeValidator($linkElement) + ->validateHasReferenceToId("xlink:from", VH::XPATH_LOGICAL_STRUCTURES) + ->validateHasReferenceToId("xlink:to", VH::XPATH_PHYSICAL_STRUCTURES); + } +} diff --git a/Classes/Validation/Mets/LogicalStructureValidator.php b/Classes/Validation/Mets/LogicalStructureValidator.php new file mode 100644 index 000000000..e2e5194b4 --- /dev/null +++ b/Classes/Validation/Mets/LogicalStructureValidator.php @@ -0,0 +1,110 @@ + + * All rights reserved + * + * This script is part of the TYPO3 project. The TYPO3 project is + * free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * The GNU General Public License can be found at + * http://www.gnu.org/copyleft/gpl.html. + * + * This script is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * This copyright notice MUST APPEAR in all copies of the script! + */ + +use Slub\Dfgviewer\Common\ValidationHelper as VH; +use Slub\Dfgviewer\Validation\AbstactDomDocumentValidator; + +/** + * The validator validates against the rules outlined in chapter 2.1 of the METS application profile 2.3.1. + * + * @package TYPO3 + * @subpackage dfg-viewer + * + * @access public + */ +class LogicalStructureValidator extends AbstactDomDocumentValidator +{ + public function isValidDocument(): void + { + // Validates against the rules of chapter "2.1.1 Logical structure - mets:structMap" + $this->createNodeListValidator(VH::XPATH_LOGICAL_STRUCTURES) + ->validateHasAny(); + + $this->validateStructuralElements(); + $this->validateExternalReferences(); + $this->validatePeriodicPublishingSequences(); + } + + /** + * Validates the structural elements. + * + * Validates against the rules of chapter "2.1.2.1 Structural element - mets:div" + * + * @return void + */ + protected function validateStructuralElements(): void + { + $structuralElements = $this->createNodeListValidator(VH::XPATH_LOGICAL_STRUCTURAL_ELEMENTS) + ->validateHasAny() + ->getNodeList(); + foreach ($structuralElements as $structuralElement) { + $this->validateStructuralElement($structuralElement); + } + } + + protected function validateStructuralElement(\DOMNode $structureElement): void + { + $this->createNodeValidator($structureElement) + ->validateHasUniqueId() + ->validateHasAttributeWithValue("TYPE", VH::STRUCTURE_DATASET); + } + + /** + * Validates the external references. + * + * Validates against the rules of chapter "2.1.2.2 Reference to external METS-files - mets:div / mets:mptr" + * + * @return void + */ + protected function validateExternalReferences(): void + { + $externalReferences = $this->createNodeListValidator(VH::XPATH_LOGICAL_EXTERNAL_REFERENCES) + ->validateHasNoneOrOne() + ->getNodeList(); + foreach ($externalReferences as $externalReference) { + $this->validateExternalReference($externalReference); + } + } + + protected function validateExternalReference(\DOMNode $externalReference): void + { + $this->createNodeValidator($externalReference) + ->validateHasAttributeWithValue("LOCTYPE", ["URL", "PURL"]) + ->validateHasAttributeWithUrl("xlink:href"); + } + + /** + * Validates the periodic publishing sequences. + * + * Validates against the rules of chapter "2.1.3 Periodic publishing sequences" + * + * @return void + */ + protected function validatePeriodicPublishingSequences(): void + { + } +} diff --git a/Classes/Validation/Mets/PhysicalStructureValidator.php b/Classes/Validation/Mets/PhysicalStructureValidator.php new file mode 100644 index 000000000..feed4e351 --- /dev/null +++ b/Classes/Validation/Mets/PhysicalStructureValidator.php @@ -0,0 +1,81 @@ + + * All rights reserved + * + * This script is part of the TYPO3 project. The TYPO3 project is + * free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * The GNU General Public License can be found at + * http://www.gnu.org/copyleft/gpl.html. + * + * This script is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * This copyright notice MUST APPEAR in all copies of the script! + */ + +use Slub\Dfgviewer\Common\ValidationHelper as VH; +use Slub\Dfgviewer\Validation\AbstactDomDocumentValidator; + +/** + * The validator validates against the rules outlined in chapter 2.2 of the METS application profile 2.3.1. + * + * @package TYPO3 + * @subpackage dfg-viewer + * + * @access public + */ +class PhysicalStructureValidator extends AbstactDomDocumentValidator +{ + public function isValidDocument(): void + { + // Validates against the rules of chapter "2.2.1 Physical structure - mets:structMap" + $this->createNodeListValidator(VH::XPATH_PHYSICAL_STRUCTURES) + ->validateHasNoneOrOne(); + + $this->validateStructuralElements(); + } + + /** + * + * Validates the structural elements. + * + * Validates against the rules of chapter "2.2.2.1 Structural element - mets:div" + * + * @return void + */ + protected function validateStructuralElements(): void + { + $node = $this->createNodeListValidator(VH::XPATH_PHYSICAL_STRUCTURAL_ELEMENT_SEQUENCE) + ->validateHasOne() + ->getFirstNode(); + + $this->createNodeValidator($node) + ->validateHasAttributeWithValue('TYPE', ['physSequence']); + + $structuralElements = $this->createNodeListValidator(VH::XPATH_PHYSICAL_STRUCTURAL_ELEMENTS) + ->validateHasAny() + ->getNodeList(); + foreach ($structuralElements as $structuralElement) { + $this->validateStructuralElement($structuralElement); + } + } + + protected function validateStructuralElement(\DOMNode $structureElement): void + { + $this->createNodeValidator($structureElement) + ->validateHasUniqueId() + ->validateHasAttributeWithValue("TYPE", ["page", "doublepage", "track"]); + } +} diff --git a/Configuration/TypoScript/Plugins/kitodo.typoscript b/Configuration/TypoScript/Plugins/kitodo.typoscript index bf2b3cf0a..eb5a7f10e 100644 --- a/Configuration/TypoScript/Plugins/kitodo.typoscript +++ b/Configuration/TypoScript/Plugins/kitodo.typoscript @@ -28,6 +28,31 @@ plugin.tx_dlf { } settings { storagePid = {$plugin.tx_dlf.persistence.storagePid} + domDocumentValidationValidators { + 10 { + title = XML-Schemes Validator + className = Kitodo\Dlf\Validation\XmlSchemesValidator + breakOnError = false + configuration { + oai { + namespace = http://www.openarchives.org/OAI/2.0/ + schemaLocation = https://www.openarchives.org/OAI/2.0/OAI-PMH.xsd + } + mets { + namespace = http://www.loc.gov/METS/ + schemaLocation = http://www.loc.gov/standards/mets/mets.xsd + } + mods { + namespace = http://www.loc.gov/mods/v3 + schemaLocation = http://www.loc.gov/standards/mods/mods.xsd + } + } + } + 20 { + title = Application Profile Validation + className = Slub\Dfgviewer\Validation\ApplicationProfileValidationStack + } + } } view { partialRootPaths { diff --git a/Tests/Fixtures/mets.xml b/Tests/Fixtures/mets.xml new file mode 100644 index 000000000..2d1193f71 --- /dev/null +++ b/Tests/Fixtures/mets.xml @@ -0,0 +1,131 @@ + + + + + + + + "INDIA" + + + none + + + "tecnoarcadia" + + aut + + + + none + + + https://zenodo.org/api/files/df31cc16-0a54-4599-89e9-0dd4ada4016d/b5df7cd550f64e818943ad96fff7e902.glb + + prv + + + + + + + + + + + + + + none + + + + + + + + cre + + + + 0 + 0 + + + + "not available" + none + none + none + + + none + CC-BY + + + INDIA + + + + + + + + + + + 3D Repository + http://dfg-viewer.de/fileadmin/_processed_/a/8/csm_HSM_Logo_T_schwarz_klein_bold_regular_d61a371993.jpg + https://3d-repository.hs-mainz.de/ + https://3d-repository.hs-mainz.de/contact + Aggregator + http://dfg-viewer.de/fileadmin/_processed_/a/8/csm_HSM_Logo_T_schwarz_klein_bold_regular_d61a371993.jpg + https://3d-repository.hs-mainz.de/ + Sponsor + http://dfg-viewer.de/fileadmin/_processed_/a/8/csm_HSM_Logo_T_schwarz_klein_bold_regular_d61a371993.jpg + https://3d-repository.hs-mainz.de/ + pdm + + + + + + + + + http://slub-dresden.de/FOZK.pl?PPN=356448053 + http://slub-dresden.de/356448053 + http://digital.slub-dresden.de/sru/356448053 + http://digital.slub-dresden.de/iiif/356448053.xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/Unit/Validation/AbstractDomDocumentValidatorTest.php b/Tests/Unit/Validation/AbstractDomDocumentValidatorTest.php new file mode 100644 index 000000000..db5986431 --- /dev/null +++ b/Tests/Unit/Validation/AbstractDomDocumentValidatorTest.php @@ -0,0 +1,363 @@ + + * All rights reserved + * + * This script is part of the TYPO3 project. The TYPO3 project is + * free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * The GNU General Public License can be found at + * http://www.gnu.org/copyleft/gpl.html. + * + * This script is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * This copyright notice MUST APPEAR in all copies of the script! + */ + +use DOMDocument; +use DOMElement; +use DOMXPath; +use Kitodo\Dlf\Validation\AbstractDlfValidator; +use TYPO3\CMS\Extbase\Error\Result; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; + +abstract class AbstractDomDocumentValidatorTest extends UnitTestCase +{ + + /** + * @var AbstractDlfValidator + */ + protected $validator; + + /** + * @var DOMDocument + */ + protected $doc; + + abstract protected function createValidator(): AbstractDlfValidator; + + public function setUp(): void + { + parent::setUp(); + $this->resetSingletonInstances = true; + $this->doc = $this->getDomDocument(); + $this->validator = $this->createValidator(); + } + + /** + * Validates the document using the created validator. + * + * @return void + */ + public function testDocument() + { + $this->hasNoError(); + } + + /** + * Validates using validator and DOMDocument + * + * @return Result + */ + protected function validate(): Result + { + return $this->validator->validate($this->doc); + } + + /** + * Validates using validator and DOMDocument and assert result error message for equality. + * + * Validates using a validator and DOMDocument, then asserts that the resulting error message matches the expected value. + * + * @param $message string + * @return void + */ + protected function validateAndAssertEquals(string $message): void + { + $result = $this->validator->validate($this->doc); + self::assertEquals($message, $result->getFirstError()->getMessage()); + } + + /** + * Reset the document. + * + * @return void + */ + protected function resetDocument(): void + { + $this->doc = $this->getDomDocument(); + } + + /** + * Add child node with name and namespace to DOMDocument. + * + * @param string $expression + * @param string $namespace + * @param string $name + * @return void + * @throws \DOMException + */ + protected function addChildNodeWithNamespace(string $expression, string $namespace, string $name): void + { + $this->addChildNode($expression, $this->doc->createElementNS($namespace, $name)); + } + + /** + * Add node as child node to DOMDocument. + * + * @param string $expression + * @param DOMElement $newNode + * @return void + */ + protected function addChildNode(string $expression, DOMElement $newNode): void + { + $xpath = new DOMXPath($this->doc); + foreach ($xpath->evaluate($expression) as $node) { + $node->appendChild($newNode); + } + } + + /** + * Remove notes found by node expression in DOMDocument. + * + * @param string $expression + * @return void + */ + protected function removeNodes(string $expression): void + { + $xpath = new DOMXPath($this->doc); + foreach ($xpath->query($expression) as $node) { + $node->parentNode->removeChild($node); + } + } + + /** + * Set value of attribute found by node expression in DOMDocument. + * + * @param string $expression + * @param string $attribute + * @param string $value + * @return void + */ + protected function setAttributeValue(string $expression, string $attribute, string $value): void + { + $xpath = new DOMXPath($this->doc); + foreach ($xpath->evaluate($expression) as $node) { + $node->setAttribute($attribute, $value); + } + } + + /** + * Remove attribute found by node expression in DOMDocument. + * + * @param string $expression + * @param string $attribute + * @return void + */ + protected function removeAttribute(string $expression, string $attribute): void + { + $xpath = new DOMXPath($this->doc); + foreach ($xpath->evaluate($expression) as $node) { + $node->removeAttribute($attribute); + } + } + + /** + * Set value of content found by node expression in DOMDocument. + * + * @param string $expression + * @param string $value + * @return void + */ + protected function setContentValue(string $expression, string $value): void + { + $xpath = new DOMXPath($this->doc); + foreach ($xpath->evaluate($expression) as $node) { + $node->nodeValue = $value; + } + } + + /** + * Gets the doc from xml file. + * + * @return DOMDocument + */ + protected function getDomDocument(): DOMDocument + { + $doc = new DOMDocument(); + $doc->load(__DIR__ . '/../../Fixtures/mets.xml'); + self::assertNotFalse($doc); + return $doc; + } + + /** + * Assert validation has no error. + * + * @return void + */ + protected function hasNoError(): void + { + $result = $this->validate(); + $this->assertFalse($result->hasErrors()); + } + + /** + * Assert error of has any validation. + * + * @param string $expression The expression in error message + * @param string $context The context in error message + * @return void + */ + protected function hasErrorAny(string $expression, string $context=''): void + { + $message = 'There must be at least one element that matches the XPath expression "' . $expression . '"'; + if ($context != '') { + $message .= ' under "' . $context . '"'; + } + $this->validateAndAssertEquals($message); + } + + /** + * Assert error of has one validation. + * + * @param string $expression The expression in error message + * @param string $context The context in error message + * @return void + */ + protected function hasErrorOne(string $expression, string $context=''): void + { + $message = 'There must be an element that matches the XPath expression "' . $expression . '"'; + if ($context != '') { + $message .= ' under "' . $context . '"'; + } + $this->validateAndAssertEquals($message); + } + + /** + * Assert error of has none or one validation. + * + * @param string $expression The expression in error message + * @param string $context The context in error message + * @return void + */ + protected function hasErrorNoneOrOne(string $expression, string $context=''): void + { + $message = 'There must be no more than one element that matches the XPath expression "' . $expression . '"'; + if ($context != '') { + $message .= ' under "' . $context . '"'; + } + $this->validateAndAssertEquals($message); + } + + /** + * Assert error of has attribute validation. + * + * @param string $expression The expression in error message + * @param string $name The attribute name + * @return void + */ + protected function hasErrorAttribute(string $expression, string $name): void + { + $this->validateAndAssertEquals('Mandatory "' . $name . '" attribute of "' . $expression . '" is missing.'); + } + + /** + * Assert error of has attribute with value validation. + * + * @param string $expression The expression in error message + * @param string $name The attribute name + * @param string $value The attribute value + * @return void + */ + protected function hasErrorAttributeWithValue(string $expression, string $name, string $value): void + { + $this->validateAndAssertEquals('Value "' . $value . '" in the "' . $name . '" attribute of "' . $expression . '" is not permissible.'); + } + + /** + * Assert error of has attribute with URL value validation. + * + * @param string $expression The expression in error message + * @param string $name The attribute name + * @param string $value The attribute value + * @return void + */ + protected function hasErrorAttributeWithUrl(string $expression, string $name, string $value): void + { + $this->validateAndAssertEquals('URL "' . $value . '" in the "' . $name . '" attribute of "' . $expression . '" is not valid.'); + } + + /** + * Assert error of has attribute reference to one validation. + * + * @param string $expression The expression in error message + * @param string $name The attribute name + * @param string $value The attribute value + * @param string $targetExpression The target context expression + * @return void + */ + protected function hasErrorAttributeRefToOne(string $expression, string $name, string $value, string $targetExpression): void + { + $this->validateAndAssertEquals('Value "' . $value . '" in the "' . $name . '" attribute of "' . $expression . '" must reference one element under XPath expression "' . $targetExpression); + } + + /** + * Assert error of has content with Email validation. + * + * @param string $expression The expression in error message + * @param string $value The content value + * @return void + */ + protected function hasErrorContentWithEmail(string $expression, string $value): void + { + $this->validateAndAssertEquals('Email "' . $value . '" in the content of "' . $expression . '" is not valid.'); + } + + /** + * Assert error of has content with URL. + * + * @param string $expression The expression in error message + * @param string $value The content value + * @return void + */ + protected function hasErrorContentWithUrl(string $expression, string $value): void + { + $this->validateAndAssertEquals('URL "' . $value . '" in the content of "' . $expression . '" is not valid.'); + } + + /** + * Assert error of has unique identifier. + * + * @param string $expression The expression in error message + * @param string $value The attribute value + * @return void + */ + protected function hasErrorUniqueId(string $expression, string $value): void + { + $this->hasErrorUniqueAttribute($expression, 'ID', $value); + } + + /** + * Assert error of has unique attribute with value. + * + * @param string $expression The expression in error message + * @param string $name The attribute name + * @param string $value The attribute value + * @return void + */ + protected function hasErrorUniqueAttribute(string $expression, string $name, string $value): void + { + $this->validateAndAssertEquals('"' . $name . '" attribute with value "' . $value . '" of "' . $expression . '" already exists.'); + } +} diff --git a/Tests/Unit/Validation/DvMetadataValidatorTest.php b/Tests/Unit/Validation/DvMetadataValidatorTest.php new file mode 100644 index 000000000..60afb738f --- /dev/null +++ b/Tests/Unit/Validation/DvMetadataValidatorTest.php @@ -0,0 +1,154 @@ + + * All rights reserved + * + * This script is part of the TYPO3 project. The TYPO3 project is + * free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * The GNU General Public License can be found at + * http://www.gnu.org/copyleft/gpl.html. + * + * This script is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * This copyright notice MUST APPEAR in all copies of the script! + */ + +use Kitodo\Dlf\Validation\AbstractDlfValidator; +use Slub\Dfgviewer\Common\ValidationHelper as VH; +use Slub\Dfgviewer\Validation\DvMetadataValidator; + +class DvMetadataValidatorTest extends AbstractDomDocumentValidatorTest +{ + /** + * Test validation against the rules of chapter "2.7.1 Rechteangaben – dv:rights" + * + * @return void + */ + public function testDvRights(): void + { + $this->removeNodes(VH::XPATH_DVRIGHTS); + $this->hasErrorOne(VH::XPATH_DVRIGHTS); + } + + /** + * Test validation against the rules of chapter "2.7.2 Unterelemente zu dv:rights" + * + * @return void + * @throws \DOMException + */ + public function testDvRightsSubelements(): void + { + $this->removeNodes(VH::XPATH_DVRIGHTS . '/dv:owner'); + $this->hasErrorOne(VH::XPATH_DVRIGHTS . '/dv:owner'); + $this->resetDocument(); + + $this->assertNodeContent(VH::XPATH_DVRIGHTS . '/dv:ownerLogo', VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_RIGHTS_METADATA) . '/mets:mdWrap/mets:xmlData/dv:rights/dv:ownerLogo'); + $this->assertNodeContent(VH::XPATH_DVRIGHTS . '/dv:ownerSiteURL', VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_RIGHTS_METADATA) . '/mets:mdWrap/mets:xmlData/dv:rights/dv:ownerSiteURL'); + $this->assertNodeContent(VH::XPATH_DVRIGHTS . '/dv:ownerContact', VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_RIGHTS_METADATA) . '/mets:mdWrap/mets:xmlData/dv:rights/dv:ownerContact'); + $this->setContentValue(VH::XPATH_DVRIGHTS . '/dv:ownerContact', 'mailto:Test'); + $this->hasErrorContentWithEmail(VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_RIGHTS_METADATA) . '/mets:mdWrap/mets:xmlData/dv:rights/dv:ownerContact', 'mailto:Test'); + $this->resetDocument(); + + $this->addChildNodeWithNamespace(VH::XPATH_DVRIGHTS, VH::NAMESPACE_DV, 'dv:aggregator'); + $this->hasErrorNoneOrOne(VH::XPATH_DVRIGHTS . '/dv:aggregator'); + $this->resetDocument(); + $this->assertOptionalNodeContent(VH::XPATH_DVRIGHTS, 'dv:aggregatorLogo', VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_RIGHTS_METADATA) . '/mets:mdWrap/mets:xmlData/dv:rights/dv:aggregatorLogo'); + $this->assertOptionalNodeContent(VH::XPATH_DVRIGHTS, 'dv:aggregatorSiteURL', VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_RIGHTS_METADATA) . '/mets:mdWrap/mets:xmlData/dv:rights/dv:aggregatorSiteURL'); + + $this->addChildNodeWithNamespace(VH::XPATH_DVRIGHTS, VH::NAMESPACE_DV, 'dv:sponsor'); + $this->hasErrorNoneOrOne(VH::XPATH_DVRIGHTS . '/dv:sponsor'); + $this->resetDocument(); + $this->assertOptionalNodeContent(VH::XPATH_DVRIGHTS, 'dv:sponsorLogo', VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_RIGHTS_METADATA) . '/mets:mdWrap/mets:xmlData/dv:rights/dv:sponsorLogo'); + $this->assertOptionalNodeContent(VH::XPATH_DVRIGHTS, 'dv:sponsorSiteURL', VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_RIGHTS_METADATA) . '/mets:mdWrap/mets:xmlData/dv:rights/dv:sponsorSiteURL'); + + $this->setContentValue(VH::XPATH_DVRIGHTS . '/dv:license', 'Test'); + $this->hasErrorContentWithUrl(VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_RIGHTS_METADATA) . '/mets:mdWrap/mets:xmlData/dv:rights/dv:license', 'Test'); + } + + /** + * Test validation against the rules of chapter "2.7.3 Verweise – dv:links" + * + * @return void + */ + public function testDvLinks(): void + { + $this->removeNodes(VH::XPATH_DVLINKS); + $this->hasErrorOne(VH::XPATH_DVLINKS); + } + + /** + * Test validation against the rules of chapter "2.7.4 Unterelemente zu dv:links" + * + * @return void + * @throws \DOMException + */ + public function testDvLinksSubelements(): void + { + $this->removeNodes(VH::XPATH_DVLINKS . '/dv:reference'); + $this->hasErrorAny(VH::XPATH_DVLINKS . '/dv:reference'); + $this->resetDocument(); + + // if there are multiple `dv:references`, the `linktext` attribute must be present. + $this->addChildNodeWithNamespace(VH::XPATH_DVLINKS, VH::NAMESPACE_DV, 'dv:reference'); + $this->hasErrorAttribute(VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_DIGIPROV_METADATA) . '/mets:mdWrap/mets:xmlData/dv:links/dv:reference[1]', 'linktext'); + $this->resetDocument(); + + $this->addChildNodeWithNamespace(VH::XPATH_DVLINKS, VH::NAMESPACE_DV, 'dv:presentation'); + $this->hasErrorNoneOrOne(VH::XPATH_DVLINKS . '/dv:presentation'); + $this->resetDocument(); + + $this->addChildNodeWithNamespace(VH::XPATH_DVLINKS, VH::NAMESPACE_DV, 'dv:sru'); + $this->hasErrorNoneOrOne(VH::XPATH_DVLINKS . '/dv:sru'); + $this->resetDocument(); + + $this->setContentValue(VH::XPATH_DVLINKS . '/dv:sru', 'Test'); + $this->hasErrorContentWithUrl(VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_DIGIPROV_METADATA) . '/mets:mdWrap/mets:xmlData/dv:links/dv:sru', 'Test'); + $this->resetDocument(); + + $this->addChildNodeWithNamespace(VH::XPATH_DVLINKS, VH::NAMESPACE_DV, 'dv:iiif'); + $this->hasErrorNoneOrOne(VH::XPATH_DVLINKS . '/dv:iiif'); + $this->resetDocument(); + + $this->setContentValue(VH::XPATH_DVLINKS . '/dv:iiif', 'Test'); + $this->hasErrorContentWithUrl(VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_DIGIPROV_METADATA) . '/mets:mdWrap/mets:xmlData/dv:links/dv:iiif', 'Test'); + } + + protected function assertNodeContent(string $expression, string $expectedExpression): void + { + $this->removeNodes($expression); + $this->hasErrorOne($expression); + $this->resetDocument(); + + $this->setContentValue($expression, 'Test'); + $this->hasErrorContentWithUrl($expectedExpression, 'Test'); + $this->resetDocument(); + } + + protected function assertOptionalNodeContent(string $expression, string $name, string $expectedExpression): void + { + $this->addChildNodeWithNamespace($expression, VH::NAMESPACE_DV, $name); + $this->hasErrorNoneOrOne($expression . '/' . $name); + $this->resetDocument(); + + $this->setContentValue($expression . '/' . $name, 'Test'); + $this->hasErrorContentWithUrl($expectedExpression, 'Test'); + $this->resetDocument(); + } + + protected function createValidator(): AbstractDlfValidator + { + return new DvMetadataValidator(); + } +} diff --git a/Tests/Unit/Validation/Mets/AdministrativeMetadataValidatorTest.php b/Tests/Unit/Validation/Mets/AdministrativeMetadataValidatorTest.php new file mode 100644 index 000000000..e470c09b9 --- /dev/null +++ b/Tests/Unit/Validation/Mets/AdministrativeMetadataValidatorTest.php @@ -0,0 +1,153 @@ + + * All rights reserved + * + * This script is part of the TYPO3 project. The TYPO3 project is + * free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * The GNU General Public License can be found at + * http://www.gnu.org/copyleft/gpl.html. + * + * This script is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * This copyright notice MUST APPEAR in all copies of the script! + */ + +use Kitodo\Dlf\Validation\AbstractDlfValidator; +use Slub\Dfgviewer\Common\ValidationHelper as VH; +use Slub\Dfgviewer\Validation\Mets\AdministrativeMetadataValidator; + +class AdministrativeMetadataValidatorTest extends AbstractDomDocumentValidatorTest +{ + /** + * Test validation against the rules of chapter "2.6.1 Metadatensektion – mets:amdSec" + * + * @return void + */ + public function testAdministrativeMetadata(): void + { + $this->removeNodes(VH::XPATH_ADMINISTRATIVE_METADATA); + $this->hasErrorAny(VH::XPATH_ADMINISTRATIVE_METADATA); + $this->resetDocument(); + + $this->removeNodes(VH::XPATH_ADMINISTRATIVE_METADATA . '/mets:rightsMD'); + $this->hasErrorOne(VH::XPATH_ADMINISTRATIVE_METADATA . '[mets:rightsMD and mets:digiprovMD]'); + $this->resetDocument(); + + $this->setAttributeValue(VH::XPATH_ADMINISTRATIVE_METADATA, 'ID', 'DMDLOG_0001'); + $this->hasErrorUniqueId(VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_METADATA), 'DMDLOG_0001'); + + $this->removeAttribute(VH::XPATH_ADMINISTRATIVE_METADATA, 'ID'); + $this->hasErrorAttribute(VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_METADATA), 'ID'); + } + + /** + * Test validation against the rules of chapters "2.6.2.5 Herstellung – mets:digiprovMD" and "2.6.2.6 Eingebettete Verweise – mets:digiprovMD/mets:mdWrap" + * + * @return void + */ + public function testDigitalProvenanceMetadataStructure(): void + { + $this->setAttributeValue(VH::XPATH_ADMINISTRATIVE_DIGIPROV_METADATA, 'ID', 'DMDLOG_0001'); + $this->hasErrorUniqueId(VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_DIGIPROV_METADATA), 'DMDLOG_0001'); + $this->resetDocument(); + + $this->removeNodes(VH::XPATH_ADMINISTRATIVE_DIGIPROV_METADATA . '/mets:mdWrap'); + $this->hasErrorOne('mets:mdWrap', VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_DIGIPROV_METADATA)); + $this->resetDocument(); + + $this->removeAttribute(VH::XPATH_ADMINISTRATIVE_DIGIPROV_METADATA . '/mets:mdWrap', 'MDTYPE'); + $this->hasErrorAttribute(VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_DIGIPROV_METADATA) . '/mets:mdWrap', 'MDTYPE'); + + $this->setAttributeValue(VH::XPATH_ADMINISTRATIVE_DIGIPROV_METADATA . '/mets:mdWrap', 'MDTYPE', 'Test'); + $this->hasErrorAttributeWithValue(VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_DIGIPROV_METADATA) . '/mets:mdWrap', 'MDTYPE', 'Test'); + $this->resetDocument(); + + $this->removeAttribute(VH::XPATH_ADMINISTRATIVE_DIGIPROV_METADATA . '/mets:mdWrap', 'OTHERMDTYPE'); + $this->hasErrorAttribute(VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_DIGIPROV_METADATA) . '/mets:mdWrap', 'OTHERMDTYPE'); + + $this->setAttributeValue(VH::XPATH_ADMINISTRATIVE_DIGIPROV_METADATA . '/mets:mdWrap', 'OTHERMDTYPE', 'Test'); + $this->hasErrorAttributeWithValue(VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_DIGIPROV_METADATA) . '/mets:mdWrap', 'OTHERMDTYPE', 'Test'); + $this->resetDocument(); + + $this->removeNodes(VH::XPATH_ADMINISTRATIVE_DIGIPROV_METADATA . '/mets:mdWrap/mets:xmlData/dv:links'); + $this->hasErrorOne('mets:xmlData[dv:links]', VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_DIGIPROV_METADATA) . '/mets:mdWrap'); + } + + /** + * Test validation against the rules of chapters "2.6.2.4 Rechtedeklaration – mets:rightsMD" and "2.6.2.4 Eingebettete Rechteangaben – mets:rightsMD/mets:mdWrap" + * + * @return void + */ + public function testRightsMetadataStructure(): void + { + $this->setAttributeValue(VH::XPATH_ADMINISTRATIVE_RIGHTS_METADATA, 'ID', 'DMDLOG_0001'); + $this->hasErrorUniqueId(VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_RIGHTS_METADATA), 'DMDLOG_0001'); + $this->resetDocument(); + + $this->removeNodes(VH::XPATH_ADMINISTRATIVE_RIGHTS_METADATA . '/mets:mdWrap'); + $this->hasErrorOne('mets:mdWrap', VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_RIGHTS_METADATA)); + $this->resetDocument(); + + $this->removeAttribute(VH::XPATH_ADMINISTRATIVE_RIGHTS_METADATA . '/mets:mdWrap', 'MDTYPE'); + $this->hasErrorAttribute(VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_RIGHTS_METADATA) . '/mets:mdWrap', 'MDTYPE'); + + $this->setAttributeValue(VH::XPATH_ADMINISTRATIVE_RIGHTS_METADATA . '/mets:mdWrap', 'MDTYPE', 'Test'); + $this->hasErrorAttributeWithValue(VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_RIGHTS_METADATA) . '/mets:mdWrap', 'MDTYPE', 'Test'); + $this->resetDocument(); + + $this->removeAttribute(VH::XPATH_ADMINISTRATIVE_RIGHTS_METADATA . '/mets:mdWrap', 'OTHERMDTYPE'); + $this->hasErrorAttribute(VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_RIGHTS_METADATA) . '/mets:mdWrap', 'OTHERMDTYPE'); + + $this->setAttributeValue(VH::XPATH_ADMINISTRATIVE_RIGHTS_METADATA . '/mets:mdWrap', 'OTHERMDTYPE', 'Test'); + $this->hasErrorAttributeWithValue(VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_RIGHTS_METADATA) . '/mets:mdWrap', 'OTHERMDTYPE', 'Test'); + $this->resetDocument(); + + $this->removeNodes(VH::XPATH_ADMINISTRATIVE_RIGHTS_METADATA . '/mets:mdWrap/mets:xmlData/dv:rights'); + $this->hasErrorOne('mets:xmlData[dv:rights]', VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_RIGHTS_METADATA) . '/mets:mdWrap'); + } + + /** + * Test validation against the rules of chapters "2.6.2.1 Technische Metadaten – mets:techMD" and "2.6.2.2 Eingebettete technische Daten – mets:techMD/mets:mdWrap" + * + * @return void + * @throws \DOMException + */ + public function testTechnicalMetadataStructure(): void + { + $this->addChildNodeWithNamespace(VH::XPATH_ADMINISTRATIVE_METADATA, VH::NAMESPACE_METS, 'mets:techMD'); + $this->hasErrorAttribute(VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_TECHNICAL_METADATA), 'ID'); + + $this->setAttributeValue(VH::XPATH_ADMINISTRATIVE_TECHNICAL_METADATA, 'ID', 'DMDLOG_0001'); + $this->hasErrorUniqueId(VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_TECHNICAL_METADATA), 'DMDLOG_0001'); + + $this->setAttributeValue(VH::XPATH_ADMINISTRATIVE_TECHNICAL_METADATA, 'ID', 'TECH_0001'); + $this->hasErrorOne('mets:mdWrap', VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_TECHNICAL_METADATA)); + + $this->addChildNodeWithNamespace(VH::XPATH_ADMINISTRATIVE_TECHNICAL_METADATA, VH::NAMESPACE_METS, 'mets:mdWrap'); + $this->hasErrorAttribute(VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_TECHNICAL_METADATA) . '/mets:mdWrap', 'MDTYPE'); + + $this->setAttributeValue(VH::XPATH_ADMINISTRATIVE_TECHNICAL_METADATA . '/mets:mdWrap', 'MDTYPE', ''); + $this->hasErrorAttribute(VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_TECHNICAL_METADATA) . '/mets:mdWrap', 'OTHERMDTYPE'); + + $this->setAttributeValue(VH::XPATH_ADMINISTRATIVE_TECHNICAL_METADATA . '/mets:mdWrap', 'OTHERMDTYPE', ''); + $this->hasErrorOne('mets:xmlData', VH::trimDoubleSlash(VH::XPATH_ADMINISTRATIVE_TECHNICAL_METADATA) . '/mets:mdWrap'); + } + + protected function createValidator(): AbstractDlfValidator + { + return new AdministrativeMetadataValidator(); + } +} diff --git a/Tests/Unit/Validation/Mets/DescriptiveMetadataValidatorTest.php b/Tests/Unit/Validation/Mets/DescriptiveMetadataValidatorTest.php new file mode 100644 index 000000000..622983b0a --- /dev/null +++ b/Tests/Unit/Validation/Mets/DescriptiveMetadataValidatorTest.php @@ -0,0 +1,76 @@ + + * All rights reserved + * + * This script is part of the TYPO3 project. The TYPO3 project is + * free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * The GNU General Public License can be found at + * http://www.gnu.org/copyleft/gpl.html. + * + * This script is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * This copyright notice MUST APPEAR in all copies of the script! + */ + +use Kitodo\Dlf\Validation\AbstractDlfValidator; +use Slub\Dfgviewer\Common\ValidationHelper as VH; +use Slub\Dfgviewer\Validation\Mets\DescriptiveMetadataValidator; + +class DescriptiveMetadataValidatorTest extends AbstractDomDocumentValidatorTest +{ + /** + * Test validation against the rules of chapter "2.5.1 Metadatensektion – mets:dmdSec" + * + * @return void + */ + public function testDescriptiveMetadata(): void + { + $this->removeNodes(VH::XPATH_DESCRIPTIVE_METADATA_SECTIONS); + $this->hasErrorAny(VH::XPATH_DESCRIPTIVE_METADATA_SECTIONS); + $this->resetDocument(); + + $this->removeNodes(VH::XPATH_LOGICAL_STRUCTURAL_ELEMENTS); + $this->hasErrorOne(VH::XPATH_LOGICAL_STRUCTURAL_ELEMENTS); + $this->resetDocument(); + + $this->setAttributeValue(VH::XPATH_LOGICAL_STRUCTURAL_ELEMENTS, 'DMDID', 'Test'); + $this->hasErrorAttributeRefToOne('/mets:mets/mets:structMap[1]/mets:div', 'DMDID', 'Test', VH::XPATH_DESCRIPTIVE_METADATA_SECTIONS); + } + + /** + * Test validation against the rules of chapter "2.5.2.1 Eingebettete Metadaten – mets:mdWrap" + * + * @return void + */ + public function testEmbeddedMetadata(): void + { + $this->removeNodes(VH::XPATH_DESCRIPTIVE_METADATA_SECTIONS . '/mets:mdWrap'); + $this->hasErrorOne('mets:mdWrap', VH::trimDoubleSlash(VH::XPATH_DESCRIPTIVE_METADATA_SECTIONS)); + $this->resetDocument(); + + $this->setAttributeValue(VH::XPATH_DESCRIPTIVE_METADATA_SECTIONS . '/mets:mdWrap', 'MDTYPE', 'Test'); + $this->hasErrorAttributeWithValue(VH::trimDoubleSlash(VH::XPATH_DESCRIPTIVE_METADATA_SECTIONS) . '/mets:mdWrap', 'MDTYPE', 'Test'); + $this->resetDocument(); + + $this->removeNodes(VH::XPATH_DESCRIPTIVE_METADATA_SECTIONS . '/mets:mdWrap/mets:xmlData/mods:mods'); + $this->hasErrorOne('mets:xmlData[mods:mods]', VH::trimDoubleSlash(VH::XPATH_DESCRIPTIVE_METADATA_SECTIONS) . '/mets:mdWrap'); + } + + protected function createValidator(): AbstractDlfValidator + { + return new DescriptiveMetadataValidator(); + } +} diff --git a/Tests/Unit/Validation/Mets/DigitalRepresentationValidatorTest.php b/Tests/Unit/Validation/Mets/DigitalRepresentationValidatorTest.php new file mode 100644 index 000000000..21198fc43 --- /dev/null +++ b/Tests/Unit/Validation/Mets/DigitalRepresentationValidatorTest.php @@ -0,0 +1,116 @@ + + * All rights reserved + * + * This script is part of the TYPO3 project. The TYPO3 project is + * free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * The GNU General Public License can be found at + * http://www.gnu.org/copyleft/gpl.html. + * + * This script is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * This copyright notice MUST APPEAR in all copies of the script! + */ + +use Kitodo\Dlf\Validation\AbstractDlfValidator; +use Slub\Dfgviewer\Common\ValidationHelper as VH; +use Slub\Dfgviewer\Validation\Mets\DigitalRepresentationValidator; + +class DigitalRepresentationValidatorTest extends AbstractDomDocumentValidatorTest +{ + /** + * Test validation against the rules of chapter "2.4.1 Dateisektion – mets:fileSec" + * + * @return void + * @throws \DOMException + */ + public function testFileSections(): void + { + $this->addChildNodeWithNamespace('/mets:mets', VH::NAMESPACE_METS, 'mets:fileSec'); + $this->hasErrorNoneOrOne(VH::XPATH_FILE_SECTIONS); + $this->resetDocument(); + + $this->removeNodes(VH::XPATH_FILE_SECTIONS); + $this->hasErrorOne(VH::XPATH_FILE_SECTIONS); + $this->resetDocument(); + + $this->removeNodes(VH::XPATH_PHYSICAL_STRUCTURES); + $this->removeNodes(VH::XPATH_FILE_SECTIONS); + $this->hasNoError(); + } + + /** + * Test validation against the rules of chapter "2.4.2.1 Dateigruppen – mets:fileGrp" + * + * @return void + */ + public function testFileGroups(): void + { + $this->removeNodes(VH::XPATH_FILE_SECTION_GROUPS); + $this->hasErrorAny(VH::XPATH_FILE_SECTION_GROUPS); + $this->resetDocument(); + + $this->removeNodes(VH::XPATH_FILE_SECTION_GROUPS . '[@USE="DEFAULT"]'); + $this->hasErrorOne(VH::XPATH_FILE_SECTION_GROUPS . '[@USE="DEFAULT"]'); + $this->resetDocument(); + + $this->setAttributeValue(VH::XPATH_FILE_SECTION_GROUPS . '[@USE="THUMBS"]', 'USE', 'DEFAULT'); + $this->hasErrorUniqueAttribute(VH::trimDoubleSlash(VH::XPATH_FILE_SECTION_GROUPS) . '[1]', 'USE', 'DEFAULT'); + } + + /** + * Test validation against the rules of chapter "2.4.2.2 Datei – mets:fileGrp/mets:file" and "2.4.2.3 Dateilink – mets:fileGrp/mets:file/mets:FLocat" + * + * @return void + */ + public function testFiles(): void + { + $this->removeNodes(VH::XPATH_FILE_SECTION_FILES); + $this->hasErrorAny(VH::XPATH_FILE_SECTION_FILES); + $this->resetDocument(); + + $this->setAttributeValue(VH::XPATH_FILE_SECTION_FILES, 'ID', 'DMDLOG_0001'); + $this->hasErrorUniqueId(VH::trimDoubleSlash(VH::XPATH_FILE_SECTION_GROUPS) . '[1]/mets:file', 'DMDLOG_0001'); + $this->resetDocument(); + + $this->removeAttribute(VH::XPATH_FILE_SECTION_FILES, 'MIMETYPE'); + $this->hasErrorAttribute(VH::trimDoubleSlash(VH::XPATH_FILE_SECTION_GROUPS) . '[1]/mets:file', 'MIMETYPE'); + $this->resetDocument(); + + $this->removeNodes(VH::XPATH_FILE_SECTION_FILES . '/mets:FLocat'); + $this->hasErrorOne('mets:FLocat', VH::trimDoubleSlash(VH::XPATH_FILE_SECTION_GROUPS) . '[1]/mets:file'); + $this->resetDocument(); + + $this->removeAttribute(VH::XPATH_FILE_SECTION_FILES . '/mets:FLocat', 'LOCTYPE'); + $this->hasErrorAttribute(VH::trimDoubleSlash(VH::XPATH_FILE_SECTION_GROUPS) . '[1]/mets:file/mets:FLocat', 'LOCTYPE'); + $this->resetDocument(); + + $this->setAttributeValue(VH::XPATH_FILE_SECTION_FILES . '/mets:FLocat', 'LOCTYPE', 'Test'); + $this->hasErrorAttributeWithValue(VH::trimDoubleSlash(VH::XPATH_FILE_SECTION_GROUPS) . '[1]/mets:file/mets:FLocat', 'LOCTYPE', 'Test'); + $this->resetDocument(); + + $this->removeAttribute(VH::XPATH_FILE_SECTION_FILES . '/mets:FLocat', 'xlink:href'); + $this->hasErrorAttribute(VH::trimDoubleSlash(VH::XPATH_FILE_SECTION_GROUPS) . '[1]/mets:file/mets:FLocat', 'xlink:href'); + + $this->setAttributeValue(VH::XPATH_FILE_SECTION_FILES . '/mets:FLocat', 'xlink:href', 'Test'); + $this->hasErrorAttributeWithUrl(VH::trimDoubleSlash(VH::XPATH_FILE_SECTION_GROUPS) . '[1]/mets:file/mets:FLocat', 'xlink:href', 'Test'); + } + + protected function createValidator(): AbstractDlfValidator + { + return new DigitalRepresentationValidator(); + } +} diff --git a/Tests/Unit/Validation/Mets/LinkingLogicalPhysicalStructureValidatorTest.php b/Tests/Unit/Validation/Mets/LinkingLogicalPhysicalStructureValidatorTest.php new file mode 100644 index 000000000..22e4bd273 --- /dev/null +++ b/Tests/Unit/Validation/Mets/LinkingLogicalPhysicalStructureValidatorTest.php @@ -0,0 +1,64 @@ + + * All rights reserved + * + * This script is part of the TYPO3 project. The TYPO3 project is + * free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * The GNU General Public License can be found at + * http://www.gnu.org/copyleft/gpl.html. + * + * This script is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * This copyright notice MUST APPEAR in all copies of the script! + */ + +use Kitodo\Dlf\Validation\AbstractDlfValidator; +use Slub\Dfgviewer\Common\ValidationHelper as VH; +use Slub\Dfgviewer\Validation\Mets\LinkingLogicalPhysicalStructureValidator; + +class LinkingLogicalPhysicalStructureValidatorTest extends AbstractDomDocumentValidatorTest +{ + /** + * Test validation against the rules of chapter "2.3.1 Structure links - mets:structLink" + * + * @return void + * @throws \DOMException + */ + public function testMultipleStructLinks(): void + { + $this->addChildNodeWithNamespace('/mets:mets', VH::NAMESPACE_METS, 'mets:structLink'); + $this->hasErrorNoneOrOne(VH::XPATH_STRUCT_LINK); + } + + public function testLinkElements(): void + { + $this->removeNodes(VH::XPATH_STRUCT_LINK_ELEMENTS); + $this->hasErrorAny(VH::XPATH_STRUCT_LINK_ELEMENTS); + $this->resetDocument(); + + $this->setAttributeValue(VH::XPATH_STRUCT_LINK_ELEMENTS, 'xlink:from', 'Test'); + $this->hasErrorAttributeRefToOne(VH::trimDoubleSlash(VH::XPATH_STRUCT_LINK_ELEMENTS), 'xlink:from', 'Test', VH::XPATH_LOGICAL_STRUCTURES); + $this->resetDocument(); + + $this->setAttributeValue(VH::XPATH_STRUCT_LINK_ELEMENTS, 'xlink:to', 'Test'); + $this->hasErrorAttributeRefToOne(VH::trimDoubleSlash(VH::XPATH_STRUCT_LINK_ELEMENTS), 'xlink:to', 'Test', VH::XPATH_PHYSICAL_STRUCTURES); + } + + protected function createValidator(): AbstractDlfValidator + { + return new LinkingLogicalPhysicalStructureValidator(); + } +} diff --git a/Tests/Unit/Validation/Mets/LogicalStructureValidatorTest.php b/Tests/Unit/Validation/Mets/LogicalStructureValidatorTest.php new file mode 100644 index 000000000..0ba64ce23 --- /dev/null +++ b/Tests/Unit/Validation/Mets/LogicalStructureValidatorTest.php @@ -0,0 +1,105 @@ + + * All rights reserved + * + * This script is part of the TYPO3 project. The TYPO3 project is + * free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * The GNU General Public License can be found at + * http://www.gnu.org/copyleft/gpl.html. + * + * This script is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * This copyright notice MUST APPEAR in all copies of the script! + */ + +use Kitodo\Dlf\Validation\AbstractDlfValidator; +use Slub\Dfgviewer\Common\ValidationHelper as VH; +use Slub\Dfgviewer\Validation\Mets\LogicalStructureValidator; + +class LogicalStructureValidatorTest extends AbstractDomDocumentValidatorTest +{ + /** + * Test validation against the rules of chapter "2.1.1 Logical structure - mets:structMap" + * + * @return void + */ + public function testNotExistingLogicalStructureElement(): void + { + $this->removeNodes(VH::XPATH_LOGICAL_STRUCTURES); + $this->hasErrorAny(VH::XPATH_LOGICAL_STRUCTURES); + } + + /** + * Test validation against the rules of chapter "2.1.2.1 Structural element - mets:div" + * @return void + */ + public function testStructuralElements(): void + { + $this->removeNodes(VH::XPATH_LOGICAL_STRUCTURAL_ELEMENTS); + $this->hasErrorAny(VH::XPATH_LOGICAL_STRUCTURAL_ELEMENTS); + $this->resetDocument(); + + $this->removeAttribute(VH::XPATH_LOGICAL_STRUCTURAL_ELEMENTS, 'ID'); + $this->hasErrorAttribute('/mets:mets/mets:structMap[1]/mets:div', 'ID'); + $this->resetDocument(); + + $node = $this->doc->createElementNS(VH::NAMESPACE_METS, 'mets:div'); + $node->setAttribute('ID', 'LOG_0001'); + $this->addChildNode(VH::XPATH_LOGICAL_STRUCTURAL_ELEMENTS, $node); + $this->hasErrorUniqueId('/mets:mets/mets:structMap[1]/mets:div', 'LOG_0001'); + $this->resetDocument(); + + $this->removeAttribute(VH::XPATH_LOGICAL_STRUCTURAL_ELEMENTS, 'TYPE'); + $this->hasErrorAttribute('/mets:mets/mets:structMap[1]/mets:div', 'TYPE'); + + $this->setAttributeValue(VH::XPATH_LOGICAL_STRUCTURAL_ELEMENTS, 'TYPE', 'Test'); + $this->hasErrorAttributeWithValue('/mets:mets/mets:structMap[1]/mets:div', 'TYPE', 'Test'); + } + + /** + * Test validation against the rules of chapter "2.1.2.2 Reference to external METS-files - mets:div / mets:mptr" + * @return void + * @throws \DOMException + */ + public function testExternalReference(): void + { + $this->addChildNodeWithNamespace(VH::XPATH_LOGICAL_STRUCTURAL_ELEMENTS, VH::NAMESPACE_METS, 'mets:mptr'); + $this->addChildNodeWithNamespace(VH::XPATH_LOGICAL_STRUCTURAL_ELEMENTS, VH::NAMESPACE_METS, 'mets:mptr'); + $this->hasErrorNoneOrOne(VH::XPATH_LOGICAL_EXTERNAL_REFERENCES); + $this->resetDocument(); + + $this->addChildNodeWithNamespace(VH::XPATH_LOGICAL_STRUCTURAL_ELEMENTS, VH::NAMESPACE_METS, 'mets:mptr'); + $this->hasErrorAttribute('/mets:mets/mets:structMap[1]/mets:div/mets:mptr', 'LOCTYPE'); + + $this->setAttributeValue(VH::XPATH_LOGICAL_EXTERNAL_REFERENCES, 'LOCTYPE', 'Test'); + $this->hasErrorAttributeWithValue('/mets:mets/mets:structMap[1]/mets:div/mets:mptr', 'LOCTYPE', 'Test'); + + $this->setAttributeValue(VH::XPATH_LOGICAL_EXTERNAL_REFERENCES, 'LOCTYPE', 'URL'); + $this->hasErrorAttribute('/mets:mets/mets:structMap[1]/mets:div/mets:mptr', 'xlink:href'); + + $this->setAttributeValue(VH::XPATH_LOGICAL_EXTERNAL_REFERENCES, 'xlink:href', 'Test'); + $this->hasErrorAttributeWithUrl('/mets:mets/mets:structMap[1]/mets:div/mets:mptr', 'xlink:href', 'Test'); + + $this->setAttributeValue(VH::XPATH_LOGICAL_EXTERNAL_REFERENCES, 'xlink:href', 'http://example.com/periodical.xml'); + $result = $this->validate(); + self::assertFalse($result->hasErrors()); + } + + protected function createValidator(): AbstractDlfValidator + { + return new LogicalStructureValidator(); + } +} diff --git a/Tests/Unit/Validation/Mets/PhysicalStructureValidatorTest.php b/Tests/Unit/Validation/Mets/PhysicalStructureValidatorTest.php new file mode 100644 index 000000000..70878537a --- /dev/null +++ b/Tests/Unit/Validation/Mets/PhysicalStructureValidatorTest.php @@ -0,0 +1,92 @@ + + * All rights reserved + * + * This script is part of the TYPO3 project. The TYPO3 project is + * free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * The GNU General Public License can be found at + * http://www.gnu.org/copyleft/gpl.html. + * + * This script is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * This copyright notice MUST APPEAR in all copies of the script! + */ + +use Kitodo\Dlf\Validation\AbstractDlfValidator; +use Slub\Dfgviewer\Common\ValidationHelper as VH; +use Slub\Dfgviewer\Validation\Mets\PhysicalStructureValidator; + +class PhysicalStructureValidatorTest extends AbstractDomDocumentValidatorTest +{ + /** + * Test validation against the rules of chapter "2.2.1 Physical structure - mets:structMap" + * + * @return void + * @throws \DOMException + */ + public function testMultiplePhysicalDivisions(): void + { + $node = $this->doc->createElementNS(VH::NAMESPACE_METS, 'mets:structMap'); + $node->setAttribute('TYPE', 'PHYSICAL'); + $this->addChildNode('/mets:mets', $node); + $this->hasErrorNoneOrOne(VH::XPATH_PHYSICAL_STRUCTURES); + } + + /** + * Test validation against the rules of chapter "2.2.2.1 Structural element - mets:div" + * + * @return void + * @throws \DOMException + */ + public function testStructuralElements(): void + { + $this->removeNodes(VH::XPATH_PHYSICAL_STRUCTURAL_ELEMENT_SEQUENCE); + $this->hasErrorOne(VH::XPATH_PHYSICAL_STRUCTURAL_ELEMENT_SEQUENCE); + $this->resetDocument(); + + $this->removeAttribute(VH::XPATH_PHYSICAL_STRUCTURAL_ELEMENT_SEQUENCE, 'TYPE'); + $this->hasErrorAttribute('/mets:mets/mets:structMap[2]/mets:div', 'TYPE'); + + $this->setAttributeValue(VH::XPATH_PHYSICAL_STRUCTURAL_ELEMENT_SEQUENCE, 'TYPE', 'Test'); + $this->hasErrorAttributeWithValue('/mets:mets/mets:structMap[2]/mets:div', 'TYPE', 'Test'); + $this->resetDocument(); + + $this->removeNodes(VH::XPATH_PHYSICAL_STRUCTURAL_ELEMENTS); + $this->hasErrorAny(VH::XPATH_PHYSICAL_STRUCTURAL_ELEMENTS); + $this->resetDocument(); + + $this->removeAttribute(VH::XPATH_PHYSICAL_STRUCTURAL_ELEMENTS, 'ID'); + $this->hasErrorAttribute('/mets:mets/mets:structMap[2]/mets:div/mets:div', 'ID'); + $this->resetDocument(); + + $node = $this->doc->createElementNS(VH::NAMESPACE_METS, 'mets:div'); + $node->setAttribute('ID', 'PHYS_0001'); + $this->addChildNode('//mets:structMap[@TYPE="PHYSICAL"]/mets:div', $node); + $this->hasErrorUniqueId('/mets:mets/mets:structMap[2]/mets:div/mets:div[1]', 'PHYS_0001'); + $this->resetDocument(); + + $this->removeAttribute(VH::XPATH_PHYSICAL_STRUCTURAL_ELEMENTS, 'TYPE'); + $this->hasErrorAttribute('/mets:mets/mets:structMap[2]/mets:div/mets:div', 'TYPE'); + + $this->setAttributeValue(VH::XPATH_PHYSICAL_STRUCTURAL_ELEMENTS, 'TYPE', 'Test'); + $this->hasErrorAttributeWithValue('/mets:mets/mets:structMap[2]/mets:div/mets:div', 'TYPE', 'Test'); + } + + protected function createValidator(): AbstractDlfValidator + { + return new PhysicalStructureValidator(); + } +} diff --git a/composer.json b/composer.json index a4b398d80..d1094f82c 100644 --- a/composer.json +++ b/composer.json @@ -48,7 +48,10 @@ "slub/slub-digitalcollections": "^4.0|dev-master" }, "require-dev": { - "phpstan/phpstan": "^1.12" + "phpstan/phpstan": "^1.12", + "phpunit/phpunit": "^9.6.22", + "spatie/phpunit-watcher": "^1.23.6", + "typo3/testing-framework": "^7.1.1" }, "replace": { "typo3-ter/dfgviewer": "self.version" @@ -82,6 +85,12 @@ ], "phpstan": [ "@php vendor/bin/phpstan --configuration=\".github/phpstan.neon\"" + ], + "test:unit:local": [ + "phpunit -c Build/Test/UnitTests.xml" + ], + "test:unit:watch": [ + "phpunit-watcher watch -c Build/Test/UnitTests.xml" ] }, "config": {