- Add multi-invoice payment support (link one bank transaction to multiple invoices) - Add payment unlinking feature to correct wrong matches - Show linked payments, invoices and bank entries in transaction detail view - Allow linking already paid invoices to bank transactions - Update README with new features - Add CHANGELOG.md Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
236 lines
12 KiB
PHP
Executable file
236 lines
12 KiB
PHP
Executable file
<?php
|
|
|
|
/** @noinspection PhpUnhandledExceptionInspection */
|
|
|
|
use Fhp\CurlException;
|
|
use Fhp\Protocol\ServerException;
|
|
use Fhp\Protocol\UnexpectedResponseException;
|
|
|
|
/**
|
|
* SAMPLE - Creates a new FinTs instance (init.php) and makes sure its logged in.
|
|
*/
|
|
|
|
/** @var \Fhp\FinTs $fints */
|
|
$fints = require_once 'init.php';
|
|
|
|
/**
|
|
* This function as well as handleTan() and handleDecoupled() below are key to how FinTS works in times of PSD2
|
|
* regulations.
|
|
* Most actions like wire transfers, getting statements and even logging in can ask for strong authentication (a TAN or
|
|
* some form of confirmation on a "decoupled" device that the user has access to), but won't always. Whether strong
|
|
* authentication required depends on the kind of action, when it was last executed, other parameters like the amount
|
|
* (of a wire transfer) or time span (of a statement request) and generally the security concept of the particular bank.
|
|
* The authentication requirements may or may not be consistent with the kinds of authentication that the same bank
|
|
* requires for the same action in the web-based online banking interface. Also, banks may change these requirements
|
|
* over time, so just because your particular bank does or does not need a TAN for login today does not mean that it
|
|
* stays that way. There is a general tendency towards less intrusive strong authentication, i.e. requiring it for fewer
|
|
* actions (based on heuristics), less often (e.g. only every 90 days) or in a decoupled mode where the user only needs
|
|
* to tap a single button.
|
|
*
|
|
* The strong authentification can be implemented in many different ways. Each application that uses the phpFinTS
|
|
* library has to implement its own way of asking users for a TAN or for decoupled confirmation, which varies depending
|
|
* 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 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.
|
|
* @throws CurlException|UnexpectedResponseException|ServerException See {@link FinTs::execute()} for details.
|
|
*/
|
|
function handleStrongAuthentication(\Fhp\BaseAction $action)
|
|
{
|
|
global $fints;
|
|
if ($fints->getSelectedTanMode()->isDecoupled()) {
|
|
handleDecoupled($action);
|
|
} else {
|
|
handleTan($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.
|
|
*/
|
|
function handleTan(Fhp\BaseAction $action)
|
|
{
|
|
global $fints, $options, $credentials;
|
|
|
|
// Find out what sort of TAN we need, tell the user about it.
|
|
$tanRequest = $action->getTanRequest();
|
|
echo 'The bank requested a TAN.';
|
|
if ($tanRequest->getChallenge() !== null) {
|
|
echo ' Instructions: ' . $tanRequest->getChallenge();
|
|
}
|
|
echo "\n";
|
|
if ($tanRequest->getTanMediumName() !== null) {
|
|
echo 'Please use this device: ' . $tanRequest->getTanMediumName() . "\n";
|
|
}
|
|
|
|
// Challenge Image for PhotoTan/ChipTan
|
|
if ($tanRequest->getChallengeHhdUc()) {
|
|
try {
|
|
$flicker = new \Fhp\Model\FlickerTan\TanRequestChallengeFlicker($tanRequest->getChallengeHhdUc());
|
|
echo 'There is a challenge flicker.' . PHP_EOL;
|
|
// save or output svg
|
|
$flickerPattern = $flicker->getFlickerPattern();
|
|
// other renderers can be implemented with this pattern
|
|
$svg = new \Fhp\Model\FlickerTan\SvgRenderer($flickerPattern);
|
|
echo $svg->getImage();
|
|
} catch (InvalidArgumentException $e) {
|
|
// was not a flicker
|
|
$challengeImage = new \Fhp\Model\TanRequestChallengeImage(
|
|
$tanRequest->getChallengeHhdUc()
|
|
);
|
|
echo 'There is a challenge image.' . PHP_EOL;
|
|
// Save the challenge image somewhere
|
|
// Alternative: HTML sample code
|
|
echo '<img src="data:' . htmlspecialchars($challengeImage->getMimeType()) . ';base64,' . base64_encode($challengeImage->getData()) . '" />' . PHP_EOL;
|
|
}
|
|
}
|
|
|
|
// 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 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();
|
|
|
|
// These are two strings (watch out, they are NOT necessarily UTF-8 encoded), which you can store anywhere.
|
|
// This example code stores them in a text file, but you might write them to your database (use a BLOB, not a
|
|
// CHAR/TEXT field to allow for arbitrary encoding) or in some other storage (possibly base64-encoded to make it
|
|
// ASCII).
|
|
file_put_contents(__DIR__ . '/state.txt', serialize([$persistedFints, $persistedAction]));
|
|
}
|
|
|
|
// Ask the user for the TAN. ----------------------------------------------------------------------------------------
|
|
// IMPORTANT: In your real application, you cannot use fgets(STDIN) of course (unless you're running PHP only as a
|
|
// command line application). So you instead want to send a response to the user. This means that, after executing
|
|
// the first half of handleTan() above, your real application will terminate the PHP process. The second half of
|
|
// handleTan() will likely live elsewhere in your application code (i.e. you will have two functions for the TAN
|
|
// handling, not just one like in this simplified example). You *only* need to carry over the $persistedInstance
|
|
// and the $persistedAction (which are simple strings) by storing them in some database or file where you can load
|
|
// them again in a new PHP process when the user sends the TAN.
|
|
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 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);
|
|
}
|
|
|
|
echo "Submitting TAN: $tan\n";
|
|
$fints->submitTan($action, $tan);
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @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, $options, $credentials;
|
|
|
|
$tanMode = $fints->getSelectedTanMode();
|
|
$tanRequest = $action->getTanRequest();
|
|
echo 'The bank requested authentication on another device.';
|
|
if ($tanRequest->getChallenge() !== null) {
|
|
echo ' Instructions: ' . $tanRequest->getChallenge();
|
|
}
|
|
echo "\n";
|
|
if ($tanRequest->getTanMediumName() !== null) {
|
|
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
|
|
// without polling entirely and just let the user confirm manually in all cases (i.e. only implement the `else`
|
|
// branch below).
|
|
if ($tanMode->allowsAutomatedPolling()) {
|
|
echo "Polling server to detect when the decoupled authentication is complete.\n";
|
|
sleep($tanMode->getFirstDecoupledCheckDelaySeconds());
|
|
for ($attempt = 0;
|
|
$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()) {
|
|
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');
|
|
}
|
|
}
|
|
|
|
// Select TAN mode and possibly medium. If you're not sure what this is about, read and run tanModesAndMedia.php first.
|
|
$tanMode = 900; // This is just a placeholder you need to fill!
|
|
$tanMedium = null; // This is just a placeholder you may need to fill.
|
|
$fints->selectTanMode($tanMode, $tanMedium);
|
|
|
|
// Log in.
|
|
$login = $fints->login();
|
|
if ($login->needsTan()) {
|
|
handleStrongAuthentication($login);
|
|
}
|
|
|
|
// Usage:
|
|
// $fints = require_once 'login.php';
|
|
return $fints;
|