515 lines
17 KiB
PHP
515 lines
17 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Service;
|
|
|
|
use Faker\Factory;
|
|
use Faker\Generator;
|
|
|
|
class BpnXmlAnonymizer
|
|
{
|
|
private const SAFE_EMAIL_DOMAIN = 'example.com';
|
|
|
|
private Generator $faker;
|
|
|
|
/**
|
|
* @var array<string, array<string, string>>
|
|
*/
|
|
private array $identities = [];
|
|
|
|
private int $fallbackIdentityCounter = 0;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->faker = Factory::create('de_DE');
|
|
}
|
|
|
|
public function anonymize(string $xml, bool $resetState = true): string
|
|
{
|
|
if (true === $resetState) {
|
|
$this->resetState();
|
|
}
|
|
|
|
$document = new \DOMDocument();
|
|
$document->preserveWhiteSpace = true;
|
|
$document->formatOutput = false;
|
|
|
|
if (false === @$document->loadXML($xml)) {
|
|
throw new \InvalidArgumentException('Invalid XML input.');
|
|
}
|
|
|
|
$xpath = new \DOMXPath($document);
|
|
$personNodes = $xpath->query('//anfrage | //anmelder | //teilnehmerliste/teilnehmer | //kundennamen/kunde | //adressdaten');
|
|
|
|
if (false === $personNodes) {
|
|
throw new \RuntimeException('Unable to query person nodes.');
|
|
}
|
|
|
|
foreach ($personNodes as $index => $personNode) {
|
|
if (false === $personNode instanceof \DOMElement) {
|
|
continue;
|
|
}
|
|
|
|
$personId = $this->resolvePersonId($personNode, (int) $index);
|
|
$identity = $this->getOrCreateIdentity($personId);
|
|
|
|
if ('anfrage' === $personNode->tagName) {
|
|
$this->replaceRequestFields($personNode, $identity);
|
|
continue;
|
|
}
|
|
|
|
$this->replaceNameFields($personNode, $identity);
|
|
$this->replaceEmailFields($personNode, $identity);
|
|
$this->replaceAddressFields($personNode, $identity);
|
|
$this->replaceDemographicFields($personNode, $identity);
|
|
$this->replaceFinancialFields($personNode, $identity);
|
|
$this->replaceMetadataFields($personNode, $identity);
|
|
}
|
|
|
|
$result = $document->saveXML();
|
|
if (false === $result) {
|
|
throw new \RuntimeException('Unable to serialize anonymized XML.');
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
public function getLastIdentityCount(): int
|
|
{
|
|
return \count($this->identities);
|
|
}
|
|
|
|
public function resetState(): void
|
|
{
|
|
$this->identities = [];
|
|
$this->fallbackIdentityCounter = 0;
|
|
}
|
|
|
|
private function resolvePersonId(\DOMElement $personNode, int $nodeIndex): string
|
|
{
|
|
if ('anfrage' === $personNode->tagName || 'adressdaten' === $personNode->tagName) {
|
|
$emailNode = $this->getDirectChild($personNode, 'email');
|
|
if (null !== $emailNode) {
|
|
$emailValue = trim($emailNode->textContent);
|
|
if ('' !== $emailValue) {
|
|
return sprintf('email:%s', mb_strtolower($emailValue));
|
|
}
|
|
}
|
|
|
|
if ('adressdaten' === $personNode->tagName) {
|
|
$communicationNode = $this->getDirectChild($personNode, 'kommunikation');
|
|
if (null !== $communicationNode) {
|
|
$nestedEmailNode = $this->getDirectChild($communicationNode, 'email');
|
|
if (null !== $nestedEmailNode) {
|
|
$emailValue = trim($nestedEmailNode->textContent);
|
|
if ('' !== $emailValue) {
|
|
return sprintf('email:%s', mb_strtolower($emailValue));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$personIdValue = $this->findPersonIdInAncestors($personNode);
|
|
if (null !== $personIdValue) {
|
|
return $personIdValue;
|
|
}
|
|
|
|
$fingerprintKey = $this->buildFingerprintKey($personNode);
|
|
if (null !== $fingerprintKey) {
|
|
return $fingerprintKey;
|
|
}
|
|
|
|
if (true === $personNode->hasAttribute('id')) {
|
|
$attributeValue = trim((string) $personNode->getAttribute('id'));
|
|
if ('' !== $attributeValue) {
|
|
return sprintf('attr-id:%s', $attributeValue);
|
|
}
|
|
}
|
|
|
|
++$this->fallbackIdentityCounter;
|
|
|
|
return sprintf('fallback:%d:%d', $nodeIndex, $this->fallbackIdentityCounter);
|
|
}
|
|
|
|
private function buildFingerprintKey(\DOMElement $personNode): ?string
|
|
{
|
|
$nameValues = [];
|
|
$nameNode = $this->getDirectChild($personNode, 'name');
|
|
if (null !== $nameNode) {
|
|
$nameValues[] = trim($nameNode->textContent);
|
|
}
|
|
if (true === $personNode->hasAttribute('name')) {
|
|
$nameValues[] = trim((string) $personNode->getAttribute('name'));
|
|
}
|
|
|
|
$firstNameNode = $this->getDirectChild($personNode, 'vorname');
|
|
if (null !== $firstNameNode) {
|
|
$nameValues[] = trim($firstNameNode->textContent);
|
|
}
|
|
if (true === $personNode->hasAttribute('vorname')) {
|
|
$nameValues[] = trim((string) $personNode->getAttribute('vorname'));
|
|
}
|
|
|
|
$communicationNode = $this->getDirectChild($personNode, 'kommunikation');
|
|
if (null !== $communicationNode) {
|
|
$emailNode = $this->getDirectChild($communicationNode, 'email');
|
|
if (null !== $emailNode) {
|
|
$nameValues[] = trim($emailNode->textContent);
|
|
}
|
|
}
|
|
|
|
$addressNode = $this->getDirectChild($personNode, 'anschrift');
|
|
if (null !== $addressNode) {
|
|
foreach (['strasse', 'plz', 'ort'] as $fieldName) {
|
|
$fieldNode = $this->getDirectChild($addressNode, $fieldName);
|
|
if (null !== $fieldNode) {
|
|
$nameValues[] = trim($fieldNode->textContent);
|
|
}
|
|
}
|
|
}
|
|
|
|
$normalizedParts = [];
|
|
foreach ($nameValues as $value) {
|
|
if ('' === $value) {
|
|
continue;
|
|
}
|
|
|
|
$normalizedParts[] = mb_strtolower($value);
|
|
}
|
|
|
|
if (0 === \count($normalizedParts)) {
|
|
return null;
|
|
}
|
|
|
|
return sprintf('fp:%s', md5(implode('|', $normalizedParts)));
|
|
}
|
|
|
|
/**
|
|
* @return array<string, string>
|
|
*/
|
|
private function getOrCreateIdentity(string $personId): array
|
|
{
|
|
if (true === isset($this->identities[$personId])) {
|
|
return $this->identities[$personId];
|
|
}
|
|
|
|
$identityIndex = \count($this->identities) + 1;
|
|
$firstName = $this->faker->firstName();
|
|
$lastName = $this->faker->lastName();
|
|
$salutation = 'Divers';
|
|
$title = '';
|
|
$gender = 'D';
|
|
$nationality = 'XX';
|
|
$street = sprintf('%s %s', $this->faker->streetName(), $this->faker->buildingNumber());
|
|
$postalCode = $this->faker->postcode();
|
|
$city = $this->faker->city();
|
|
$district = '';
|
|
$country = 'XX';
|
|
$mobilePhone = sprintf('01%09d', $identityIndex);
|
|
$phone = sprintf('02%09d', $identityIndex);
|
|
$newsletter = 'False';
|
|
$iban = sprintf('DE%020d', $identityIndex);
|
|
$userName = sprintf('anon-user-%d', $identityIndex);
|
|
$requestKey = str_pad((string) $identityIndex, 32, '0', STR_PAD_LEFT);
|
|
$requestPassword = str_pad((string) ($identityIndex + 1), 32, '0', STR_PAD_LEFT);
|
|
$personIdValue = (string) (700000 + $identityIndex);
|
|
$addressIdValue = (string) (800000 + $identityIndex);
|
|
$customerIdValue = (string) (900000 + $identityIndex);
|
|
$height = '170';
|
|
$weight = '70';
|
|
$shoeSize = '42';
|
|
|
|
$email = sprintf('%s@%s', $userName, self::SAFE_EMAIL_DOMAIN);
|
|
|
|
$this->identities[$personId] = [
|
|
'firstName' => $firstName,
|
|
'lastName' => $lastName,
|
|
'salutation' => $salutation,
|
|
'title' => $title,
|
|
'gender' => $gender,
|
|
'nationality' => $nationality,
|
|
'street' => $street,
|
|
'postalCode' => $postalCode,
|
|
'city' => $city,
|
|
'district' => $district,
|
|
'country' => $country,
|
|
'email' => strtolower($email),
|
|
'mobilePhone' => $mobilePhone,
|
|
'phone' => $phone,
|
|
'newsletter' => $newsletter,
|
|
'iban' => $iban,
|
|
'userName' => $userName,
|
|
'requestKey' => $requestKey,
|
|
'requestPassword' => $requestPassword,
|
|
'personId' => $personIdValue,
|
|
'addressId' => $addressIdValue,
|
|
'customerId' => $customerIdValue,
|
|
'height' => $height,
|
|
'weight' => $weight,
|
|
'shoeSize' => $shoeSize,
|
|
];
|
|
|
|
return $this->identities[$personId];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, string> $identity
|
|
*/
|
|
private function replaceNameFields(\DOMElement $personNode, array $identity): void
|
|
{
|
|
$nameNode = $this->getDirectChild($personNode, 'name');
|
|
if (null !== $nameNode) {
|
|
$this->replaceNodeValue($nameNode, $identity['lastName']);
|
|
}
|
|
|
|
$firstNameNode = $this->getDirectChild($personNode, 'vorname');
|
|
if (null !== $firstNameNode) {
|
|
$this->replaceNodeValue($firstNameNode, $identity['firstName']);
|
|
}
|
|
|
|
if (true === $personNode->hasAttribute('name')) {
|
|
$this->replaceAttributeValue($personNode, 'name', $identity['lastName']);
|
|
}
|
|
|
|
if (true === $personNode->hasAttribute('vorname')) {
|
|
$this->replaceAttributeValue($personNode, 'vorname', $identity['firstName']);
|
|
}
|
|
|
|
if (true === $personNode->hasAttribute('id') && 'kunde' === $personNode->tagName) {
|
|
$this->replaceAttributeValue($personNode, 'id', $identity['customerId']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<string, string> $identity
|
|
*/
|
|
private function replaceEmailFields(\DOMElement $personNode, array $identity): void
|
|
{
|
|
$communicationNode = $this->getDirectChild($personNode, 'kommunikation');
|
|
if (null === $communicationNode) {
|
|
if ('anfrage' === $personNode->tagName) {
|
|
$emailNode = $this->getDirectChild($personNode, 'email');
|
|
if (null !== $emailNode) {
|
|
$this->replaceNodeValue($emailNode, $identity['email']);
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
$emailNode = $this->getDirectChild($communicationNode, 'email');
|
|
if (null !== $emailNode) {
|
|
$this->replaceNodeValue($emailNode, $identity['email']);
|
|
}
|
|
|
|
$mobileNode = $this->getDirectChild($communicationNode, 'telefonmobil');
|
|
if (null !== $mobileNode) {
|
|
$this->replaceNodeValue($mobileNode, $identity['mobilePhone']);
|
|
}
|
|
|
|
$phoneNode = $this->getDirectChild($communicationNode, 'telefonprivat');
|
|
if (null !== $phoneNode) {
|
|
$this->replaceNodeValue($phoneNode, $identity['phone']);
|
|
}
|
|
|
|
$newsletterNode = $this->getDirectChild($communicationNode, 'newsletter');
|
|
if (null !== $newsletterNode) {
|
|
$this->replaceNodeValue($newsletterNode, $identity['newsletter']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<string, string> $identity
|
|
*/
|
|
private function replaceAddressFields(\DOMElement $personNode, array $identity): void
|
|
{
|
|
$addressNode = $this->getDirectChild($personNode, 'anschrift');
|
|
if (null === $addressNode) {
|
|
return;
|
|
}
|
|
|
|
$streetNode = $this->getDirectChild($addressNode, 'strasse');
|
|
if (null !== $streetNode) {
|
|
$this->replaceNodeValue($streetNode, $identity['street']);
|
|
}
|
|
|
|
$postalCodeNode = $this->getDirectChild($addressNode, 'plz');
|
|
if (null !== $postalCodeNode) {
|
|
$this->replaceNodeValue($postalCodeNode, $identity['postalCode']);
|
|
}
|
|
|
|
$cityNode = $this->getDirectChild($addressNode, 'ort');
|
|
if (null !== $cityNode) {
|
|
$this->replaceNodeValue($cityNode, $identity['city']);
|
|
}
|
|
|
|
$districtNode = $this->getDirectChild($addressNode, 'ortsteil');
|
|
if (null !== $districtNode) {
|
|
$this->replaceNodeValue($districtNode, $identity['district']);
|
|
}
|
|
|
|
$countryNode = $this->getDirectChild($addressNode, 'land');
|
|
if (null !== $countryNode) {
|
|
$this->replaceNodeValue($countryNode, $identity['country']);
|
|
}
|
|
|
|
$addressIdNode = $this->getDirectChild($addressNode, 'id');
|
|
if (null !== $addressIdNode) {
|
|
$this->replaceNodeValue($addressIdNode, $identity['addressId']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<string, string> $identity
|
|
*/
|
|
private function replaceDemographicFields(\DOMElement $personNode, array $identity): void
|
|
{
|
|
$salutationNode = $this->getDirectChild($personNode, 'anrede');
|
|
if (null !== $salutationNode) {
|
|
$this->replaceNodeValue($salutationNode, $identity['salutation']);
|
|
}
|
|
|
|
$titleNode = $this->getDirectChild($personNode, 'titel');
|
|
if (null !== $titleNode) {
|
|
$this->replaceNodeValue($titleNode, $identity['title']);
|
|
}
|
|
|
|
$genderNode = $this->getDirectChild($personNode, 'geschlecht');
|
|
if (null !== $genderNode) {
|
|
$this->replaceNodeValue($genderNode, $identity['gender']);
|
|
}
|
|
|
|
$nationalityNode = $this->getDirectChild($personNode, 'nationalitaet');
|
|
if (null !== $nationalityNode) {
|
|
$this->replaceNodeValue($nationalityNode, $identity['nationality']);
|
|
}
|
|
|
|
$heightNode = $this->getDirectChild($personNode, 'sonstiges1');
|
|
if (null !== $heightNode) {
|
|
$this->replaceNodeValue($heightNode, $identity['height']);
|
|
}
|
|
|
|
$weightNode = $this->getDirectChild($personNode, 'sonstiges2');
|
|
if (null !== $weightNode) {
|
|
$this->replaceNodeValue($weightNode, $identity['weight']);
|
|
}
|
|
|
|
$shoeSizeNode = $this->getDirectChild($personNode, 'sonstiges3');
|
|
if (null !== $shoeSizeNode) {
|
|
$this->replaceNodeValue($shoeSizeNode, $identity['shoeSize']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<string, string> $identity
|
|
*/
|
|
private function replaceFinancialFields(\DOMElement $personNode, array $identity): void
|
|
{
|
|
$bankNode = $this->getDirectChild($personNode, 'bankverbindung');
|
|
if (null === $bankNode) {
|
|
return;
|
|
}
|
|
|
|
$ibanNode = $this->getDirectChild($bankNode, 'iban');
|
|
if (null !== $ibanNode) {
|
|
$this->replaceNodeValue($ibanNode, $identity['iban']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<string, string> $identity
|
|
*/
|
|
private function replaceMetadataFields(\DOMElement $personNode, array $identity): void
|
|
{
|
|
if (true === $personNode->hasAttribute('id')) {
|
|
if ('kunde' === $personNode->tagName) {
|
|
$this->replaceAttributeValue($personNode, 'id', $identity['customerId']);
|
|
} elseif ('teilnehmer' === $personNode->tagName || 'anmelder' === $personNode->tagName) {
|
|
$this->replaceAttributeValue($personNode, 'id', $identity['personId']);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<string, string> $identity
|
|
*/
|
|
private function replaceRequestFields(\DOMElement $personNode, array $identity): void
|
|
{
|
|
$userNode = $this->getDirectChild($personNode, 'user');
|
|
if (null !== $userNode) {
|
|
$this->replaceNodeValue($userNode, $identity['userName']);
|
|
}
|
|
|
|
$keyNode = $this->getDirectChild($personNode, 'key');
|
|
if (null !== $keyNode) {
|
|
$this->replaceNodeValue($keyNode, $identity['requestKey']);
|
|
}
|
|
|
|
$emailNode = $this->getDirectChild($personNode, 'email');
|
|
if (null !== $emailNode) {
|
|
$this->replaceNodeValue($emailNode, $identity['email']);
|
|
}
|
|
|
|
$passwordNode = $this->getDirectChild($personNode, 'passwort');
|
|
if (null !== $passwordNode) {
|
|
$this->replaceNodeValue($passwordNode, $identity['requestPassword']);
|
|
}
|
|
}
|
|
|
|
private function replaceNodeValue(\DOMElement $node, string $replacement): void
|
|
{
|
|
if ('' === trim($node->textContent)) {
|
|
return;
|
|
}
|
|
|
|
$node->nodeValue = $replacement;
|
|
}
|
|
|
|
private function replaceAttributeValue(\DOMElement $node, string $attributeName, string $replacement): void
|
|
{
|
|
if ('' === trim((string) $node->getAttribute($attributeName))) {
|
|
return;
|
|
}
|
|
|
|
$node->setAttribute($attributeName, $replacement);
|
|
}
|
|
|
|
private function findPersonIdInAncestors(\DOMElement $node): ?string
|
|
{
|
|
foreach (['personid', 'idperson', 'idadresseperson'] as $tagName) {
|
|
$child = $this->getDirectChild($node, $tagName);
|
|
if (null !== $child) {
|
|
$value = trim($child->textContent);
|
|
if ('' !== $value) {
|
|
return $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
$parent = $node->parentNode;
|
|
if (false === $parent instanceof \DOMElement) {
|
|
return null;
|
|
}
|
|
|
|
return $this->findPersonIdInAncestors($parent);
|
|
}
|
|
|
|
private function getDirectChild(\DOMElement $parent, string $name): ?\DOMElement
|
|
{
|
|
foreach ($parent->childNodes as $childNode) {
|
|
if (false === $childNode instanceof \DOMElement) {
|
|
continue;
|
|
}
|
|
|
|
if ($childNode->tagName === $name) {
|
|
return $childNode;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
}
|