Skip to content

Commit

Permalink
Merge pull request z38#22 from z38/escaping
Browse files Browse the repository at this point in the history
Validate and escape all inputs
  • Loading branch information
z38 authored Mar 25, 2018
2 parents bf7da28 + bb7dbed commit 3eefd9f
Show file tree
Hide file tree
Showing 16 changed files with 377 additions and 61 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use Z38\SwissPayment\PostalAccount;
use Z38\SwissPayment\StructuredPostalAddress;
use Z38\SwissPayment\TransactionInformation\BankCreditTransfer;
use Z38\SwissPayment\TransactionInformation\IS1CreditTransfer;
use Z38\SwissPayment\UnstructuredPostalAddress;

$transaction1 = new BankCreditTransfer(
'instr-001',
Expand All @@ -43,7 +44,7 @@ $transaction2 = new IS1CreditTransfer(
'e2e-002',
new Money\CHF(30000), // CHF 300.00
'Finanzverwaltung Stadt Musterhausen',
new StructuredPostalAddress('Altstadt', '1a', '4998', 'Muserhausen'),
UnstructuredPostalAddress::sanitize('Altstadt 1a', '4998 Musterhausen'),
new PostalAccount('80-151-4')
);

Expand Down
6 changes: 4 additions & 2 deletions src/Z38/SwissPayment/FinancialInstitutionAddress.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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;
Expand Down
11 changes: 2 additions & 9 deletions src/Z38/SwissPayment/GeneralAccount.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@
*/
class GeneralAccount implements AccountInterface
{
const MAX_LENGTH = 34;

/**
* @var string
*/
Expand All @@ -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);
}

/**
Expand All @@ -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;
Expand Down
6 changes: 4 additions & 2 deletions src/Z38/SwissPayment/Message/AbstractMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Z38\SwissPayment\Message;

use Z38\SwissPayment\Text;

/**
* AbstractMessages eases message creation using DOM
*/
Expand Down Expand Up @@ -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;
}
Expand Down
17 changes: 10 additions & 7 deletions src/Z38/SwissPayment/Message/CustomerCreditTransfer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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);
Expand Down
11 changes: 7 additions & 4 deletions src/Z38/SwissPayment/PaymentInformation/PaymentInformation.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -71,18 +72,20 @@ 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)
{
if (!$debtorAgent instanceof BIC && !$debtorAgent instanceof IID) {
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;
}
Expand Down Expand Up @@ -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')));

Expand Down Expand Up @@ -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');
Expand Down
52 changes: 38 additions & 14 deletions src/Z38/SwissPayment/StructuredPostalAddress.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
class StructuredPostalAddress implements PostalAddressInterface
{
/**
* @var string
* @var string|null
*/
protected $street;

/**
* @var string
* @var string|null
*/
protected $buildingNo;

Expand All @@ -40,14 +40,38 @@ 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);
}

/**
* Creates a new instance after sanitizing all inputs
*
* @param string|null $street Street name or null
* @param string|null $buildingNo Building number or null
* @param string $postCode Postal code
* @param string $town Town name
* @param string $country Country code (ISO 3166-1 alpha-2)
*
* @return StructuredPostalAddress
*/
public static function sanitize($street, $buildingNo, $postCode, $town, $country = 'CH')
{
return new self(
Text::sanitizeOptional($street, 70),
Text::sanitizeOptional($buildingNo, 16),
Text::sanitize($postCode, 16),
Text::sanitize($town, 35),
$country
);
}

/**
Expand All @@ -57,15 +81,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;
}
Expand Down
114 changes: 114 additions & 0 deletions src/Z38/SwissPayment/Text.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php

namespace Z38\SwissPayment;

use DOMDocument;
use InvalidArgumentException;

class Text
{
const TEXT_NON_CH = '/[^A-Za-z0-9 .,:\'\/()?+\-!"#%&*;<>÷=@_$£[\]{}\` ́~àáâäçèéêëìíîïñòóôöùúûüýßÀÁÂÄÇÈÉÊËÌÍÎÏÒÓÔÖÙÚÛÜÑ]+/u';
const TEXT_NON_SWIFT = '/[^A-Za-z0-9 .,:\'\/()?+\-]+/';

/**
* Sanitizes and trims a string to conform to the Swiss character
* set.
*
* @param string|null $input
* @param int $maxLength
*
* @return string The sanitized string
*/
public static function sanitize($input, $maxLength)
{
$input = preg_replace('/\s+/', ' ', (string) $input);
$input = trim(preg_replace(self::TEXT_NON_CH, '', $input));

return function_exists('mb_substr') ? mb_substr($input, 0, $maxLength, 'UTF-8') : substr($input, 0, $maxLength);
}

/**
* Sanitizes and trims a string to conform to the Swiss character
* set.
*
* @param string|null $input
* @param int $maxLength
*
* @return string|null The sanitized string or null if it is empty.
*/
public static function sanitizeOptional($input, $maxLength)
{
$sanitized = self::sanitize($input, $maxLength);

return $sanitized !== '' ? $sanitized : null;
}

/**
* @internal
*/
public static function assertOptional($input, $maxLength)
{
if ($input === null) {
return null;
}

return self::assert($input, $maxLength);
}

/**
* @internal
*/
public static function assert($input, $maxLength)
{
return self::assertNotPattern($input, $maxLength, self::TEXT_NON_CH);
}

/**
* @internal
*/
public static function assertIdentifier($input)
{
$input = self::assertNotPattern($input, 35, self::TEXT_NON_SWIFT);
if ($input[0] === '/' || strpos($input, '//') !== false) {
throw new InvalidArgumentException('The identifier contains unallowed slashes.');
}

return $input;
}

/**
* @internal
*/
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 assertNotPattern($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;
}

/**
* @internal
*/
public static function xml(DOMDocument $doc, $tag, $content)
{
$element = $doc->createElement($tag);
$element->appendChild($doc->createTextNode($content));

return $element;
}
}
Loading

0 comments on commit 3eefd9f

Please sign in to comment.