From eb158937c3ce44bb94937c16c07ee12962efff0f Mon Sep 17 00:00:00 2001 From: Manuel Reinhard Date: Sun, 23 Jun 2024 20:21:24 +0200 Subject: [PATCH 1/4] Remove unallowed characters in qr code --- src/QrCode/QrCode.php | 9 +++++++++ tests/QrCode/QrCodeTest.php | 27 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/QrCode/QrCode.php b/src/QrCode/QrCode.php index 95fa5841..af0569dc 100644 --- a/src/QrCode/QrCode.php +++ b/src/QrCode/QrCode.php @@ -45,6 +45,8 @@ public static function create(string $data, string $fileFormat = null): self private function __construct(string $data, string $fileFormat) { + $data = $this->cleanUnsupportedCharacters($data); + if (class_exists(ErrorCorrectionLevel\ErrorCorrectionLevelMedium::class)) { // Endroid 4.x $this->qrCode = BaseQrCode::create($data) @@ -127,6 +129,13 @@ public function avoidCompactSvgs(): void } } + private function cleanUnsupportedCharacters(string $data): string + { + $pattern = '/([^a-zA-Z0-9.,;:\'"+\-\/()?*\[\]{}|`´~ !^#%&<>÷=@_$£àáâäçèéêëìíîïñòóôöùúûüýßÀÁÂÄÇÈÉÊËÌÍÎÏÒÓÔÖÙÚÛÜÑ\\n])/u'; + + return preg_replace($pattern, '', $data); + } + private function setWriterByExtension(string $extension): void { if (!in_array($extension, self::SUPPORTED_FILE_FORMATS)) { diff --git a/tests/QrCode/QrCodeTest.php b/tests/QrCode/QrCodeTest.php index d3c855d5..dd54a322 100644 --- a/tests/QrCode/QrCodeTest.php +++ b/tests/QrCode/QrCodeTest.php @@ -118,4 +118,31 @@ public function stringProvider() ] ]; } + + /** + * @dataProvider invalidCharactersCodeProvider + */ + public function testItRemovesInvalidCharacters(string $providedString, string $expectedString): void + { + $qrCode = QrCode::create($providedString); + + $this->assertEquals( + $expectedString, + $qrCode->getText() + ); + } + + public function invalidCharactersCodeProvider(): array + { + return [ + 'keepAllAllowedCharacters' => [ + 'providedString' => 'a-zA-Z0-9.,;:\'+-/()?*[]{}|`´~!"#%&<>÷=@_$£^àáâäçèéêëìíîïñòóôöùúûüýßÀÁÂÄÇÈÉÊËÌÍÎÏÒÓÔÖÙÚÛÜÑ', + 'expectedString' => 'a-zA-Z0-9.,;:\'+-/()?*[]{}|`´~!"#%&<>÷=@_$£^àáâäçèéêëìíîïñòóôöùúûüýßÀÁÂÄÇÈÉÊËÌÍÎÏÒÓÔÖÙÚÛÜÑ' + ], + 'removeUnallowedCharacters' => [ + 'providedString' => '«This is a test!»', + 'expectedString' => 'This is a test!' + ], + ]; + } } From 0e99f88d6d938841812bb48a6c5f3171962b5614 Mon Sep 17 00:00:00 2001 From: Manuel Reinhard Date: Sun, 23 Jun 2024 20:54:13 +0200 Subject: [PATCH 2/4] Allow to inject unsupported character replacements --- src/QrBill.php | 5 ++-- src/QrCode/QrCode.php | 24 +++++++++++++++---- tests/QrBillTest.php | 37 +++++++++++++++++++++++++++++ tests/QrCode/QrCodeTest.php | 40 +++++++++++++++++++++++++++++--- tests/TestQrBillCreatorTrait.php | 15 ++++++++++++ 5 files changed, 111 insertions(+), 10 deletions(-) diff --git a/src/QrBill.php b/src/QrBill.php index 477face6..775744e9 100644 --- a/src/QrBill.php +++ b/src/QrBill.php @@ -165,7 +165,7 @@ public function addAlternativeScheme(AlternativeScheme $alternativeScheme): self /** * @throws InvalidQrBillDataException */ - public function getQrCode(?string $fileFormat = null): QrCode + public function getQrCode(?string $fileFormat = null, array $unsupportedCharacterReplacements = []): QrCode { if (!$this->isValid()) { throw new InvalidQrBillDataException( @@ -175,7 +175,8 @@ public function getQrCode(?string $fileFormat = null): QrCode return QrCode::create( $this->getQrCodeContent(), - $fileFormat + $fileFormat, + $unsupportedCharacterReplacements ); } diff --git a/src/QrCode/QrCode.php b/src/QrCode/QrCode.php index af0569dc..ecb58a6a 100644 --- a/src/QrCode/QrCode.php +++ b/src/QrCode/QrCode.php @@ -18,6 +18,8 @@ final class QrCode public const FILE_FORMAT_PNG = 'png'; public const FILE_FORMAT_SVG = 'svg'; + public const SUPPORTED_CHARACTERS = 'a-zA-Z0-9.,;:\'"+\-\/()?*\[\]{}|`´~ !^#%&<>÷=@_$£àáâäçèéêëìíîïñòóôöùúûüýßÀÁÂÄÇÈÉÊËÌÍÎÏÒÓÔÖÙÚÛÜÑ'; + private const SUPPORTED_FILE_FORMATS = [ self::FILE_FORMAT_PNG, self::FILE_FORMAT_SVG @@ -34,17 +36,18 @@ final class QrCode /** @var array $writerOptions */ private array $writerOptions = [SvgWriter::WRITER_OPTION_FORCE_XLINK_HREF => true]; - public static function create(string $data, string $fileFormat = null): self + public static function create(string $data, string $fileFormat = null, array $unsupportedCharacterReplacements = []): self { if (null === $fileFormat) { $fileFormat = self::FILE_FORMAT_SVG; } - return new self($data, $fileFormat); + return new self($data, $fileFormat, $unsupportedCharacterReplacements); } - private function __construct(string $data, string $fileFormat) + private function __construct(string $data, string $fileFormat, array $unsupportedCharacterReplacements) { + $data = $this->replaceUnsupportedCharacters($data, $unsupportedCharacterReplacements); $data = $this->cleanUnsupportedCharacters($data); if (class_exists(ErrorCorrectionLevel\ErrorCorrectionLevelMedium::class)) { @@ -129,11 +132,22 @@ public function avoidCompactSvgs(): void } } + private function replaceUnsupportedCharacters(string $data, array $unsupportedCharacterReplacements): string + { + foreach ($unsupportedCharacterReplacements as $character => $replacement) { + if (preg_match("/([^" . self::SUPPORTED_CHARACTERS . "])/u", $character)) { + $data = str_replace($character, $replacement, $data); + } + } + + return $data; + } + private function cleanUnsupportedCharacters(string $data): string { - $pattern = '/([^a-zA-Z0-9.,;:\'"+\-\/()?*\[\]{}|`´~ !^#%&<>÷=@_$£àáâäçèéêëìíîïñòóôöùúûüýßÀÁÂÄÇÈÉÊËÌÍÎÏÒÓÔÖÙÚÛÜÑ\\n])/u'; + $supportedCharacters = self::SUPPORTED_CHARACTERS . "\\n"; - return preg_replace($pattern, '', $data); + return preg_replace("/([^$supportedCharacters])/u", '', $data); } private function setWriterByExtension(string $extension): void diff --git a/tests/QrBillTest.php b/tests/QrBillTest.php index ff95bb62..4fd8d85a 100644 --- a/tests/QrBillTest.php +++ b/tests/QrBillTest.php @@ -237,6 +237,43 @@ public function testMaximumTwoAlternativeSchemesAreAllowed() $this->assertFalse($qrBill->isValid()); } + public function testItReplacesUnsupportedCharacters() + { + $qrBill = $this->createQrBill([ + 'header', + 'creditorInformationQrIban', + 'creditorWithUnsupportedCharacters', + 'paymentAmountInformation', + 'paymentReferenceQr', + ]); + + $this->assertStringContainsString( + 'Team We are the Champions!', + $qrBill->getQrCode()->getText() + ); + } + + public function testItConsidersReplacementCharacters() + { + $qrBill = $this->createQrBill([ + 'header', + 'creditorInformationQrIban', + 'creditorWithUnsupportedCharacters', + 'paymentAmountInformation', + 'paymentReferenceQr', + ]); + + $unsupportedCharacterReplacements = [ + '«' => '"', + '»' => '"', + ]; + + $this->assertStringContainsString( + 'Team "We are the Champions!"', + $qrBill->getQrCode(null, $unsupportedCharacterReplacements)->getText() + ); + } + public function testCatchInvalidData() { $this->expectException(InvalidQrBillDataException::class); diff --git a/tests/QrCode/QrCodeTest.php b/tests/QrCode/QrCodeTest.php index dd54a322..8b04fbab 100644 --- a/tests/QrCode/QrCodeTest.php +++ b/tests/QrCode/QrCodeTest.php @@ -120,9 +120,43 @@ public function stringProvider() } /** - * @dataProvider invalidCharactersCodeProvider + * @dataProvider replacementCharactersProvider */ - public function testItRemovesInvalidCharacters(string $providedString, string $expectedString): void + public function testItReplacesUnsupportedCharacters(string $providedString, array $replacements, string $expectedString): void + { + $qrCode = QrCode::create($providedString, null, $replacements); + + $this->assertEquals( + $expectedString, + $qrCode->getText() + ); + } + + public function replacementCharactersProvider(): array + { + return [ + 'replaceSpecificUnsupportedCharacters' => [ + 'providedString' => '«This is a test!»', + 'replacements' => [ + '«' => '"', + '»' => '"' + ], + 'expectedString' => '"This is a test!"' + ], + 'ignoreReplacementsOfSupportedCharacters' => [ + 'providedString' => '«This is a test!»', + 'replacements' => [ + 't' => 'a', + ], + 'expectedString' => 'This is a test!' + ], + ]; + } + + /** + * @dataProvider unsupportedCharactersProvider + */ + public function testItRemovesUnsupportedCharacters(string $providedString, string $expectedString): void { $qrCode = QrCode::create($providedString); @@ -132,7 +166,7 @@ public function testItRemovesInvalidCharacters(string $providedString, string $e ); } - public function invalidCharactersCodeProvider(): array + public function unsupportedCharactersProvider(): array { return [ 'keepAllAllowedCharacters' => [ diff --git a/tests/TestQrBillCreatorTrait.php b/tests/TestQrBillCreatorTrait.php index f277938e..9727dbba 100644 --- a/tests/TestQrBillCreatorTrait.php +++ b/tests/TestQrBillCreatorTrait.php @@ -218,6 +218,11 @@ public function creditor(QrBill &$qrBill) $qrBill->setCreditor($this->structuredAddress()); } + public function creditorWithUnsupportedCharacters(QrBill &$qrBill) + { + $qrBill->setCreditor($this->addressWithUnsupportedCharacters()); + } + public function creditorMediumLong(QrBill &$qrBill) { $qrBill->setCreditor($this->mediumLongAddress()); @@ -392,6 +397,16 @@ public function longAddress() ); } + public function addressWithUnsupportedCharacters() + { + return CombinedAddress::create( + 'Team «We are the Champions!»', + 'Rue examplaire 22a', + '1000 Lausanne', + 'CH' + ); + } + public function invalidAddress() { return CombinedAddress::create( From 60f5b5d7b4b806a031f6452b9a78fd844649541d Mon Sep 17 00:00:00 2001 From: Manuel Reinhard Date: Sun, 23 Jun 2024 20:58:27 +0200 Subject: [PATCH 3/4] Improve injection of unsupportedCharacterReplacements --- src/QrBill.php | 17 ++++++++++++++--- src/QrCode/QrCode.php | 18 ++++++++++++++++++ tests/QrBillTest.php | 4 +++- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/QrBill.php b/src/QrBill.php index 775744e9..5e23b3e9 100644 --- a/src/QrBill.php +++ b/src/QrBill.php @@ -34,6 +34,9 @@ final class QrBill implements SelfValidatableInterface private ?PaymentReference $paymentReference = null; private ?AdditionalInformation $additionalInformation = null; + /** @var array */ + private array $unsupportedCharacterReplacements = []; + /** @var AlternativeScheme[] */ private array $alternativeSchemes = []; @@ -163,9 +166,17 @@ public function addAlternativeScheme(AlternativeScheme $alternativeScheme): self } /** - * @throws InvalidQrBillDataException + * @param array $unsupportedCharacterReplacements + * @return $this */ - public function getQrCode(?string $fileFormat = null, array $unsupportedCharacterReplacements = []): QrCode + public function setUnsupportedCharacterReplacements(array $unsupportedCharacterReplacements): self + { + $this->unsupportedCharacterReplacements = $unsupportedCharacterReplacements; + + return $this; + } + + public function getQrCode(?string $fileFormat = null): QrCode { if (!$this->isValid()) { throw new InvalidQrBillDataException( @@ -176,7 +187,7 @@ public function getQrCode(?string $fileFormat = null, array $unsupportedCharacte return QrCode::create( $this->getQrCodeContent(), $fileFormat, - $unsupportedCharacterReplacements + $this->unsupportedCharacterReplacements ); } diff --git a/src/QrCode/QrCode.php b/src/QrCode/QrCode.php index ecb58a6a..c02769dc 100644 --- a/src/QrCode/QrCode.php +++ b/src/QrCode/QrCode.php @@ -36,6 +36,13 @@ final class QrCode /** @var array $writerOptions */ private array $writerOptions = [SvgWriter::WRITER_OPTION_FORCE_XLINK_HREF => true]; + /** + * @param string $data + * @param string|null $fileFormat + * @param array $unsupportedCharacterReplacements + * @return self + * @throws UnsupportedFileExtensionException + */ public static function create(string $data, string $fileFormat = null, array $unsupportedCharacterReplacements = []): self { if (null === $fileFormat) { @@ -45,6 +52,12 @@ public static function create(string $data, string $fileFormat = null, array $un return new self($data, $fileFormat, $unsupportedCharacterReplacements); } + /** + * @param string $data + * @param string $fileFormat + * @param array $unsupportedCharacterReplacements + * @throws UnsupportedFileExtensionException + */ private function __construct(string $data, string $fileFormat, array $unsupportedCharacterReplacements) { $data = $this->replaceUnsupportedCharacters($data, $unsupportedCharacterReplacements); @@ -132,6 +145,11 @@ public function avoidCompactSvgs(): void } } + /** + * @param string $data + * @param array $unsupportedCharacterReplacements + * @return string + */ private function replaceUnsupportedCharacters(string $data, array $unsupportedCharacterReplacements): string { foreach ($unsupportedCharacterReplacements as $character => $replacement) { diff --git a/tests/QrBillTest.php b/tests/QrBillTest.php index 4fd8d85a..7b063de3 100644 --- a/tests/QrBillTest.php +++ b/tests/QrBillTest.php @@ -268,9 +268,11 @@ public function testItConsidersReplacementCharacters() '»' => '"', ]; + $qrBill->setUnsupportedCharacterReplacements($unsupportedCharacterReplacements); + $this->assertStringContainsString( 'Team "We are the Champions!"', - $qrBill->getQrCode(null, $unsupportedCharacterReplacements)->getText() + $qrBill->getQrCode()->getText() ); } From ce2fa0964de027c00b5f57c24a9d810429d47bd4 Mon Sep 17 00:00:00 2001 From: Manuel Reinhard Date: Sun, 23 Jun 2024 21:18:01 +0200 Subject: [PATCH 4/4] Fix static code analysis --- src/DataGroup/Element/PaymentReference.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/DataGroup/Element/PaymentReference.php b/src/DataGroup/Element/PaymentReference.php index 2e58da07..8cf287f1 100644 --- a/src/DataGroup/Element/PaymentReference.php +++ b/src/DataGroup/Element/PaymentReference.php @@ -93,6 +93,7 @@ public static function loadValidatorMetadata(ClassMetadata $metadata): void ]); $metadata->addPropertyConstraints('reference', [ + /** @phpstan-ignore-next-line because docs do not match bc compatible syntax */ new Assert\Type([ 'type' => 'alnum', 'groups' => [self::TYPE_QR]