diff --git a/src/Z38/SwissPayment/FinancialInstitutionAddress.php b/src/Z38/SwissPayment/FinancialInstitutionAddress.php index 77b4dc8..24f2af6 100644 --- a/src/Z38/SwissPayment/FinancialInstitutionAddress.php +++ b/src/Z38/SwissPayment/FinancialInstitutionAddress.php @@ -22,10 +22,12 @@ class FinancialInstitutionAddress implements FinancialInstitutionInterface * * @param string $name Name of the FI * @param PostalAddressInterface Address of the FI + * + * @throws \InvalidArgumentException When the name is invalid. */ public function __construct($name, PostalAddressInterface $address) { - $this->name = (string) $name; + $this->name = Text::assert($name, 70); $this->address = $address; } @@ -35,7 +37,7 @@ public function __construct($name, PostalAddressInterface $address) public function asDom(\DOMDocument $doc) { $xml = $doc->createElement('FinInstnId'); - $xml->appendChild($doc->createElement('Nm', $this->name)); + $xml->appendChild(Text::xml($doc, 'Nm', $this->name)); $xml->appendChild($this->address->asDom($doc)); return $xml; diff --git a/src/Z38/SwissPayment/GeneralAccount.php b/src/Z38/SwissPayment/GeneralAccount.php index fbe59c1..bdae481 100644 --- a/src/Z38/SwissPayment/GeneralAccount.php +++ b/src/Z38/SwissPayment/GeneralAccount.php @@ -10,8 +10,6 @@ */ class GeneralAccount implements AccountInterface { - const MAX_LENGTH = 34; - /** * @var string */ @@ -26,12 +24,7 @@ class GeneralAccount implements AccountInterface */ public function __construct($id) { - $stringId = (string) $id; - if (strlen($stringId) > self::MAX_LENGTH) { - throw new InvalidArgumentException('The account identifcation is too long.'); - } - - $this->id = $stringId; + $this->id = Text::assert($id, 34); } /** @@ -49,7 +42,7 @@ public function asDom(DOMDocument $doc) { $root = $doc->createElement('Id'); $other = $doc->createElement('Othr'); - $other->appendChild($doc->createElement('Id', $this->format())); + $other->appendChild(Text::xml($doc, 'Id', $this->format())); $root->appendChild($other); return $root; diff --git a/src/Z38/SwissPayment/Message/AbstractMessage.php b/src/Z38/SwissPayment/Message/AbstractMessage.php index adbbc2b..c927fd1 100644 --- a/src/Z38/SwissPayment/Message/AbstractMessage.php +++ b/src/Z38/SwissPayment/Message/AbstractMessage.php @@ -2,6 +2,8 @@ namespace Z38\SwissPayment\Message; +use Z38\SwissPayment\Text; + /** * AbstractMessages eases message creation using DOM */ @@ -85,8 +87,8 @@ protected function buildContactDetails(\DOMDocument $doc) { $root = $doc->createElement('CtctDtls'); - $root->appendChild($doc->createElement('Nm', $this->getSoftwareName())); - $root->appendChild($doc->createElement('Othr', $this->getSoftwareVersion())); + $root->appendChild(Text::xml($doc, 'Nm', $this->getSoftwareName())); + $root->appendChild(Text::xml($doc, 'Othr', $this->getSoftwareVersion())); return $root; } diff --git a/src/Z38/SwissPayment/Message/CustomerCreditTransfer.php b/src/Z38/SwissPayment/Message/CustomerCreditTransfer.php index 2493c15..80cf741 100644 --- a/src/Z38/SwissPayment/Message/CustomerCreditTransfer.php +++ b/src/Z38/SwissPayment/Message/CustomerCreditTransfer.php @@ -4,6 +4,7 @@ use Z38\SwissPayment\Money; use Z38\SwissPayment\PaymentInformation\PaymentInformation; +use Z38\SwissPayment\Text; /** * CustomerCreditTransfer represents a Customer Credit Transfer Initiation (pain.001) message @@ -35,11 +36,13 @@ class CustomerCreditTransfer extends AbstractMessage * * @param string $id Identifier of the message (should usually be unique over a period of at least 90 days) * @param string $initiatingParty Name of the initiating party + * + * @throws \InvalidArgumentException When any of the inputs contain invalid characters or are too long. */ public function __construct($id, $initiatingParty) { - $this->id = (string) $id; - $this->initiatingParty = (string) $initiatingParty; + $this->id = Text::assertIdentifier($id); + $this->initiatingParty = Text::assert($initiatingParty, 70); $this->payments = []; $this->creationTime = new \DateTime(); } @@ -104,12 +107,12 @@ protected function buildDom(\DOMDocument $doc) $root = $doc->createElement('CstmrCdtTrfInitn'); $header = $doc->createElement('GrpHdr'); - $header->appendChild($doc->createElement('MsgId', $this->id)); - $header->appendChild($doc->createElement('CreDtTm', $this->creationTime->format('Y-m-d\TH:i:sP'))); - $header->appendChild($doc->createElement('NbOfTxs', $transactionCount)); - $header->appendChild($doc->createElement('CtrlSum', $transactionSum->format())); + $header->appendChild(Text::xml($doc, 'MsgId', $this->id)); + $header->appendChild(Text::xml($doc, 'CreDtTm', $this->creationTime->format('Y-m-d\TH:i:sP'))); + $header->appendChild(Text::xml($doc, 'NbOfTxs', $transactionCount)); + $header->appendChild(Text::xml($doc, 'CtrlSum', $transactionSum->format())); $initgParty = $doc->createElement('InitgPty'); - $initgParty->appendChild($doc->createElement('Nm', $this->initiatingParty)); + $initgParty->appendChild(Text::xml($doc, 'Nm', $this->initiatingParty)); $initgParty->appendChild($this->buildContactDetails($doc)); $header->appendChild($initgParty); $root->appendChild($header); diff --git a/src/Z38/SwissPayment/PaymentInformation/PaymentInformation.php b/src/Z38/SwissPayment/PaymentInformation/PaymentInformation.php index 4f2ca2b..1d09748 100644 --- a/src/Z38/SwissPayment/PaymentInformation/PaymentInformation.php +++ b/src/Z38/SwissPayment/PaymentInformation/PaymentInformation.php @@ -7,6 +7,7 @@ use Z38\SwissPayment\IBAN; use Z38\SwissPayment\IID; use Z38\SwissPayment\Money; +use Z38\SwissPayment\Text; use Z38\SwissPayment\TransactionInformation\CreditTransfer; /** @@ -71,6 +72,8 @@ class PaymentInformation * @param string $debtorName Name of the debtor * @param BIC|IID $debtorAgent BIC or IID of the debtor's financial institution * @param IBAN $debtorIBAN IBAN of the debtor's account + * + * @throws \InvalidArgumentException When any of the inputs contain invalid characters or are too long. */ public function __construct($id, $debtorName, FinancialInstitutionInterface $debtorAgent, IBAN $debtorIBAN) { @@ -78,11 +81,11 @@ public function __construct($id, $debtorName, FinancialInstitutionInterface $deb throw new \InvalidArgumentException('The debtor agent must be an instance of BIC or IID.'); } - $this->id = (string) $id; + $this->id = Text::assertIdentifier($id); $this->transactions = []; $this->batchBooking = true; $this->executionDate = new \DateTime(); - $this->debtorName = (string) $debtorName; + $this->debtorName = Text::assert($debtorName, 70); $this->debtorAgent = $debtorAgent; $this->debtorIBAN = $debtorIBAN; } @@ -212,7 +215,7 @@ public function asDom(\DOMDocument $doc) { $root = $doc->createElement('PmtInf'); - $root->appendChild($doc->createElement('PmtInfId', $this->id)); + $root->appendChild(Text::xml($doc, 'PmtInfId', $this->id)); $root->appendChild($doc->createElement('PmtMtd', 'TRF')); $root->appendChild($doc->createElement('BtchBookg', ($this->batchBooking ? 'true' : 'false'))); @@ -241,7 +244,7 @@ public function asDom(\DOMDocument $doc) $root->appendChild($doc->createElement('ReqdExctnDt', $this->executionDate->format('Y-m-d'))); $debtor = $doc->createElement('Dbtr'); - $debtor->appendChild($doc->createElement('Nm', $this->debtorName)); + $debtor->appendChild(Text::xml($doc, 'Nm', $this->debtorName)); $root->appendChild($debtor); $debtorAccount = $doc->createElement('DbtrAcct'); diff --git a/src/Z38/SwissPayment/StructuredPostalAddress.php b/src/Z38/SwissPayment/StructuredPostalAddress.php index 8acca1f..a2c5d23 100644 --- a/src/Z38/SwissPayment/StructuredPostalAddress.php +++ b/src/Z38/SwissPayment/StructuredPostalAddress.php @@ -8,12 +8,12 @@ class StructuredPostalAddress implements PostalAddressInterface { /** - * @var string + * @var string|null */ protected $street; /** - * @var string + * @var string|null */ protected $buildingNo; @@ -40,14 +40,16 @@ class StructuredPostalAddress implements PostalAddressInterface * @param string $postCode Postal code * @param string $town Town name * @param string $country Country code (ISO 3166-1 alpha-2) + * + * @throws \InvalidArgumentException When the address contains invalid characters or is too long. */ public function __construct($street, $buildingNo, $postCode, $town, $country = 'CH') { - $this->street = (string) $street; - $this->buildingNo = (string) $buildingNo; - $this->postCode = (string) $postCode; - $this->town = (string) $town; - $this->country = (string) $country; + $this->street = Text::assertOptional($street, 70); + $this->buildingNo = Text::assertOptional($buildingNo, 16); + $this->postCode = Text::assert($postCode, 16); + $this->town = Text::assert($town, 35); + $this->country = Text::assertCountryCode($country); } /** @@ -57,15 +59,15 @@ public function asDom(\DOMDocument $doc) { $root = $doc->createElement('PstlAdr'); - if (strlen($this->street)) { - $root->appendChild($doc->createElement('StrtNm', $this->street)); + if ($this->street !== null) { + $root->appendChild(Text::xml($doc, 'StrtNm', $this->street)); } - if (strlen($this->buildingNo)) { - $root->appendChild($doc->createElement('BldgNb', $this->buildingNo)); + if ($this->buildingNo !== null) { + $root->appendChild(Text::xml($doc, 'BldgNb', $this->buildingNo)); } - $root->appendChild($doc->createElement('PstCd', $this->postCode)); - $root->appendChild($doc->createElement('TwnNm', $this->town)); - $root->appendChild($doc->createElement('Ctry', $this->country)); + $root->appendChild(Text::xml($doc, 'PstCd', $this->postCode)); + $root->appendChild(Text::xml($doc, 'TwnNm', $this->town)); + $root->appendChild(Text::xml($doc, 'Ctry', $this->country)); return $root; } diff --git a/src/Z38/SwissPayment/Text.php b/src/Z38/SwissPayment/Text.php new file mode 100644 index 0000000..fb88ad0 --- /dev/null +++ b/src/Z38/SwissPayment/Text.php @@ -0,0 +1,69 @@ +÷=@_$£[\]{}\` ́~àáâäçèéêëìíîïñòóôöùúûüýßÀÁÂÄÇÈÉÊËÌÍÎÏÒÓÔÖÙÚÛÜÑ]*$/u'; + const TEXT_SWIFT = '/^[A-Za-z0-9 .,:\'\/()?+\-]*$/'; + + public static function assertOptional($input, $maxLength) + { + if ($input === null) { + return null; + } + + return self::assert($input, $maxLength); + } + + public static function assert($input, $maxLength) + { + return self::assertPattern($input, $maxLength, self::TEXT_CH); + } + + public static function assertIdentifier($input) + { + $input = self::assertPattern($input, 35, self::TEXT_SWIFT); + if ($input[0] === '/' || strpos($input, '//') !== false) { + throw new InvalidArgumentException('The identifier contains unallowed slashes.'); + } + + return $input; + } + + public static function assertCountryCode($input) + { + if (!preg_match('/^[A-Z]{2}$/', $input)) { + throw new InvalidArgumentException('The country code is invalid.'); + } + + return $input; + } + + protected static function assertPattern($input, $maxLength, $pattern) + { + $length = function_exists('mb_strlen') ? mb_strlen($input, 'UTF-8') : strlen($input); + if (!is_string($input) || $length === 0 || $length > $maxLength) { + throw new InvalidArgumentException(sprintf('The string can not be empty or longer than %d characters.', $maxLength)); + } + if (!preg_match($pattern, $input)) { + throw new InvalidArgumentException('The string contains invalid characters.'); + } + + return $input; + } + + public static function xml(DOMDocument $doc, $tag, $content) + { + $element = $doc->createElement($tag); + $element->appendChild($doc->createTextNode($content)); + + return $element; + } +} diff --git a/src/Z38/SwissPayment/TransactionInformation/CreditTransfer.php b/src/Z38/SwissPayment/TransactionInformation/CreditTransfer.php index 7006b69..a6b131d 100644 --- a/src/Z38/SwissPayment/TransactionInformation/CreditTransfer.php +++ b/src/Z38/SwissPayment/TransactionInformation/CreditTransfer.php @@ -5,6 +5,7 @@ use Z38\SwissPayment\Money\Money; use Z38\SwissPayment\PaymentInformation\PaymentInformation; use Z38\SwissPayment\PostalAddressInterface; +use Z38\SwissPayment\Text; /** * CreditTransfer contains all the information about the beneficiary and further information about the transaction. @@ -64,13 +65,15 @@ abstract class CreditTransfer * @param Money $amount Amount of money to be transferred * @param string $creditorName Name of the creditor * @param PostalAddressInterface $creditorAddress Address of the creditor + * + * @throws \InvalidArgumentException When any of the inputs contain invalid characters or are too long. */ public function __construct($instructionId, $endToEndId, Money $amount, $creditorName, PostalAddressInterface $creditorAddress) { - $this->instructionId = (string) $instructionId; - $this->endToEndId = (string) $endToEndId; + $this->instructionId = Text::assertIdentifier($instructionId); + $this->endToEndId = Text::assertIdentifier($endToEndId); $this->amount = $amount; - $this->creditorName = (string) $creditorName; + $this->creditorName = Text::assert($creditorName, 70); $this->creditorAddress = $creditorAddress; } @@ -114,10 +117,12 @@ public function setPurpose(PurposeCode $purpose) * @param string|null $remittanceInformation * * @return CreditTransfer This credit transfer + * + * @throws \InvalidArgumentException When the information contains invalid characters or is too long. */ public function setRemittanceInformation($remittanceInformation) { - $this->remittanceInformation = $remittanceInformation; + $this->remittanceInformation = Text::assertOptional($remittanceInformation, 140); return $this; } @@ -155,8 +160,8 @@ protected function buildHeader(\DOMDocument $doc, PaymentInformation $paymentInf $root = $doc->createElement('CdtTrfTxInf'); $id = $doc->createElement('PmtId'); - $id->appendChild($doc->createElement('InstrId', $this->instructionId)); - $id->appendChild($doc->createElement('EndToEndId', $this->endToEndId)); + $id->appendChild(Text::xml($doc, 'InstrId', $this->instructionId)); + $id->appendChild(Text::xml($doc, 'EndToEndId', $this->endToEndId)); $root->appendChild($id); if (!$paymentInformation->hasPaymentTypeInformation() && ($this->localInstrument !== null || $this->serviceLevel !== null)) { @@ -193,7 +198,7 @@ protected function buildHeader(\DOMDocument $doc, PaymentInformation $paymentInf protected function buildCreditor(\DOMDocument $doc) { $creditor = $doc->createElement('Cdtr'); - $creditor->appendChild($doc->createElement('Nm', $this->creditorName)); + $creditor->appendChild(Text::xml($doc, 'Nm', $this->creditorName)); $creditor->appendChild($this->creditorAddress->asDom($doc)); return $creditor; @@ -224,7 +229,7 @@ protected function appendRemittanceInformation(\DOMDocument $doc, \DOMElement $t { if (!empty($this->remittanceInformation)) { $remittanceNode = $doc->createElement('RmtInf'); - $remittanceNode->appendChild($doc->createElement('Ustrd', $this->remittanceInformation)); + $remittanceNode->appendChild(Text::xml($doc, 'Ustrd', $this->remittanceInformation)); $transaction->appendChild($remittanceNode); } } diff --git a/src/Z38/SwissPayment/TransactionInformation/IS2CreditTransfer.php b/src/Z38/SwissPayment/TransactionInformation/IS2CreditTransfer.php index e52a577..a39e755 100644 --- a/src/Z38/SwissPayment/TransactionInformation/IS2CreditTransfer.php +++ b/src/Z38/SwissPayment/TransactionInformation/IS2CreditTransfer.php @@ -9,6 +9,7 @@ use Z38\SwissPayment\PaymentInformation\PaymentInformation; use Z38\SwissPayment\PostalAccount; use Z38\SwissPayment\PostalAddressInterface; +use Z38\SwissPayment\Text; /** * IS2CreditTransfer contains all the information about a IS 2-stage (type 2.2) transaction. @@ -51,7 +52,7 @@ public function __construct($instructionId, $endToEndId, Money\Money $amount, $c parent::__construct($instructionId, $endToEndId, $amount, $creditorName, $creditorAddress); $this->creditorIBAN = $creditorIBAN; - $this->creditorAgentName = (string) $creditorAgentName; + $this->creditorAgentName = Text::assert($creditorAgentName, 70); $this->creditorAgentPostal = $creditorAgentPostal; $this->localInstrument = 'CH03'; } @@ -65,7 +66,7 @@ public function asDom(DOMDocument $doc, PaymentInformation $paymentInformation) $creditorAgent = $doc->createElement('CdtrAgt'); $creditorAgentId = $doc->createElement('FinInstnId'); - $creditorAgentId->appendChild($doc->createElement('Nm', $this->creditorAgentName)); + $creditorAgentId->appendChild(Text::xml($doc, 'Nm', $this->creditorAgentName)); $creditorAgentIdOther = $doc->createElement('Othr'); $creditorAgentIdOther->appendChild($doc->createElement('Id', $this->creditorAgentPostal->format())); $creditorAgentId->appendChild($creditorAgentIdOther); diff --git a/src/Z38/SwissPayment/TransactionInformation/ISRCreditTransfer.php b/src/Z38/SwissPayment/TransactionInformation/ISRCreditTransfer.php index 17743c9..55ba4b4 100644 --- a/src/Z38/SwissPayment/TransactionInformation/ISRCreditTransfer.php +++ b/src/Z38/SwissPayment/TransactionInformation/ISRCreditTransfer.php @@ -10,6 +10,7 @@ use Z38\SwissPayment\PaymentInformation\PaymentInformation; use Z38\SwissPayment\PostalAccount; use Z38\SwissPayment\PostalAddressInterface; +use Z38\SwissPayment\Text; /** * ISRCreditTransfer contains all the information about a ISR (type 1) transaction. @@ -43,15 +44,15 @@ public function __construct($instructionId, $endToEndId, Money\Money $amount, IS )); } - if (!PostalAccount::validateCheckDigit($creditorReference)) { - throw new InvalidArgumentException('ISR creditor reference has an invalid check digit.'); + if (!preg_match('/^[0-9]{1,27}$/', $creditorReference) || !PostalAccount::validateCheckDigit($creditorReference)) { + throw new InvalidArgumentException('ISR creditor reference is invalid.'); } - $this->instructionId = (string) $instructionId; - $this->endToEndId = (string) $endToEndId; + $this->instructionId = Text::assertIdentifier($instructionId); + $this->endToEndId = Text::assertIdentifier($endToEndId); $this->amount = $amount; $this->creditorAccount = $creditorAccount; - $this->creditorReference = (string) $creditorReference; + $this->creditorReference = $creditorReference; $this->localInstrument = 'CH01'; } @@ -63,7 +64,7 @@ public function __construct($instructionId, $endToEndId, Money\Money $amount, IS */ public function setCreditorDetails($creditorName, PostalAddressInterface $creditorAddress) { - $this->creditorName = (string) $creditorName; + $this->creditorName = Text::assert($creditorName, 70); $this->creditorAddress = $creditorAddress; } diff --git a/src/Z38/SwissPayment/UnstructuredPostalAddress.php b/src/Z38/SwissPayment/UnstructuredPostalAddress.php index f3d1738..778a327 100644 --- a/src/Z38/SwissPayment/UnstructuredPostalAddress.php +++ b/src/Z38/SwissPayment/UnstructuredPostalAddress.php @@ -23,17 +23,19 @@ class UnstructuredPostalAddress implements PostalAddressInterface * @param string $addressLine1 Street name and house number * @param string $addressLine2 Postcode and town * @param string $country Country code (ISO 3166-1 alpha-2) + * + * @throws \InvalidArgumentException When the address contains invalid characters or is too long. */ public function __construct($addressLine1 = null, $addressLine2 = null, $country = 'CH') { $this->addressLines = []; if ($addressLine1 !== null) { - $this->addressLines[] = $addressLine1; + $this->addressLines[] = Text::assert($addressLine1, 70); } if ($addressLine2 !== null) { - $this->addressLines[] = $addressLine2; + $this->addressLines[] = Text::assert($addressLine2, 70); } - $this->country = (string) $country; + $this->country = Text::assertCountryCode($country); } /** @@ -43,9 +45,9 @@ public function asDom(\DOMDocument $doc) { $root = $doc->createElement('PstlAdr'); - $root->appendChild($doc->createElement('Ctry', $this->country)); + $root->appendChild(Text::xml($doc, 'Ctry', $this->country)); foreach ($this->addressLines as $line) { - $root->appendChild($doc->createElement('AdrLine', $line)); + $root->appendChild(Text::xml($doc, 'AdrLine', $line)); } return $root; diff --git a/tests/Z38/SwissPayment/Tests/Message/CustomerCreditTransferTest.php b/tests/Z38/SwissPayment/Tests/Message/CustomerCreditTransferTest.php index 5e2aaa2..6c8eb73 100644 --- a/tests/Z38/SwissPayment/Tests/Message/CustomerCreditTransferTest.php +++ b/tests/Z38/SwissPayment/Tests/Message/CustomerCreditTransferTest.php @@ -149,7 +149,7 @@ protected function buildMessage() 'instr-012', 'e2e-012', new Money\CHF(50000), // CHF 500.00 - 'Fritz Bischof', + 'Meier & Söhne AG', new StructuredPostalAddress('Dorfstrasse', '17', '9911', 'Musterwald'), new PostalAccount('60-9-9') ); diff --git a/tests/Z38/SwissPayment/Tests/TextTest.php b/tests/Z38/SwissPayment/Tests/TextTest.php new file mode 100644 index 0000000..b646e7f --- /dev/null +++ b/tests/Z38/SwissPayment/Tests/TextTest.php @@ -0,0 +1,76 @@ +assertSame('abcd', Text::assert('abcd', 4)); + } + + public function testAssertUnicode() + { + $this->assertSame('÷ß', Text::assert('÷ß', 2)); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testAssertInvalid() + { + Text::assert('°', 10); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testAssertIdentiferBeginsWithSlash() + { + Text::assertIdentifier('/abc'); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testAssertIdentiferContainsDoubleSlash() + { + Text::assertIdentifier('ab//c'); + } + + public function testAssertIdentiferContainsSlash() + { + $this->assertSame('ab/c', Text::assertIdentifier('ab/c')); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testAssertCountryCodeUppercase() + { + Text::assertCountryCode('ch'); + } + + public function testXml() + { + $doc = new DOMDocument(); + + $element = Text::xml($doc, 'abc', '<>&'); + + $this->assertSame('<>&', $doc->saveXML($element)); + } +}