Skip to content

Commit

Permalink
Merge pull request #255 from sprain/ensure-characters
Browse files Browse the repository at this point in the history
Only keep supported characters within qr codes
  • Loading branch information
sprain committed Jun 27, 2024
2 parents 7910c9c + ce2fa09 commit 5490e91
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 5 deletions.
1 change: 1 addition & 0 deletions src/DataGroup/Element/PaymentReference.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
16 changes: 14 additions & 2 deletions src/QrBill.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ final class QrBill implements SelfValidatableInterface
private ?PaymentReference $paymentReference = null;
private ?AdditionalInformation $additionalInformation = null;

/** @var array<string, string> */
private array $unsupportedCharacterReplacements = [];

/** @var AlternativeScheme[] */
private array $alternativeSchemes = [];

Expand Down Expand Up @@ -163,8 +166,16 @@ public function addAlternativeScheme(AlternativeScheme $alternativeScheme): self
}

/**
* @throws InvalidQrBillDataException
* @param array<string, string> $unsupportedCharacterReplacements
* @return $this
*/
public function setUnsupportedCharacterReplacements(array $unsupportedCharacterReplacements): self
{
$this->unsupportedCharacterReplacements = $unsupportedCharacterReplacements;

return $this;
}

public function getQrCode(?string $fileFormat = null): QrCode
{
if (!$this->isValid()) {
Expand All @@ -175,7 +186,8 @@ public function getQrCode(?string $fileFormat = null): QrCode

return QrCode::create(
$this->getQrCodeContent(),
$fileFormat
$fileFormat,
$this->unsupportedCharacterReplacements
);
}

Expand Down
47 changes: 44 additions & 3 deletions src/QrCode/QrCode.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -34,17 +36,33 @@ final class QrCode
/** @var array<string, bool> $writerOptions */
private array $writerOptions = [SvgWriter::WRITER_OPTION_FORCE_XLINK_HREF => true];

public static function create(string $data, string $fileFormat = null): self
/**
* @param string $data
* @param string|null $fileFormat
* @param array<string, string> $unsupportedCharacterReplacements
* @return self
* @throws UnsupportedFileExtensionException
*/
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)
/**
* @param string $data
* @param string $fileFormat
* @param array<string, string> $unsupportedCharacterReplacements
* @throws UnsupportedFileExtensionException
*/
private function __construct(string $data, string $fileFormat, array $unsupportedCharacterReplacements)
{
$data = $this->replaceUnsupportedCharacters($data, $unsupportedCharacterReplacements);
$data = $this->cleanUnsupportedCharacters($data);

if (class_exists(ErrorCorrectionLevel\ErrorCorrectionLevelMedium::class)) {
// Endroid 4.x
$this->qrCode = BaseQrCode::create($data)
Expand Down Expand Up @@ -127,6 +145,29 @@ public function avoidCompactSvgs(): void
}
}

/**
* @param string $data
* @param array<string, string> $unsupportedCharacterReplacements
* @return string
*/
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
{
$supportedCharacters = self::SUPPORTED_CHARACTERS . "\\n";

return preg_replace("/([^$supportedCharacters])/u", '', $data);
}

private function setWriterByExtension(string $extension): void
{
if (!in_array($extension, self::SUPPORTED_FILE_FORMATS)) {
Expand Down
39 changes: 39 additions & 0 deletions tests/QrBillTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,45 @@ 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 = [
'«' => '"',
'»' => '"',
];

$qrBill->setUnsupportedCharacterReplacements($unsupportedCharacterReplacements);

$this->assertStringContainsString(
'Team "We are the Champions!"',
$qrBill->getQrCode()->getText()
);
}

public function testCatchInvalidData()
{
$this->expectException(InvalidQrBillDataException::class);
Expand Down
61 changes: 61 additions & 0 deletions tests/QrCode/QrCodeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,65 @@ public function stringProvider()
]
];
}

/**
* @dataProvider replacementCharactersProvider
*/
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);

$this->assertEquals(
$expectedString,
$qrCode->getText()
);
}

public function unsupportedCharactersProvider(): array
{
return [
'keepAllAllowedCharacters' => [
'providedString' => 'a-zA-Z0-9.,;:\'+-/()?*[]{}|`´~!"#%&<>÷=@_$£^àáâäçèéêëìíîïñòóôöùúûüýßÀÁÂÄÇÈÉÊËÌÍÎÏÒÓÔÖÙÚÛÜÑ',
'expectedString' => 'a-zA-Z0-9.,;:\'+-/()?*[]{}|`´~!"#%&<>÷=@_$£^àáâäçèéêëìíîïñòóôöùúûüýßÀÁÂÄÇÈÉÊËÌÍÎÏÒÓÔÖÙÚÛÜÑ'
],
'removeUnallowedCharacters' => [
'providedString' => '«This is a test!»',
'expectedString' => 'This is a test!'
],
];
}
}
15 changes: 15 additions & 0 deletions tests/TestQrBillCreatorTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit 5490e91

Please sign in to comment.