Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d30c0c6
version bump
nemiah Mar 25, 2025
6a43c76
Update HICMEv1.php
dhm80 Jun 16, 2025
dbcd9f7
Update HICSEv1.php
dhm80 Jun 16, 2025
ccdc258
Merge pull request #479 from ansas/master
nemiah Jul 3, 2025
184adc9
Clarify post condition of submitTan()
Philipp91 Aug 9, 2025
8719e94
Merge pull request #486 from Philipp91/patch-9
nemiah Aug 9, 2025
cb27985
Improve documentation around FinTs::persist()
Philipp91 Aug 16, 2025
35f2d2a
Demonstrate combined use of checkDecoupledSubmission and persist in c…
Philipp91 Aug 16, 2025
376bb31
adds a failing test
leobeal Aug 26, 2025
2c2a6af
tests workflow
leobeal Aug 26, 2025
dedb2b7
Introduces lazy quantifiers
leobeal Aug 26, 2025
5713e50
Allow minor version update on composer
leobeal Aug 26, 2025
1fb1c26
removes composer validate
leobeal Aug 26, 2025
d94129d
Better support for new pain formats
ampaze Sep 1, 2025
7af5869
Better support for new pain formats
ampaze Sep 1, 2025
8bf191e
Merge pull request #487 from Philipp91/gh485
nemiah Sep 1, 2025
d3301f5
Merge pull request #491 from leobeal/chore-allow-minors
nemiah Sep 1, 2025
e800766
Merge pull request #489 from leobeal/send-sepa-direct-debit-fix
nemiah Sep 1, 2025
43d321c
Better support for new pain formats
ampaze Sep 2, 2025
20adce7
Merge pull request #495 from ampaze/better-schema-check
nemiah Sep 2, 2025
01b2960
Use the UnterstuetzteSEPADatenformateTrait in all Actions to avoid pa…
ampaze Sep 3, 2025
297a999
Merge pull request #496 from ampaze/better-schema-check
nemiah Sep 10, 2025
9b8c0d3
Update ParameterSEPAInstantPaymentZahlungV2.php
vmario89 Oct 8, 2025
dca2bd5
Update HIIPZSv2.php
vmario89 Oct 8, 2025
962deb1
Merge pull request #504 from vmario89/master
nemiah Oct 8, 2025
9b76734
Remove @inheritdoc annotations
Philipp91 Oct 13, 2025
a6929b6
Fix CS Fixer warnings on its own config file
Philipp91 Oct 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# .github/workflows/tests.yml
name: tests

on:
push:
branches:
- master
pull_request:

jobs:
phpunit:
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
php: [ '8.0', '8.1', '8.2', '8.3', '8.4' ]

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: mbstring

- name: Install dependencies
run: composer install --no-interaction --prefer-dist --no-progress

- name: Run PHPUnit
run: ./vendor/bin/phpunit
20 changes: 10 additions & 10 deletions .php-cs-fixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,19 @@

// But then we have some exclusions, i.e. we disable some of the checks/rules from Symfony:
// Logic
'yoda_style' => FALSE, // Allow both Yoda-style and regular comparisons.
'yoda_style' => false, // Allow both Yoda-style and regular comparisons.

// Whitespace
'blank_line_before_statement' => FALSE, // Don't put blank lines before `return` statements.
'concat_space' => FALSE, // Allow spaces around string concatenation operator.
'blank_line_after_opening_tag' => FALSE, // Allow file-level @noinspection suppressions to live on the `<?php` line.
'single_line_throw' => FALSE, // Allow `throw` statements to span multiple lines.
'blank_line_before_statement' => false, // Don't put blank lines before `return` statements.
'concat_space' => false, // Allow spaces around string concatenation operator.
'blank_line_after_opening_tag' => false, // Allow file-level @noinspection suppressions to live on the `<?php` line.
'single_line_throw' => false, // Allow `throw` statements to span multiple lines.

// phpDoc
'phpdoc_align' => FALSE, // Don't add spaces within phpDoc just to make parameter names / descriptions align.
'phpdoc_annotation_without_dot' => FALSE, // Allow terminating dot on @param and such.
'phpdoc_no_alias_tag' => FALSE, // Allow @link in addition to @see.
'phpdoc_separation' => FALSE, // Don't put blank line between @params, @throws and @return.
'phpdoc_summary' => FALSE, // Don't force terminating dot on the first line.
'phpdoc_align' => false, // Don't add spaces within phpDoc just to make parameter names / descriptions align.
'phpdoc_annotation_without_dot' => false, // Allow terminating dot on @param and such.
'phpdoc_no_alias_tag' => false, // Allow @link in addition to @see.
'phpdoc_separation' => false, // Don't put blank line between @params, @throws and @return.
'phpdoc_summary' => false, // Don't force terminating dot on the first line.
])
->setFinder($finder);
23 changes: 21 additions & 2 deletions Samples/browser.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,37 @@ function handleRequest(\stdClass $request, \Fhp\FinTs $fints)
return ['result' => 'success'];
case 'submitTan':
$fints->submitTan(unserialize($persistedAction), $request->tan);
$persistedAction = null;
return ['result' => 'success'];
case 'checkDecoupledSubmission':
if ($fints->checkDecoupledSubmission(unserialize($persistedAction))) {
$persistedAction = null;
return ['result' => 'success'];
} else {
// IMPORTANT: If you pull this example code apart in your real application code, remember that after
// calling checkDecoupledSubmission(), you need to call $fints->persist() again, just like this
// example code will do after we return from handleRequest() here.
return ['result' => 'ongoing'];
}
case 'getBalances':
$getAccounts = \Fhp\Action\GetSEPAAccounts::create();
$fints->execute($getAccounts); // We assume that needsTan() is always false here.
$fints->execute($getAccounts);
if ($getAccounts->needsTan()) {
throw new \Fhp\UnsupportedException(
"This simple example code does not support strong authentication on GetSEPAAccounts calls. " .
"But in your real application, you can do so analogously to how login() is handled above."
);
}

$getBalances = \Fhp\Action\GetBalance::create($getAccounts->getAccounts()[0], true);
$fints->execute($getBalances); // We assume that needsTan() is always false here.
$fints->execute($getBalances);
if ($getAccounts->needsTan()) {
throw new \Fhp\UnsupportedException(
"This simple example code does not support strong authentication on GetBalance calls. " .
"But in your real application, you can do so analogously to how login() is handled above."
);
}

$balances = [];
foreach ($getBalances->getBalances() as $balance) {
$sdo = $balance->getGebuchterSaldo();
Expand Down
62 changes: 49 additions & 13 deletions Samples/login.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
* on its user interfaces. The implementation does not have to be in a single function like this -- it can be inlined
* with the calling code, or live elsewhere. The TAN/confirmation can be obtained while the same PHP script is still
* running (i.e. handleStrongAuthentication() is a blocking function that only returns once the authentication is done,
* which is useful for a CLI application), but it is also possible to interrupt the PHP execution entirely while asking
* which is useful for a CLI application), but it is also possible to interrupt the PHP process entirely while asking
* for the TAN/confirmation and resume it later (which is useful for a web application).
*
* @param \Fhp\BaseAction $action Some action that requires strong authentication.
Expand All @@ -51,6 +51,7 @@ function handleStrongAuthentication(\Fhp\BaseAction $action)
/**
* This function handles strong authentication for the case where the user needs to enter a TAN into the PHP
* application.
*
* @param \Fhp\BaseAction $action Some action that requires a TAN.
* @throws CurlException|UnexpectedResponseException|ServerException See {@link FinTs::execute()} for details.
*/
Expand Down Expand Up @@ -93,7 +94,7 @@ function handleTan(Fhp\BaseAction $action)

// Optional: Instead of printing the above to the console, you can relay the information (challenge and TAN medium)
// to the user in any other way (through your REST API, a push notification, ...). If waiting for the TAN requires
// you to interrupt this PHP execution and the TAN will arrive in a fresh (HTTP/REST/...) request, you can do so:
// you to interrupt this PHP process and the TAN will arrive in a fresh (HTTP/REST/...) request, you can do so:
if ($optionallyPersistEverything = false) {
$persistedAction = serialize($action);
$persistedFints = $fints->persist();
Expand All @@ -116,7 +117,7 @@ function handleTan(Fhp\BaseAction $action)
echo "Please enter the TAN:\n";
$tan = trim(fgets(STDIN));

// Optional: If the state was persisted above, we can restore it now (imagine this is a new PHP execution).
// Optional: If the state was persisted above, we can restore it now (imagine this is a new PHP process).
if ($optionallyPersistEverything) {
$restoredState = file_get_contents(__DIR__ . '/state.txt');
list($persistedInstance, $persistedAction) = unserialize($restoredState);
Expand All @@ -130,14 +131,15 @@ function handleTan(Fhp\BaseAction $action)

/**
* This function handles strong authentication for the case where the user needs to confirm the action on another
* device. Note: Depending on the banks you need compatibility with you may not need to implement decoupled
* authentication at all, i.e. you could filter out any decoupled TanModes when letting the user choose.
* device. Note: Depending on the banks you need compatibility with, you may not need to implement decoupled
* authentication at all, i.e., you could filter out any decoupled TanModes when letting the user choose.
*
* @param \Fhp\BaseAction $action Some action that requires decoupled authentication.
* @throws CurlException|UnexpectedResponseException|ServerException See {@link FinTs::execute()} for details.
*/
function handleDecoupled(Fhp\BaseAction $action)
{
global $fints;
global $fints, $options, $credentials;

$tanMode = $fints->getSelectedTanMode();
$tanRequest = $action->getTanRequest();
Expand All @@ -150,6 +152,18 @@ function handleDecoupled(Fhp\BaseAction $action)
echo 'Please check this device: ' . $tanRequest->getTanMediumName() . "\n";
}

// Just like in handleTan() above, we have the option to interrupt the PHP process at this point. In fact, the
// for-loop below that deals with the polling may even be running on the client side of your larger application,
// polling your application server regularly, which spawns a new PHP process each time. Here, we demonstrate this by
// persisting the instance to a local file and restoring it (even though that's not technically necessary for a
// single-process CLI script like this).
if ($optionallyPersistEverything = false) {
$persistedAction = serialize($action);
$persistedFints = $fints->persist();
// See handleTan() for how to deal with this in practice.
file_put_contents(__DIR__ . '/state.txt', serialize([$persistedFints, $persistedAction]));
}

// IMPORTANT: In your real application, you don't have to use sleep() in PHP. You can persist the state in the same
// way as in handleTan() and restore it later. This allows you to use some other timer mechanism (e.g. in the user's
// browser). This PHP sample code just serves to show the *logic* of the polling. Alternatively, you can even do
Expand All @@ -162,22 +176,44 @@ function handleDecoupled(Fhp\BaseAction $action)
$tanMode->getMaxDecoupledChecks() === 0 || $attempt < $tanMode->getMaxDecoupledChecks();
++$attempt
) {
// Optional: If the state was persisted above, we can restore it now (imagine this is a new PHP process).
if ($optionallyPersistEverything) {
$restoredState = file_get_contents(__DIR__ . '/state.txt');
list($persistedInstance, $persistedAction) = unserialize($restoredState);
$fints = \Fhp\FinTs::new($options, $credentials, $persistedInstance);
$action = unserialize($persistedAction);
}

// Check if we're done.
if ($fints->checkDecoupledSubmission($action)) {
echo "Confirmed.\n";
return;
}
echo "Still waiting...\n";

// THIS IS CRUCIAL if you're using persistence in between polls. You must re-persist() the instance after
// calling checkDecoupledSubmission() and before calling it the next time. Don't reuse the
// $persistedInstance from above multiple times.
if ($optionallyPersistEverything) {
$persistedAction = serialize($action);
$persistedFints = $fints->persist();
file_put_contents(__DIR__ . '/state.txt', serialize([$persistedFints, $persistedAction]));
}

sleep($tanMode->getPeriodicDecoupledCheckDelaySeconds());
}
throw new RuntimeException("Not confirmed after $attempt attempts, which is the limit.");
} elseif ($tanMode->allowsManualConfirmation()) {
do {
echo "Please type 'done' and hit Return when you've completed the authentication on the other device.\n";
while (trim(fgets(STDIN)) !== 'done') {
echo "Try again.\n";
}
echo "Confirming that the action is done.\n";
} while (!$fints->checkDecoupledSubmission($action));
echo "Please type 'done' and hit Return when you've completed the authentication on the other device.\n";
while (trim(fgets(STDIN)) !== 'done') {
echo "Try again.\n";
}
echo "Confirming that the action is done.\n";
if (!$fints->checkDecoupledSubmission($action)) {
throw new RuntimeException(
"You confirmed that the authentication for action was copmleted, but the server does not think so."
);
}
echo "Confirmed\n";
} else {
throw new AssertionError('Server allows neither automated polling nor manual confirmation');
Expand Down
8 changes: 4 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "nemiah/php-fints",
"description": "PHP Library for the protocols fints and hbci",
"homepage": "https://github.com/nemiah/phpFinTS",
"version": "3.5.0",
"version": "3.6.0",
"license": "MIT",
"autoload": {
"psr-0": {
Expand All @@ -17,9 +17,9 @@
"ext-mbstring": "*"
},
"require-dev": {
"phpunit/phpunit": "9.5.*",
"php-mock/php-mock-phpunit": "2.6.*",
"friendsofphp/php-cs-fixer": "3.*"
"phpunit/phpunit": "^9.5",
"php-mock/php-mock-phpunit": "^2.6",
"friendsofphp/php-cs-fixer": "^3.0"
},
"suggest": {
"monolog/monolog": "Allow sending log messages to a variety of different handlers",
Expand Down
2 changes: 0 additions & 2 deletions lib/Fhp/Action/GetBalance.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ public function getBalances()
return $this->response;
}

/** {@inheritdoc} */
protected function createRequest(BPD $bpd, ?UPD $upd)
{
/** @var BaseSegment $hisals */
Expand All @@ -115,7 +114,6 @@ protected function createRequest(BPD $bpd, ?UPD $upd)
}
}

/** {@inheritdoc} */
public function processResponse(Message $response)
{
parent::processResponse($response);
Expand Down
2 changes: 0 additions & 2 deletions lib/Fhp/Action/GetDepotAufstellung.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ public function getDepotWert(): float
return $this->depotWert;
}

/** {@inheritdoc} */
protected function createRequest(BPD $bpd, ?UPD $upd)
{
/** @var HIWPDS $hiwpds */
Expand All @@ -125,7 +124,6 @@ protected function createRequest(BPD $bpd, ?UPD $upd)
}
}

/** {@inheritdoc} */
public function processResponse(Message $response)
{
parent::processResponse($response);
Expand Down
2 changes: 0 additions & 2 deletions lib/Fhp/Action/GetSEPAAccounts.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ public function getAccounts(): array
return $this->accounts;
}

/** {@inheritdoc} */
protected function createRequest(BPD $bpd, ?UPD $upd)
{
/** @var BaseSegment $hispas */
Expand All @@ -64,7 +63,6 @@ protected function createRequest(BPD $bpd, ?UPD $upd)
}
}

/** {@inheritdoc} */
public function processResponse(Message $response)
{
parent::processResponse($response);
Expand Down
1 change: 0 additions & 1 deletion lib/Fhp/Action/GetSEPADirectDebitParameters.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ public static function getHixxesSegmentName(string $directDebitType, bool $singl
}
}

/** {@inheritdoc} */
protected function createRequest(BPD $bpd, ?UPD $upd)
{
$this->hidxes = $bpd->requireLatestSupportedParameters(static::getHixxesSegmentName($this->directDebitType, $this->singleDirectDebit));
Expand Down
2 changes: 0 additions & 2 deletions lib/Fhp/Action/GetStatementOfAccount.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,6 @@ public function getStatement(): StatementOfAccount
return $this->statement;
}

/** {@inheritdoc} */
protected function createRequest(BPD $bpd, ?UPD $upd)
{
$this->bankName = $bpd->getBankName();
Expand All @@ -171,7 +170,6 @@ protected function createRequest(BPD $bpd, ?UPD $upd)
}
}

/** {@inheritdoc} */
public function processResponse(Message $response)
{
parent::processResponse($response);
Expand Down
2 changes: 0 additions & 2 deletions lib/Fhp/Action/GetStatementOfAccountXML.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,6 @@ public function getBookedXML(): array
return $this->xml;
}

/** {@inheritdoc} */
protected function createRequest(BPD $bpd, ?UPD $upd)
{
if ($upd === null) {
Expand Down Expand Up @@ -149,7 +148,6 @@ protected function createRequest(BPD $bpd, ?UPD $upd)
}
}

/** {@inheritdoc} */
public function processResponse(Message $response)
{
parent::processResponse($response);
Expand Down
19 changes: 14 additions & 5 deletions lib/Fhp/Action/SendSEPADirectDebit.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,11 @@ public static function create(SEPAAccount $account, string $painMessage, bool $t
$nbOfTxs = substr_count($painMessage, '<DrctDbtTxInf>');
$ctrlSum = null;

if (preg_match('@<GrpHdr>.*<CtrlSum>(?<ctrlsum>[.0-9]+)</CtrlSum>.*</GrpHdr>@s', $painMessage, $matches) === 1) {
if (preg_match('@<GrpHdr>.*?<CtrlSum>(?<ctrlsum>[0-9.]+)</CtrlSum>.*?</GrpHdr>@s', $painMessage, $matches) === 1) {
$ctrlSum = $matches['ctrlsum'];
}

if (preg_match('@<PmtTpInf>.*<LclInstrm>.*<Cd>(?<coretype>CORE|COR1|B2B)</Cd>.*</LclInstrm>.*</PmtTpInf>@s', $painMessage, $matches) === 1) {
if (preg_match('@<PmtTpInf>.*?<LclInstrm>.*?<Cd>(?<coretype>CORE|COR1|B2B)</Cd>.*?</LclInstrm>.*?</PmtTpInf>@s', $painMessage, $matches) === 1) {
$coreType = $matches['coretype'];
} else {
throw new \InvalidArgumentException('The type CORE/COR1/B2B is missing in PAIN message');
Expand Down Expand Up @@ -138,17 +138,26 @@ protected function createRequest(BPD $bpd, ?UPD $upd)

if ($hidxes->getVersion() === 2) {
/** @var HIDMESv2|HIDSESv2 $hidxes */
$supportedPainNamespaces = $hidxes->getParameter()->unterstuetzteSEPADatenformate;
$supportedPainNamespaces = $hidxes->getParameter()->getUnterstuetzteSEPADatenformate();
}

// If there are no SEPA formats available in the HIDXES Parameters, we look to the general formats
if (!is_array($supportedPainNamespaces) || count($supportedPainNamespaces) === 0) {
/** @var HISPAS $hispas */
$hispas = $bpd->requireLatestSupportedParameters('HISPAS');
$supportedPainNamespaces = $hispas->getParameter()->getUnterstuetzteSepaDatenformate();
$supportedPainNamespaces = $hispas->getParameter()->getUnterstuetzteSEPADatenformate();
}

if (!in_array($this->painNamespace, $supportedPainNamespaces)) {
// Sometimes the Bank reports supported schemas with a "_GBIC_X" postfix.
// GIBC_X stands for German Banking Industry Committee and a version counter.
$xmlSchema = $this->painNamespace;
$matchingSchemas = array_filter($supportedPainNamespaces, function($value) use ($xmlSchema) {
// For example urn:iso:std:iso:20022:tech:xsd:pain.008.001.08 from the xml matches
// urn:iso:std:iso:20022:tech:xsd:pain.008.001.08_GBIC_4
return str_starts_with($value, $xmlSchema);
});

if (count($matchingSchemas) === 0) {
throw new UnsupportedException("The bank does not support the XML schema $this->painNamespace, but only "
. implode(', ', $supportedPainNamespaces));
}
Expand Down
Loading
Loading