Skip to content

Commit

Permalink
[FEATURE] General reusable validation implementation and middleware f…
Browse files Browse the repository at this point in the history
…or validating DOMDocument (#1313)

Co-authored-by: Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
  • Loading branch information
markusweigelt and sebastian-meyer authored Sep 23, 2024
1 parent 5240b16 commit c5d13ea
Show file tree
Hide file tree
Showing 15 changed files with 943 additions and 2 deletions.
1 change: 0 additions & 1 deletion Build/Test/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# Adopted/reduced from https://github.com/TYPO3/typo3/blob/608f238a8b7696a49a47e1e73ce8e2845455f0f5/Build/testing-docker/local/docker-compose.yml

version: "2.3"
services:
mysql:
image: docker.io/mysql:${MYSQL_VERSION}
Expand Down
116 changes: 116 additions & 0 deletions Classes/Middleware/DOMDocumentValidation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php

namespace Kitodo\Dlf\Middleware;

/**
* (c) Kitodo. Key to digital objects e.V. <contact@kitodo.org>
*
* This file is part of the Kitodo and TYPO3 projects.
*
* @license GNU General Public License version 3 or later.
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/

use DOMDocument;
use InvalidArgumentException;
use Kitodo\Dlf\Validation\DOMDocumentValidationStack;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerAwareTrait;
use TYPO3\CMS\Core\Http\ResponseFactory;
use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
use TYPO3\CMS\Extbase\Error\Result;

/**
* Middleware for validation of DOMDocuments.
*
* @package TYPO3
* @subpackage dlf
* @access public
*/
class DOMDocumentValidation implements MiddlewareInterface
{
use LoggerAwareTrait;

/**
* The main method of the middleware.
*
* @access public
*
* @param ServerRequestInterface $request for processing
* @param RequestHandlerInterface $handler for processing
*
* @throws InvalidArgumentException
*
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$this->logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(static::class);
$response = $handler->handle($request);
// parameters are sent by POST --> use getParsedBody() instead of getQueryParams()
$parameters = $request->getQueryParams();

// Return if not this middleware
if (!isset($parameters['middleware']) || ($parameters['middleware'] != 'dlf/domDocumentValidation')) {
return $response;
}

$urlParam = $parameters['url'];
if (!isset($urlParam)) {
throw new InvalidArgumentException('URL parameter is missing.', 1724334674);
}

/** @var ConfigurationManagerInterface $configurationManager */
$configurationManager = GeneralUtility::makeInstance(ConfigurationManagerInterface::class);
$settings = $configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS);

if (!array_key_exists("domDocumentValidationValidators", $settings)) {
$this->logger->error('DOMDocumentValidation is not configured correctly.');
throw new InvalidArgumentException('DOMDocumentValidation is not configured correctly.', 1724335601);
}

$validation = GeneralUtility::makeInstance(DOMDocumentValidationStack::class, $settings['domDocumentValidationValidators']);

if (!GeneralUtility::isValidUrl($urlParam)) {
$this->logger->debug('Parameter "' . $urlParam . '" is not a valid url.');
throw new InvalidArgumentException('Value of url parameter is not a valid url.', 1724852611);
}

$content = GeneralUtility::getUrl($urlParam);
if ($content === false) {
$this->logger->debug('Error while loading content of "' . $urlParam . '"');
throw new InvalidArgumentException('Error while loading content of url.', 1724420640);
}

$document = new DOMDocument();
if ($document->loadXML($content) === false) {
$this->logger->debug('Error converting content of "' . $urlParam . '" to xml.');
throw new InvalidArgumentException('Error converting content to xml.', 1724420648);
}

$result = $validation->validate($document);
return $this->getJsonResponse($result);
}

protected function getJsonResponse(Result $result): ResponseInterface
{
$resultContent = ["valid" => !$result->hasErrors()];

foreach ($result->getErrors() as $error) {
$resultContent["results"][$error->getTitle()][] = $error->getMessage();
}

/** @var ResponseFactory $responseFactory */
$responseFactory = GeneralUtility::makeInstance(ResponseFactory::class);
$response = $responseFactory->createResponse()
->withHeader('Content-Type', 'application/json; charset=utf-8');
$response->getBody()->write(json_encode($resultContent));
return $response;
}
}
129 changes: 129 additions & 0 deletions Classes/Validation/AbstractDlfValidationStack.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php

declare(strict_types=1);

/**
* (c) Kitodo. Key to digital objects e.V. <contact@kitodo.org>
*
* This file is part of the Kitodo and TYPO3 projects.
*
* @license GNU General Public License version 3 or later.
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/

namespace Kitodo\Dlf\Validation;

use InvalidArgumentException;
use Psr\Log\LoggerAwareTrait;
use TYPO3\CMS\Core\Utility\GeneralUtility;

/**
* Abstract class provides functions for implementing a validation stack.
*
* @package TYPO3
* @subpackage dlf
*
* @access public
*/
abstract class AbstractDlfValidationStack extends AbstractDlfValidator
{
use LoggerAwareTrait;

const ITEM_KEY_TITLE = "title";
const ITEM_KEY_BREAK_ON_ERROR = "breakOnError";
const ITEM_KEY_VALIDATOR = "validator";

protected array $validatorStack = [];

public function __construct(string $valueClassName)
{
parent::__construct($valueClassName);
}

/**
* Add validators by validation stack configuration to the internal validator stack.
*
* @param array $configuration The configuration of validators
*
* @throws InvalidArgumentException
*
* @return void
*/
public function addValidators(array $configuration): void
{
foreach ($configuration as $configurationItem) {
if (!class_exists($configurationItem["className"])) {
$this->logger->error('Unable to load class ' . $configurationItem["className"] . '.');
throw new InvalidArgumentException('Unable to load validator class.', 1723200537037);
}
$breakOnError = !isset($configurationItem["breakOnError"]) || $configurationItem["breakOnError"] !== "false";
$this->addValidator($configurationItem["className"], $configurationItem["title"] ?? "", $breakOnError, $configurationItem["configuration"] ?? []);
}
}

/**
* Add validator to the internal validator stack.
*
* @param string $className Class name of the validator which was derived from Kitodo\Dlf\Validation\AbstractDlfValidator
* @param string $title The title of the validator
* @param bool $breakOnError True if the execution of validator stack is interrupted when validator throws an error
* @param array|null $configuration The configuration of validator
*
* @throws InvalidArgumentException
*
* @return void
*/
protected function addValidator(string $className, string $title, bool $breakOnError = true, array $configuration = null): void
{
if ($configuration === null) {
$validator = GeneralUtility::makeInstance($className);
} else {
$validator = GeneralUtility::makeInstance($className, $configuration);
}

if (!$validator instanceof AbstractDlfValidator) {
$this->logger->error($className . ' must be an instance of AbstractDlfValidator.');
throw new InvalidArgumentException('Class must be an instance of AbstractDlfValidator.', 1723121212747);
}

$title = empty($title) ? $className : $title;

$this->validatorStack[] = [self::ITEM_KEY_TITLE => $title, self::ITEM_KEY_VALIDATOR => $validator, self::ITEM_KEY_BREAK_ON_ERROR => $breakOnError];
}

/**
* Check if value is valid across all validation classes of validation stack.
*
* @param $value The value of defined class name.
*
* @throws InvalidArgumentException
*
* @return void
*/
protected function isValid($value): void
{
if (!$value instanceof $this->valueClassName) {
$this->logger->error('Value must be an instance of ' . $this->valueClassName . '.');
throw new InvalidArgumentException('Type of value is not valid.', 1723127564821);
}

if (empty($this->validatorStack)) {
$this->logger->error('The validation stack has no validator.');
throw new InvalidArgumentException('The validation stack has no validator.', 1724662426);
}

foreach ($this->validatorStack as $validationStackItem) {
$validator = $validationStackItem[self::ITEM_KEY_VALIDATOR];
$result = $validator->validate($value);

foreach ($result->getErrors() as $error) {
$this->addError($error->getMessage(), $error->getCode(), [], $validationStackItem[self::ITEM_KEY_TITLE]);
}

if ($validationStackItem[self::ITEM_KEY_BREAK_ON_ERROR] && $result->hasErrors()) {
break;
}
}
}
}
56 changes: 56 additions & 0 deletions Classes/Validation/AbstractDlfValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

/**
* (c) Kitodo. Key to digital objects e.V. <contact@kitodo.org>
*
* This file is part of the Kitodo and TYPO3 projects.
*
* @license GNU General Public License version 3 or later.
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/

namespace Kitodo\Dlf\Validation;

use InvalidArgumentException;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator;

/**
* Base Validator provides functionalities for using the derived validator within a validation stack.
*
* @package TYPO3
* @subpackage dlf
*
* @access public
*/
abstract class AbstractDlfValidator extends AbstractValidator
{
use LoggerAwareTrait;

protected string $valueClassName;

/**
* @param $valueClassName string The class name of the value
*/
public function __construct(string $valueClassName)
{
parent::__construct();
$this->logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(static::class);
$this->valueClassName = $valueClassName;
}

public function validate($value)
{
if (!$value instanceof $this->valueClassName) {
$this->logger->debug('Value must be an instance of ' . $this->valueClassName . '.');
throw new InvalidArgumentException('Type of value is not valid.', 1723126505626);
}
return parent::validate($value);
}
}
32 changes: 32 additions & 0 deletions Classes/Validation/DOMDocumentValidationStack.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

/**
* (c) Kitodo. Key to digital objects e.V. <contact@kitodo.org>
*
* This file is part of the Kitodo and TYPO3 projects.
*
* @license GNU General Public License version 3 or later.
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/

namespace Kitodo\Dlf\Validation;

/**
* Implementation of AbstractDlfValidationStack for validating DOMDocument against the configured validators.
*
* @package TYPO3
* @subpackage dlf
*
* @access public
*/
class DOMDocumentValidationStack extends AbstractDlfValidationStack
{
public function __construct(array $configuration)
{
parent::__construct(\DOMDocument::class);
$this->addValidators($configuration);
}
}
44 changes: 44 additions & 0 deletions Classes/Validation/LibXmlTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace Kitodo\Dlf\Validation;

/**
* (c) Kitodo. Key to digital objects e.V. <contact@kitodo.org>
*
* This file is part of the Kitodo and TYPO3 projects.
*
* @license GNU General Public License version 3 or later.
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*/

trait LibXmlTrait
{
/**
* Add the errors from the libxml error buffer as validation error.
*
* To enable user error handling, you need to use libxml_use_internal_errors(true) beforehand.
*
* @return void
*/
public function addErrorsOfBuffer(): void
{
$errors = libxml_get_errors();
foreach ($errors as $error) {
$this->addError($error->message, $error->code);
}
libxml_clear_errors();
}

public function enableErrorBuffer(): void
{
libxml_use_internal_errors(true);
}

public function disableErrorBuffer(): void
{
libxml_use_internal_errors(false);
}
}
Loading

0 comments on commit c5d13ea

Please sign in to comment.