- HKEKA v3/v4/v5 Segmente fuer phpFinTS implementiert (VR Bank unterstuetzt kein HKEKP) - GetElectronicStatement Action mit Base64-Erkennung und Quittungscode - PDF-Deduplizierung per MD5 (Bank sendet identische Saldenmitteilungen) - Saldenmitteilungen ohne Auszugsnummer werden uebersprungen - Datums-Validierung: 30.02. (Bank-Konvention) wird auf 28.02. korrigiert - Numerische Sortierung fuer statement_number (CAST statt String-Sort) - Jahr-Filter: statement_year=0 ausgeschlossen - Menue/Button: "Kontoauszuege" -> "Umsaetze" (statements.php zeigt MT940, nicht PDFs) - Redirect nach FinTS-Abruf auf aktuelles Jahr statt year=0 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
258 lines
11 KiB
PHP
Executable file
258 lines
11 KiB
PHP
Executable file
<?php
|
|
|
|
/** @noinspection PhpUnused */
|
|
|
|
namespace Fhp;
|
|
|
|
use Fhp\Model\TanRequest;
|
|
use Fhp\Protocol\ActionIncompleteException;
|
|
use Fhp\Protocol\BPD;
|
|
use Fhp\Protocol\Message;
|
|
use Fhp\Protocol\TanRequiredException;
|
|
use Fhp\Protocol\UnexpectedResponseException;
|
|
use Fhp\Protocol\UPD;
|
|
use Fhp\Segment\BaseSegment;
|
|
use Fhp\Segment\HIRMS\Rueckmeldung;
|
|
use Fhp\Segment\HIRMS\Rueckmeldungscode;
|
|
|
|
/**
|
|
* Base class for actions that can be performed against a bank server. On a high level, there are two kinds of actions:
|
|
* - requests for information (e.g. an account statement), which the bank server will return
|
|
* - transactions (e.g. a wire transfer to another account), which the bank server will execute.
|
|
*
|
|
* In part, this class is designed like futures/promises in concurrent programming. The outcome of the action (i.e. the
|
|
* requested information or the execution confirmation of the transaction) becomes available in the future, possibly
|
|
* much later than when the request was sent, in case the user needs to enter a TAN.
|
|
* All action instances are serializable, so that the execution can be interrupted to ask the user for a TAN. Once the
|
|
* TAN is available, the execution can resume either a couple seconds later in the same PHP process using the same
|
|
* physical connection to the bank, or on the order of minutes later in a new PHP process with a newly established
|
|
* connection to the bank. Note that the serialization only applies to selected relevant request parameters, and not to
|
|
* the response. Thus it is only possible to serialize an action when its execution has been attempted but resulted in a
|
|
* TAN request.
|
|
* Actions that do not require a TAN will complete immediately.
|
|
*
|
|
* The implementation of an action consists of two parts: assembling the request to the bank, and processing the
|
|
* response.
|
|
*/
|
|
abstract class BaseAction implements \Serializable
|
|
{
|
|
/** @var int[] Stores segment numbers that were assigned to the segments returned from {@link createRequest()}. */
|
|
protected $requestSegmentNumbers;
|
|
|
|
/**
|
|
* @var string|null Contains the name of the segment, that might need a tan, used by FinTs::execute to signal
|
|
* to the bank that supplying a tan is supported.
|
|
*/
|
|
protected $needTanForSegment = null;
|
|
|
|
/**
|
|
* If set, the last response from the server regarding this action asked for a TAN from the user.
|
|
* @var TanRequest|null
|
|
*/
|
|
protected $tanRequest;
|
|
|
|
/** @var bool */
|
|
protected $isDone = false;
|
|
|
|
/**
|
|
* Will be populated with the message the bank sent along with the success indication, can be used to show to
|
|
* the user.
|
|
* @var string
|
|
*/
|
|
public $successMessage;
|
|
|
|
/**
|
|
* @deprecated Beginning from PHP7.4 __unserialize is used for new generated strings, then this method is only used for previously generated strings - remove after May 2023
|
|
*
|
|
* NOTE: A common mistake is to call this function directly. Instead, you probably want `serialize($instance)`.
|
|
*
|
|
* An action can only be serialized *after* it has been executed in case it needs a TAN, i.e. when the result is not
|
|
* present yet.
|
|
* If a sub-class overrides this, it should call the parent function and include it in its result.
|
|
* @return string The serialized action, e.g. for storage in a database. This will not contain sensitive user data.
|
|
*/
|
|
public function serialize(): string
|
|
{
|
|
return serialize($this->__serialize());
|
|
}
|
|
|
|
/**
|
|
* An action can only be serialized *after* it has been executed in case it needs a TAN, i.e. when the result is not
|
|
* present yet.
|
|
* If a sub-class overrides this, it should call the parent function and include it in its result.
|
|
*
|
|
* @return array The serialized action, e.g. for storage in a database. This will not contain sensitive user data.
|
|
*/
|
|
public function __serialize(): array
|
|
{
|
|
if (!$this->needsTan()) {
|
|
throw new \RuntimeException('Cannot serialize this action, because it is not waiting for a TAN.');
|
|
}
|
|
return [
|
|
$this->requestSegmentNumbers,
|
|
$this->tanRequest,
|
|
$this->needTanForSegment,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @deprecated Beginning from PHP7.4 __unserialize is used for new generated strings, then this method is only used for previously generated strings - remove after May 2023
|
|
*
|
|
* @param string $serialized
|
|
* @return void
|
|
*/
|
|
public function unserialize($serialized)
|
|
{
|
|
self::__unserialize(unserialize($serialized));
|
|
}
|
|
|
|
public function __unserialize(array $serialized): void
|
|
{
|
|
list(
|
|
$this->requestSegmentNumbers,
|
|
$this->tanRequest,
|
|
$this->needTanForSegment
|
|
) = $serialized;
|
|
}
|
|
|
|
/**
|
|
* @return bool Whether the underlying operation has completed successfully and the result in this "future" is
|
|
* available. Note: If this returns false, check {@link needsTan()}.
|
|
*/
|
|
public function isDone(): bool
|
|
{
|
|
return $this->isDone;
|
|
}
|
|
|
|
/**
|
|
* @return bool If this returns true, the underlying operation has not completed because it is awaiting a TAN or a
|
|
* "decoupled" confirmation. You should ask the user for this TAN/confirmation and pass it to
|
|
* {@link FinTs::submitTan()} or call {@link FinTs::checkDecoupledSubmission()}, respectively.
|
|
*/
|
|
public function needsTan(): bool
|
|
{
|
|
return !$this->isDone() && $this->tanRequest !== null;
|
|
}
|
|
|
|
public function getNeedTanForSegment(): ?string
|
|
{
|
|
return $this->needTanForSegment;
|
|
}
|
|
|
|
public function getTanRequest(): ?TanRequest
|
|
{
|
|
return $this->tanRequest;
|
|
}
|
|
|
|
/**
|
|
* Throws an exception unless this action has been successfully executed, i.e. in the following cases:
|
|
* - the action has not been {@link FinTs::execute()}-d at all or the {@link FinTs::execute()} call for it threw an
|
|
* exception,
|
|
* - the action is awaiting a TAN/confirmation (as per {@link BaseAction::needsTan()}.
|
|
*
|
|
* After executing an action, you can use this function to make sure that it succeeded. This is especially useful
|
|
* for actions that don't have any results (as each result getter would call {@link ensureDone()} internally).
|
|
* On the other hand, you do not need to call this function if you make sure that (1) you called
|
|
* {@link FinTs::execute()} and (2) you checked {@link needsTan()} and, if it returned true, supplied a TAN by
|
|
* calling {@ink FinTs::submitTan()}. Note that both exception types thrown from this method are sub-classes of
|
|
* {@link \RuntimeException}, so you shouldn't need a try-catch block at the call site for this.
|
|
* @throws ActionIncompleteException If the action hasn't even been executed.
|
|
* @throws TanRequiredException If the action needs a TAN.
|
|
*/
|
|
public function ensureDone()
|
|
{
|
|
if ($this->tanRequest !== null) {
|
|
throw new TanRequiredException($this->tanRequest);
|
|
} elseif (!$this->isDone()) {
|
|
throw new ActionIncompleteException();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called when this action is about to be executed, in order to construct the request.
|
|
* @param BPD $bpd See {@link BPD}.
|
|
* @param UPD|null $upd See {@link UPD}. This is usually present (non-null), except for a few special login and TAN
|
|
* management actions.
|
|
* @return BaseSegment|BaseSegment[] A segment or a series of segments that should be sent to the bank server.
|
|
* Note that an action can return an empty array to indicate that it does not need to make a request to the
|
|
* server, but can instead compute the result just from the BPD/UPD, in which case it should set
|
|
* `$this->isDone = true;` already in {@link createRequest()} and {@link processResponse()} will never
|
|
* be executed.
|
|
* @throws \InvalidArgumentException When the request cannot be built because the input data or BPD/UPD is invalid.
|
|
*/
|
|
abstract protected function createRequest(BPD $bpd, ?UPD $upd);
|
|
|
|
/**
|
|
* Called by FinTs::execute when this action is about to be executed, in order to get a request. This function can
|
|
* be called multiple times in case the response is paginated.
|
|
* This method also tries to check if the segments might need a tan and stores this information for use in
|
|
* FinTs::execute
|
|
* @param BPD|null $bpd See {@link BPD}.
|
|
* @param UPD|null $upd See {@link UPD}. This is usually present (non-null), except for a few special login and TAN
|
|
* management actions.
|
|
* @return BaseSegment[] A segment or a series of segments that should be sent to the bank server.
|
|
* An empty array means that no request is necessary at all.
|
|
* @throws \InvalidArgumentException When the request cannot be built because the input data or BPD/UPD is invalid.
|
|
*/
|
|
public function getNextRequest(BPD $bpd, ?UPD $upd)
|
|
{
|
|
$requestSegments = $this->createRequest($bpd, $upd);
|
|
$requestSegments = is_array($requestSegments) ? $requestSegments : [$requestSegments];
|
|
|
|
$this->needTanForSegment = $bpd->tanRequiredForRequest($requestSegments);
|
|
|
|
return $requestSegments;
|
|
}
|
|
|
|
/**
|
|
* Called when this action was executed on the server (never if {@link createRequest()} returned an empty request),
|
|
* to process the response. This function can be called multiple times in case the response is paginated.
|
|
* In case the response indicates that this action failed, this function may throw an appropriate exception. Sub-classes should override this function
|
|
* and call the parent/super function.
|
|
* @param Message $response A fake message that contains the subset of segments received from the server that
|
|
* were in response to the request segments that were created by {@link createRequest()}.
|
|
* @throws UnexpectedResponseException When the response indicates failure.
|
|
*/
|
|
public function processResponse(Message $response)
|
|
{
|
|
$this->isDone = true;
|
|
|
|
$info = $response->findRueckmeldungen(Rueckmeldungscode::AUSGEFUEHRT);
|
|
if (count($info) === 0) {
|
|
$info = $response->findRueckmeldungen(Rueckmeldungscode::ENTGEGENGENOMMEN);
|
|
}
|
|
if (count($info) > 0) {
|
|
$this->successMessage = implode("\n", array_map(function (Rueckmeldung $rueckmeldung) {
|
|
return $rueckmeldung->rueckmeldungstext;
|
|
}, $info));
|
|
}
|
|
}
|
|
|
|
/** @return int[] */
|
|
public function getRequestSegmentNumbers(): array
|
|
{
|
|
return $this->requestSegmentNumbers;
|
|
}
|
|
|
|
/**
|
|
* To be called only by the FinTs instance that executes this action.
|
|
* @param int[] $requestSegmentNumbers
|
|
*/
|
|
final public function setRequestSegmentNumbers(array $requestSegmentNumbers)
|
|
{
|
|
foreach ($requestSegmentNumbers as $segmentNumber) {
|
|
if (!is_int($segmentNumber)) {
|
|
throw new \InvalidArgumentException("Invalid segment number: $segmentNumber");
|
|
}
|
|
}
|
|
$this->requestSegmentNumbers = $requestSegmentNumbers;
|
|
}
|
|
|
|
/**
|
|
* To be called only by the FinTs instance that executes this action.
|
|
*/
|
|
final public function setTanRequest(?TanRequest $tanRequest)
|
|
{
|
|
$this->tanRequest = $tanRequest;
|
|
}
|
|
}
|