> */ 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 */ 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 $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 $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 $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 $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 $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 $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 $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; } }