, * hinweis: string}>} */ public static function abfragen(int $zugangId): array { $zugang = Bankzugang::zugangsdaten($zugangId); if ($zugang === null) { return ['status' => 'fehler', 'meldung' => 'Den Bankzugang gibt es nicht.', 'verfahren' => []]; } foreach (['blz' => 'Bankleitzahl', 'fints_url' => 'FinTS-Adresse', 'benutzerkennung' => 'Benutzerkennung', 'pin' => 'PIN'] as $feld => $name) { if (trim((string)$zugang[$feld]) === '') { return [ 'status' => 'fehler', 'meldung' => 'Es fehlt: ' . $name . '. Ohne vollständige Zugangsdaten ' . 'lässt sich die Bank nicht fragen.', 'verfahren' => [], ]; } } try { $fints = self::verbindung($zugang); $modi = $fints->getTanModes(); } catch (\Throwable $e) { DebugLog::fehler($e); return [ 'status' => 'fehler', 'meldung' => 'Die Bank antwortet nicht wie erwartet: ' . $e->getMessage(), 'verfahren' => [], ]; } if ($modi === []) { return [ 'status' => 'fehler', 'meldung' => 'Die Bank meldet keine TAN-Verfahren für diese Benutzerkennung.', 'verfahren' => [], ]; } $verfahren = []; foreach ($modi as $modus) { $verfahren[] = self::beschreibe($fints, $modus); } try { $fints->close(); } catch (\Throwable) { // Der Dialog ist ohnehin am Ende; ein Fehler beim Abmelden ändert // an der Liste nichts. } return [ 'status' => 'erfolg', 'meldung' => count($verfahren) === 1 ? 'Die Bank bietet ein Verfahren an.' : sprintf('Die Bank bietet %d Verfahren an.', count($verfahren)), 'verfahren' => $verfahren, ]; } /** * Übernimmt die Wahl des Nutzers. * * Geprüft wird gegen die Liste, die gerade abgefragt wurde: Eine ID, die die * Bank nicht angeboten hat, würde beim nächsten Abruf stillschweigend * verworfen – dann lieber jetzt eine Meldung. * * @param list> $bekannt Ergebnis von abfragen()['verfahren'] */ public static function uebernehmen(int $zugangId, int $verfahrenId, string $medium, array $bekannt): void { $treffer = null; foreach ($bekannt as $eintrag) { if ((int)$eintrag['id'] === $verfahrenId) { $treffer = $eintrag; break; } } if ($treffer === null) { throw new \InvalidArgumentException( 'Dieses Verfahren steht nicht in der abgefragten Liste. Bitte neu abfragen.' ); } $medien = array_map('strval', $treffer['medien'] ?? []); if ($treffer['medium_noetig'] === true) { if ($medium === '') { throw new \InvalidArgumentException( 'Das Verfahren „' . $treffer['name'] . '“ braucht ein TAN-Medium.' ); } if ($medien !== [] && !in_array($medium, $medien, true)) { throw new \InvalidArgumentException('Dieses TAN-Medium hat die Bank nicht genannt.'); } } else { // Ein Medium ohne Not gespeichert würde die Bibliothek beim nächsten // Dialog mitschicken – manche Banken lehnen das ab. $medium = ''; } Bankzugang::merkeTanVerfahren($zugangId, $verfahrenId, $medium, (string)$treffer['name']); } /** * Stellt das Verfahren für einen laufenden Dialog ein. * * Wird von `FinTsAbruf` und `FinTsUeberweisung` benutzt, damit beide dieselbe * Wahl treffen. Ist am Zugang nichts hinterlegt, entscheidet das erste * gemeldete Verfahren – und diese Notwahl wird gespeichert, damit sie im * Formular sichtbar ist. Eine **ausdrückliche** Wahl wird dagegen nie * überschrieben: Bietet die Bank sie gerade nicht an, sagt das Protokoll das, * und die Einstellung bleibt stehen. * * @param array $zugang Zugang mit entschlüsseltem TAN-Medium * @param callable(string):void $notiere Protokollzeile schreiben */ public static function anwenden(FinTs $fints, array $zugang, callable $notiere): void { $modi = $fints->getTanModes(); if ($modi === []) { throw new \RuntimeException('Die Bank meldet keine TAN-Verfahren.'); } $gewuenscht = $zugang['tan_verfahren_id'] === null ? null : (int)$zugang['tan_verfahren_id']; $gewaehlt = null; foreach ($modi as $modus) { if ($gewuenscht !== null && $modus->getId() === $gewuenscht) { $gewaehlt = $modus; break; } } $abweichung = $gewuenscht !== null && $gewaehlt === null; $gewaehlt ??= reset($modi); if ($abweichung) { $notiere(sprintf( 'Das eingestellte TAN-Verfahren (%s) bietet die Bank gerade nicht an – es wird %s benutzt.', (string)$zugang['tan_verfahren_name'] !== '' ? $zugang['tan_verfahren_name'] . ', ID ' . $gewuenscht : 'ID ' . $gewuenscht, $gewaehlt->getName() )); } $medium = null; $mediumName = ''; if ($gewaehlt->needsTanMedium()) { $medien = $fints->getTanMedia($gewaehlt); if ($medien === []) { throw new \RuntimeException( 'Das Verfahren „' . $gewaehlt->getName() . '“ braucht ein TAN-Medium, ' . 'die Bank nennt aber keines.' ); } $gespeichert = (string)($zugang['tan_medium'] ?? ''); foreach ($medien as $kandidat) { if ($gespeichert !== '' && $kandidat->getName() === $gespeichert) { $medium = $kandidat; break; } } $medium ??= reset($medien); $mediumName = (string)$medium->getName(); } $fints->selectTanMode($gewaehlt, $medium); $notiere('TAN-Verfahren: ' . $gewaehlt->getName() . ($mediumName !== '' ? ' (' . $mediumName . ')' : '')); // Nur die Notwahl wird festgehalten – eine ausdrückliche bleibt, wie sie ist. if ($gewuenscht === null) { Bankzugang::merkeTanVerfahren( (int)$zugang['id'], $gewaehlt->getId(), $mediumName, $gewaehlt->getName() ); } } // ── Innereien ──────────────────────────────────────────────────────────── /** * @return array{id: int, name: string, entkoppelt: bool, medium_noetig: bool, * medien: list, hinweis: string} */ private static function beschreibe(FinTs $fints, TanMode $modus): array { $medien = []; $hinweis = ''; if ($modus->needsTanMedium()) { try { foreach ($fints->getTanMedia($modus) as $medium) { $medien[] = (string)$medium->getName(); } } catch (\Throwable $e) { // Ein Verfahren, dessen Medien die Bank nicht herausgibt, bleibt // in der Liste – der Nutzer soll sehen, dass es existiert. $hinweis = 'Medien nicht abrufbar: ' . $e->getMessage(); } } if ($modus->isDecoupled()) { $hinweis = $hinweis === '' ? 'Freigabe in der Banking-App, keine TAN einzugeben.' : $hinweis; } return [ 'id' => $modus->getId(), 'name' => $modus->getName(), 'entkoppelt' => $modus->isDecoupled(), 'medium_noetig' => $modus->needsTanMedium(), 'medien' => $medien, 'hinweis' => $hinweis, ]; } /** @param array $zugang */ private static function verbindung(array $zugang): FinTs { $optionen = new FinTsOptions(); $optionen->url = (string)$zugang['fints_url']; $optionen->bankCode = (string)$zugang['blz']; $optionen->productName = Einstellung::get('banking_produkt_id') ?: self::PRODUKT; $optionen->productVersion = self::PRODUKT_VERSION; $fints = FinTs::new( $optionen, Credentials::create((string)$zugang['benutzerkennung'], (string)$zugang['pin']) ); $fints->setLogger(new DebugLogAdapter()); return $fints; } }