Compare commits

..
8 Commits
Author SHA1 Message Date
clemensandClaude Opus 5 e7955e354c Rufnummernsuche auf den Altbestand user_old ausweiten
Die Suche lief nur gegen persons und fand deshalb den Anrufer von Anfrage
13037 nicht: dessen Mobilnummer steht in user_old, in persons ist zu
derselben Person nur eine aeltere siebenstellige Nummer gepflegt. Beide
Zeilen sind ueber tmp_user_person_map korrekt verknuepft (userid 3204 ->
person_id 2266), die Rufnummer ist bei der Migration schlicht nicht
nachgezogen worden.

Das ist kein Einzelfall. Von 2552 gemappten Paaren weicht bei 122 die
Nummer ab, 87 Nummern stehen ausschliesslich in user_old, und 176 alte
Zeilen haben gar kein Mapping. persons ist damit keine Obermenge, und die
Suche allein darueber verfehlt 87 der 2739 bekannten Rufnummern.

Der Index wird jetzt aus beiden Tabellen aufgebaut. Findet sich zu einer
alten Zeile ein aktueller Datensatz, werden dessen Daten gezeigt - die
Nummer ist nur der Schluessel, angezeigt gehoert der gepflegte Stand. Der
Vorschlag weist aus, wenn die Nummer aus dem Altbestand stammt, und
unterscheidet den Fall ohne aktuellen Datensatz.

Ein Patient darf je Nummer nur einmal erscheinen. Die Personen-ID reicht
dafuer nicht: nicht gemappte Altzeilen haben eine eigene userid und standen
dadurch neben ihrem persons-Gegenstueck, sichtbar als "2 Patienten:
Andreas Busche, Andreas Busche". Zweiter Schluessel ist deshalb Name plus
Geburtstag. Der Vergleich traegt hier, weil ohnehin nur Eintraege zu ein und
derselben Rufnummer verglichen werden - ueber den Gesamtbestand waere er
unbrauchbar, weil 0000-00-00 als Geburtstag massenhaft vorkommt.

Nachgeprueft ueber alle 2739 bekannten Rufnummern: jede liefert einen
Treffer, keine einzige eine doppelte Identitaet, groesste Trefferzahl 16 an
einer Nummer. 87 Nummern sind neu auffindbar, darunter die von Anfrage
13037, die jetzt korrekt auf person_id 2266 zeigt. Listenlogik ueber alle
12793 Anfragen unveraendert fehlerfrei; der Vorschlag steht weiterhin nie
bei einer Anfrage mit bereits zugeordnetem Patienten. Kosten bleiben zwei
Abfragen je Seitenaufruf, unabhaengig von der Zeilenzahl.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 21:09:53 +02:00
clemensandClaude Opus 5 53cff55ac2 Bei Telefonanfragen den Patienten zur Rufnummer vorschlagen
Die Telefonanlage fuellt anfragen.anrufernummer inzwischen - die erste
Anfrage damit ist #13037. Wenn die Anlage den Anrufer nicht selbst zuordnen
konnte, wird die Nummer jetzt gegen persons.tele gesucht und der Treffer
angezeigt.

NormalisiereRufnummer() vergleicht ziffernweise und fuehrt +49 und 0049 auf
die nationale Schreibweise mit fuehrender 0 zurueck. Nur so finden die
Anzeige der Anlage und die von Hand gepflegten Nummern zusammen: von 2972
gepflegten Nummern stehen 2941 als reine Ziffern da, 22 mit Bindestrich,
6 international.

Bewusst nur ein Vorschlag, keine Zuordnung. 521 der 2972 Nummern gehoeren zu
mehr als einer Person - Familien teilen sich einen Anschluss, zwei Nummern
haengen sogar an 16 bzw. 18 Personen. Bei mehreren Treffern werden deshalb
die Namen aufgezaehlt statt einer ausgewaehlt, und auch der eindeutige Fall
ist als "nicht bestaetigt - nur ueber die Rufnummer erkannt" ausgewiesen.
Eine falsche Zuordnung haette die Daten des falschen Patienten neben die
Anfrage gestellt.

Nummern unter 7 Ziffern werden nicht gesucht: in persons stehen ein paar
Platzhalter mit 1 bis 5 Ziffern, die sonst wahllos zusammenfallen.

Der Index wird beim ersten Treffer einmal je Seitenaufruf aufgebaut und
danach wiederverwendet - die Liste zeigt bis zu einige tausend Zeilen, eine
Abfrage je Zeile waere zu teuer. Gemessen ueber 12800 Zeilen: genau eine
Abfrage gegen persons. Ohne Telefonanfrage mit brauchbarer Nummer wird der
Index gar nicht erst gebaut.

Angezeigt in der Anfrageliste (Kontaktspalte), auf der Hinweisseite fuer
Anfragen ohne zugeordneten Patienten und unter Antwort einsehen.

Geprueft: Normalisierung gegen internationale, geklammerte und mit
Leerzeichen geschriebene Nummern; Vorschlag bleibt aus bei Mail- und
Portalanfragen, bei fehlender, zu kurzer und unterdrueckter Nummer sowie
bei bereits zugeordnetem Patienten. Listenlogik ueber alle 12793 Anfragen
mit erzwungenen Treffern durchgerechnet - der Vorschlag steht nie bei einer
Anfrage, die schon einen Patienten hat. Fuer #13037 selbst gibt es keinen
Treffer, die Nummer ist in persons nicht gepflegt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 19:24:25 +02:00
clemensandClaude Opus 5 4dabad0b22 Anrufernummer verlinken, PraxisKI-Link auf /anfragen/
Der Hinweis aus dem letzten Commit zeigte auf die Startseite von PraxisKI.
Richtig ist https://ki-praxis.praxis.local/anfragen/.

Neue Spalte anfragen.anrufernummer VARCHAR(255) NULL wird jetzt bei
Telefonanfragen als anklickbarer tel:-Link ausgegeben. GetTelefonLink()
baut das Ziel aus Ziffern und einem fuehrenden Plus, zeigt die Nummer aber
unveraendert an - Leerzeichen, Klammern, Slashes und Durchwahl-Bindestriche
stoeren den Link damit nicht. Nummern ganz ohne Ziffer, etwa bei
unterdrueckter Rufnummer, bleiben Text ohne Link. Der Rohwert wird in der
Funktion maskiert.

GetAnruferZeile() liefert die beschriftete Zeile nur fuer source='telefon'
und nur, wenn eine Nummer da ist. In der Liste steht sie in der
Kontaktspalte, wo Telefonnummern hingehoeren; dort ersetzt sie bei Anfragen
ohne zugeordneten Patienten den Platzhalter "Kontaktdaten siehe Nachricht".
In Antwortformular, Hinweisseite, Antwort einsehen und Patientenhistorie
steht sie bei den uebrigen Anfrageinformationen. Die Abfragen der letzten
beiden lasen die Spalte nicht mit, sie ist dort ergaenzt.

Die Spalte ist derzeit in allen 309 Telefonanfragen NULL - die Anlage
schreibt sie noch nicht. Bis dahin aendert sich die Anzeige nicht, der
bisherige Rueckfall greift.

Geprueft: GetTelefonLink gegen internationale, geklammerte, mit Leerzeichen
und Durchwahl geschriebene Nummern sowie gegen Leerwert, NULL, reinen Text
und einen Injektionsversuch - das Script kommt maskiert heraus, nicht als
Markup. Listenlogik ueber alle 12789 Anfragen mit simulierter Nummer
durchgerechnet: der Link erscheint genau dann, wenn source='telefon' ist
und eine Nummer vorliegt, und Nicht-Telefonzeilen bekommen kein
zusaetzliches <br> in die Kontaktspalte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 17:41:36 +02:00
clemensandClaude Opus 5 e39c00105e Bei Telefonanfragen auf die Sprachnachricht in PraxisKI hinweisen
anfragen.nachricht enthaelt bei source='telefon' nur das automatisch
erzeugte Transkript der Sprachnachricht, das Erkennungsfehler enthalten
kann. Bisher war aus der Oberflaeche nicht ersichtlich, dass es die
Aufnahme selbst noch gibt und wo sie liegt.

Neue Funktion GetSprachnachrichtHinweis() gibt fuer source='telefon' einen
Hinweis mit Link auf https://ki-praxis.praxis.local/ aus, fuer jede andere
Herkunft einen leeren String. Der Link ist nur aus dem Praxisnetz
erreichbar, das steht im Hinweis dabei.

Eingesetzt an allen fuenf Stellen, an denen eine Telefonanfrage sichtbar
wird: Anfrageliste, Antwortformular, Hinweisseite fuer Anfragen ohne
zugeordneten Patienten, Antwort einsehen und Patientenhistorie. Die
Abfragen fuer die letzten beiden lasen source bisher nicht mit, die Spalte
ist dort ergaenzt.

Nachgeprueft gegen die Datenbank: alle vier Abfragen liefern source, und
der Hinweis erscheint ueber alle 12785 Anfragen hinweg genau dann, wenn
source='telefon' ist - je Zeile geprueft, keine Ausnahme. Beide Dateien
bleiben UTF-8 ohne BOM.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 13:07:29 +02:00
clemensandClaude Opus 5 f19c490f44 Telefon-Anfragen ohne Personenbezug wieder anzeigen
Seit dem 02.09.2026 legt die Telefonanlage Anfragen mit source='telefon'
an. Von 309 solchen Zeilen haben 207 keine requester_person_id, weil der
Anrufer nicht zugeordnet werden konnte - darunter alle 33 offenen. Saemtliche
Anzeigepfade haengen per INNER JOIN an persons, wodurch genau diese Zeilen
kommentarlos aus dem Ergebnis fielen. Die Ansicht "Unbeantwortete Anfragen"
zeigte dadurch 2 statt 35 Eintraege.

Bei mail- und portal-Anfragen konnte das nicht auffallen: die Formulare
legen die Person vorher in persons an und brechen sonst ab (rezepte.php),
entsprechend hat dort keine einzige Zeile eine fehlende Person. personid
und userid stehen zwar auch bei mail auf 0, sind aber tote Legacy-Spalten
und tragen den Personenbezug nicht.

Die Anzeigepfade verwenden jetzt LEFT JOIN und zeigen ohne Treffer
"Kein Patient zugeordnet" statt einer leeren Zeile: Liste, Antworten,
Loesch-Dialog, Loeschen ohne Mail und Antwort einsehen. Die beiden Knoepfe,
die zwingend eine Mailadresse brauchen - Antworten und Loeschen mit Mail -
erscheinen nur noch mit zugeordneter Person; wer das Antwortformular direkt
aufruft, bekommt einen Hinweis samt Transkript statt eines Abbruchs.
"Telefonisch beantwortet" brauchte keine Aenderung, dieser Weg fragt nur
die anfrageid ab.

Der Mailversand in functions.inc.php und die patientenbezogene Historie
behalten ihren INNER JOIN - dort ist eine Person Voraussetzung.

Neu ist GetAnfrageHerkunft(). Die Liste leitete die Herkunft bisher aus
sicherenachricht ab, was Telefonanfragen mangels Wert als "Mailanfrage"
etikettierte. Nur dieser dritte Fall kommt aus source. Die Unterscheidung
intern/Mail bleibt bewusst an sicherenachricht haengen, weil insertAnfrage()
kein source setzt und 304 interne Anfragen deshalb als source='mail' in der
Tabelle stehen; ueber source vergeben haette das Label 9871 Bestandszeilen
umetikettiert, darunter die datenschutzrelevante Unterscheidung.

Nachgeprueft gegen die Datenbank: die Ansicht liefert statt 2 nun 35 Zeilen
mit allen 33 offenen Telefon-Anfragen. Der Renderblock ueber alle 12784
Zeilen simuliert, error_reporting=E_ALL als Exception, ohne Warnung; das
Herkunfts-Label bleibt fuer 12475 Bestandszeilen unveraendert und aendert
sich nur fuer die 309 Telefon-Zeilen. Alle fuenf von der Liste erreichbaren
Aktionen laden fuer eine Zeile ohne Person.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 12:56:45 +02:00
clemensandClaude Opus 5 6d903ac4fc CSRF-Schutz auf intern/ ausweiten und BOMs entfernen
intern/ enthaelt dieselben Patientendaten wie der Adminbereich und war
bisher genauso ungeschuetzt. config.inc.php aktiviert den Token-Filter
jetzt fuer beide Verzeichnisse. Eigenen AJAX-Code oder Nicht-HTML-Ausgaben
gibt es dort nicht, die Umstellung betrifft nur Formulare.

intern/login.php rief nach dem Include ein zweites session_start() auf und
setzte danach session.gc_maxlifetime und session.cookie_lifetime per
ini_set(). Beides lief ins Leere, weil die Sitzung zu dem Zeitpunkt schon
laeuft, und erzeugte Warnungen. Die Lebensdauer kommt aus
session_set_cookie_params() in config.inc.php.

13 Dateien begannen mit einem UTF-8-BOM. Die drei Bytes gehen vor dem
Include raus, womit die Header gesendet sind und
session_set_cookie_params() sowie session_start() in config.inc.php
scheitern - genau die Flags, um die es hier geht. Sichtbar wird das nur
ohne output_buffering, aber darauf sollte sich die Sitzungssicherheit
nicht verlassen.

Nachgeprueft mit output_buffering=0 und error_reporting=E_ALL: admin/ und
intern/ melden keine Header- oder Sessionwarnungen mehr, POSTs ohne Token
liefern 403, mit Token laufen sie durch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 15:16:40 +02:00
clemensandClaude Opus 5 86115b18a7 Patientendaten maskieren und SQL-Injection in togoadmin entfernen
Stored XSS
Die Anfrageuebersicht und das Antwortformular haben Nachricht, Medikamente,
Dateiname und Adressdaten des Patienten roh in HTML-Strings gesetzt.
Gespeichert wird der Text nur mit trim(), ein Patient konnte darueber
Skriptcode in den Browser der Mitarbeiterin einschleusen - mit deren
Sitzung. Die Werte laufen jetzt beim Auslesen durch e(). Der dritte Zweig
(Antwort einsehen) maskierte bereits bei der Ausgabe und bleibt, wie er
ist.

SQL-Injection
togoadmin.php interpolierte $_GET["id"] an drei Stellen ungecastet in
UPDATE-Statements, waehrend die Nachbarzeilen bereits (int) verwenden.
Dazu drei reflektierte Ausgaben desselben Wertes in Formularfelder.
Beides auf (int) umgestellt.

Formularziele
$_SERVER['PHP_SELF'] enthaelt bei Aufrufen wie /admin/anfragen.php/"><script>
auch den angehaengten Pfad und landete an 72 Stellen ungeprueft im HTML.
Ersetzt durch self_action() aus inc/security.inc.php, das den Basisnamen
des Skripts maskiert zurueckgibt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 14:19:13 +02:00
clemensandClaude Opus 5 19f1ac7ec8 CSRF-Schutz, Brute-Force-Bremse und wirksame Session-Cookie-Flags
Neu: inc/security.inc.php, eingebunden von inc/config.inc.php.

CSRF
Der Adminbereich hatte keinerlei Schutz gegen fremde POSTs. Jetzt haengt
csrf_inject_output() als Ausgabefilter das Token an jedes POST-Formular und
csrf_require() weist POSTs ohne gueltiges Token ab. Beides wird nur fuer
Skripte unterhalb von admin/ aktiviert, der oeffentliche Bereich bleibt
unveraendert. Der Umweg ueber den Ausgabefilter erspart es, die rund 60
bestehenden Formulare einzeln anzufassen; der AJAX-Aufruf auf
mailtemplate.php schickt das Token als Feld mit.

Session-Cookie-Flags
config.inc.php setzt secure, httponly und samesite - aber 25 Dateien in
admin/ und intern/ riefen session_start() vor dem Include auf, womit die
Parameter wirkungslos waren. Die vorgezogenen Aufrufe sind entfernt,
config.inc.php startet die Sitzung nur noch, wenn keine laeuft.
admin/logout.php musste umgestellt werden, weil dort session_destroy()
vor dem Include stand; die Cookies werden jetzt mit denselben Parametern
geloescht, mit denen sie gesetzt wurden.

Brute-Force
Nach 5 Fehlversuchen je Konto oder 20 je IP ist die Anmeldung 15 Minuten
gesperrt, gezaehlt in der neuen Tabelle login_attempts. Fehlt die Tabelle,
laeuft der Login wie bisher - gleiche Vorgehensweise wie bei
securitytokensHatAblaufspalte(). Eine erfolgreiche Anmeldung raeumt die
Fehlversuche des Kontos ab.

Passwort vergessen
admin/passwortvergessen.php uebergab $mail und $body an SendMailMessage();
beide Variablen gibt es dort nicht, sie heissen $empfaenger und $text. Die
Mail ging deshalb nie raus, obwohl der Reset-Code gesetzt wurde.

Getestet gegen einen lokalen PHP-Server: Token wird eingesetzt, POST ohne
Token liefert 403, mit Token laeuft der Login normal, der sechste
Fehlversuch wird gesperrt, das Sitzungscookie traegt secure/HttpOnly/
SameSite. Oeffentliche Seiten sind unveraendert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 14:18:59 +02:00
40 changed files with 877 additions and 237 deletions
+163 -89
View File
@@ -1,5 +1,4 @@
<?php
session_start();
<?php
// WICHTIG: Pfade aus /admin heraus korrekt auflösen
require_once __DIR__ . "/../inc/config.inc.php";
@@ -28,7 +27,7 @@ include("templates/footer.inc.php");
Herzlich Willkommen im internen Bereich!<br><br>
</div>
<div style="width:200px;">
<form action="<?php echo htmlspecialchars($_SERVER['PHP_SELF'], ENT_QUOTES, 'UTF-8'); ?>" id="formbenutzersuche" method="POST">
<form action="<?php echo self_action(); ?>" id="formbenutzersuche" method="POST">
<input type="hidden" name="aktion" value="benutzersuche" />
<input type="hidden" name="userid_input" id="userid_input" />
<label>Benutzersuche Anfragen:</label>
@@ -67,28 +66,28 @@ if(!check_worker()){
echo "</div><div class='container main-container' style='background-color:white;'>";
echo '<div style="float: left;padding: 20px;">
<form action="' . $_SERVER["PHP_SELF"] .'" method=POST>
<form action="' . self_action() .'" method=POST>
<input type=hidden name=aktion value="1">
<input type=hidden name=art value="1">
<input type=submit class="btn btn-primary btn-sm" value="Unbeantworte Anfragen">
</form>
</div>';
echo '<div style="float: left;padding: 20px;">
<form action="' . $_SERVER["PHP_SELF"] .'" method=POST>
<form action="' . self_action() .'" method=POST>
<input type=hidden name=aktion value="1">
<input type=hidden name=art value="2">
<input type=submit class="btn btn-primary btn-sm" value="Letzten 100 Anfragen">
</form>
</div>';
echo '<div style="float: left;padding: 20px;">
<form action="' . $_SERVER["PHP_SELF"] .'" method=POST>
<form action="' . self_action() .'" method=POST>
<input type=hidden name=aktion value="1">
<input type=hidden name=art value="3">
<input type=submit class="btn btn-primary btn-sm" value="Letzten 500 Anfragen">
</form>
</div>';
echo '<div style="float: left;padding: 20px;">
<form action="' . $_SERVER["PHP_SELF"] .'" method=POST>
<form action="' . self_action() .'" method=POST>
<input type=hidden name=aktion value="1">
<input type=hidden name=art value="4">
<input type=submit class="btn btn-primary btn-sm" value="Alle Anfragen">
@@ -116,8 +115,8 @@ if(!check_worker()){
p.ort, p.plz, p.strasse,
aa.artname
FROM anfragen a
JOIN persons p ON a.requester_person_id = p.person_id
JOIN anfrageart aa ON a.anforderungart = aa.artid
LEFT JOIN persons p ON a.requester_person_id = p.person_id
LEFT JOIN anfrageart aa ON a.anforderungart = aa.artid
";
$params = [];
@@ -175,39 +174,46 @@ if(!check_worker()){
$date_created = $row["create_time"];
$update_time = $row["update_time"];
$vorname = $row["vorname"];
$nachname = $row["nachname"];
$mail = $row["email"];
$tel = $row["tele"];
// Alle vom Patienten stammenden Felder werden hier maskiert.
// Sie landen weiter unten unveraendert in einem HTML-String;
// ohne e() koennte ein Anfragetext Skriptcode in den Browser
// der Mitarbeiterin einschleusen.
$vorname = e((string)$row["vorname"]);
$nachname = e((string)$row["nachname"]);
$mail = e((string)$row["email"]);
$tel = e((string)$row["tele"]);
$beantwortet = $row["beantwortet"];
$geburtstag = $row["geburtstag"];
$geburtstag = e((string)$row["geburtstag"]);
$ausgabegeburstag = $geburtstag;
$ort = $row["ort"];
$plz = $row["plz"];
$strasse = $row["strasse"];
$ort = e((string)$row["ort"]);
$plz = e((string)$row["plz"]);
$strasse = e((string)$row["strasse"]);
$nachricht = $row["nachricht"];
$medikamenteins = $row["medikament1"];
$medikamentzwei = $row["medikament2"];
$medikamentdrei = $row["medikament3"];
$medikamentvier = $row["medikament4"];
$medikamentfuenf = $row["medikament5"];
$medikamentsechs = $row["medikament6"];
$dateiname = $row["dateiname"];
$nachricht = e((string)$row["nachricht"]);
$medikamenteins = e((string)$row["medikament1"]);
$medikamentzwei = e((string)$row["medikament2"]);
$medikamentdrei = e((string)$row["medikament3"]);
$medikamentvier = e((string)$row["medikament4"]);
$medikamentfuenf = e((string)$row["medikament5"]);
$medikamentsechs = e((string)$row["medikament6"]);
$dateiname = e((string)$row["dateiname"]);
$anfrageart = $row["artname"];
// artname kann seit dem LEFT JOIN leer sein.
$anfrageart = e((string)($row["artname"] ?? ""));
if($anfrageart === ""){
$anfrageart = "unbekannte Art";
}
$ordnungsid = $row["ordnungsid"];
$ordnungsstring = GetOrdnungsid($ordnungsid);
$WeitereInfos= "";
$sicherenachricht = $row["sicherenachricht"];
if($sicherenachricht == "1"){
$WeitereInfos .= "Interne Anfrage<br>";
}else{
$WeitereInfos .= "Mailanfrage<br>";
}
// Telefonanfragen liessen sicherenachricht NULL und wurden
// dadurch faelschlich als Mailanfrage etikettiert.
$WeitereInfos .= GetAnfrageHerkunft($row["source"] ?? "mail", $sicherenachricht) . "<br>";
$WeitereInfos .= GetSprachnachrichtHinweis($row["source"] ?? "mail");
if($medikamenteins){
$WeitereInfos .= "Medikament1: $medikamenteins<br>";
}
@@ -241,26 +247,53 @@ if(!check_worker()){
$farbe = GetStatusFarbe($checked);
$checkausgabe = GetStatus($checked);
$userausgabe = $vorname . " " . $nachname . "<br>" . $ausgabegeburstag;
$adresse = $plz . " " . $ort . "<br>" . $strasse ;
$adresse = $adresse;
echo "<tr style='background-color:". $farbe. ";'><td>$userausgabe <br> $mail <br>Tel:<a href='tel:".$tel."'>$tel</a></td><td>$adresse</td><td>$ordnungsstring - $anfrageart <br>$WeitereInfos</td><td>$datumausgabe</td><td>$update_time<br>$checkausgabe<br>$ausgabeworker</td><td>
// Ohne Treffer im LEFT JOIN gibt es keine Personendaten -
// typisch fuer Telefonanfragen, deren Anrufer nicht zugeordnet
// werden konnte. Frueher fielen diese Zeilen durch den INNER
// JOIN komplett aus der Liste.
$hatPerson = ($row["vorname"] !== null || $row["nachname"] !== null);
// Die Anlage haelt die Rufnummer des Anrufers getrennt von den
// Stammdaten - sie kann von der hinterlegten abweichen.
$anruferzeile = GetAnruferZeile($row["source"] ?? "mail", $row["anrufernummer"] ?? null);
// Wer koennte angerufen haben? Nur ein Vorschlag anhand der
// Rufnummer, die Auswahl trifft die Mitarbeiterin.
$anrufervorschlag = GetPatientZuRufnummerAusgabe($pdo, $row["source"] ?? "mail", $row["anrufernummer"] ?? null, $row["requester_person_id"] ?? null);
if($hatPerson){
$userausgabe = $vorname . " " . $nachname . "<br>" . $ausgabegeburstag;
$adresse = $plz . " " . $ort . "<br>" . $strasse ;
$kontakt = "$mail <br>Tel:<a href='tel:$tel'>$tel</a>";
if($anruferzeile !== ""){
$kontakt .= "<br>" . $anruferzeile;
}
}else{
$userausgabe = "<i>Kein Patient zugeordnet</i>";
$adresse = "-";
$kontakt = ($anruferzeile !== "") ? $anruferzeile : "<i>Kontaktdaten siehe Nachricht</i>";
$kontakt .= $anrufervorschlag;
}
echo "<tr style='background-color:". $farbe. ";'><td>$userausgabe <br> $kontakt</td><td>$adresse</td><td>$ordnungsstring - $anfrageart <br>$WeitereInfos</td><td>$datumausgabe</td><td>$update_time<br>$checkausgabe<br>$ausgabeworker</td><td>";
// "Antworten" verschickt eine Mailvorlage. Ohne zugeordnete Person
// gibt es keine Adresse, der Knopf wuerde ins Leere laufen.
if($hatPerson){
echo "
<div style='float: left;margin:15px; height: 20px;'>
<form action='". $_SERVER["PHP_SELF"] ."' method=POST>
<form action='". self_action() ."' method=POST>
<input type=hidden name=aktion value=2>
<input type=hidden name=anfrageid value=$anfrageid>
<input type=submit class='btn btn-primary' value='Antworten'>
</form>
</div>
</div>";
}
echo "
<div style='float: left;margin:15px; height: 20px;'>
<form action='". $_SERVER["PHP_SELF"] ."' method=POST>
<form action='". self_action() ."' method=POST>
<input type=hidden name=aktion value=20>
<input type=hidden name=anfrageid value=$anfrageid>
<input type=submit class='btn btn-primary' value='Telefonisch beantwortet'>
</form>
</div>
<div style='float: left;margin:15px; height: 20px;'>
<form action='". $_SERVER["PHP_SELF"] ."' method=POST>
<form action='". self_action() ."' method=POST>
<input type=hidden name=aktion value=8>
<input type=hidden name=anfrageid value=$anfrageid>
<input type=submit class='btn btn-primary' value='Löschen'>
@@ -269,7 +302,7 @@ if(!check_worker()){
if($beantwortet){
echo "
<div style='float: left;margin:15px; height: 20px;'>
<form action='". $_SERVER["PHP_SELF"] ."' method=POST>
<form action='". self_action() ."' method=POST>
<input type=hidden name=aktion value=11>
<input type=hidden name=anfrageid value=$anfrageid>
<input type=submit class='btn btn-primary' value='Antwort einsehen'>
@@ -290,7 +323,7 @@ if(!check_worker()){
// Benutzer nachtragen
}else if (($_POST["aktion"] ?? '') == "2") {
echo "<form action='". $_SERVER['PHP_SELF'] . "' method=POST>";
echo "<form action='". self_action() . "' method=POST>";
$anfrageid = (int)($_POST["anfrageid"] ?? 0);
@@ -305,8 +338,8 @@ if(!check_worker()){
p.ort, p.plz, p.strasse,
aa.artname
FROM anfragen a
INNER JOIN persons p ON a.requester_person_id = p.person_id
INNER JOIN anfrageart aa ON a.anforderungart = aa.artid
LEFT JOIN persons p ON a.requester_person_id = p.person_id
LEFT JOIN anfrageart aa ON a.anforderungart = aa.artid
WHERE a.anfrageid = ?
LIMIT 1
";
@@ -320,6 +353,27 @@ if(!check_worker()){
exit;
}
// Ohne zugeordnete Person gibt es keine Mailadresse - die
// Antwortvorlagen unten wuerden ins Leere laufen. Betrifft vor
// allem Telefonanfragen ohne erkannten Anrufer.
if ($row["vorname"] === null && $row["nachname"] === null) {
echo "<div class='alert alert-warning' role='alert'>";
echo "<b>Dieser Anfrage ist kein Patient zugeordnet.</b><br>";
echo "Herkunft: " . e(GetAnfrageHerkunft($row["source"] ?? "mail", $row["sicherenachricht"] ?? null)) . "<br>";
echo "Eine Antwort per Mail ist deshalb nicht moeglich. ";
echo "Bitte telefonisch beantworten und die Anfrage anschliessend ";
echo "ueber <b>Telefonisch beantwortet</b> abschliessen.";
echo "</div>";
echo GetAnruferZeile($row["source"] ?? "mail", $row["anrufernummer"] ?? null);
echo GetPatientZuRufnummerAusgabe($pdo, $row["source"] ?? "mail", $row["anrufernummer"] ?? null, $row["requester_person_id"] ?? null);
echo GetSprachnachrichtHinweis($row["source"] ?? "mail");
echo "<b>Nachricht:</b><br>" . nl2br(e((string)$row["nachricht"])) . "<br><br>";
ZuruckzumAnfrageOverView();
echo "</form>";
include(__DIR__ . "/templates/footer.inc.php");
exit;
}
// Ticket und Antwort ansehen.
$abholort = $row["abholort"] ?? null;
$sicherenachricht = $row["sicherenachricht"] ?? null;
@@ -330,34 +384,38 @@ if(!check_worker()){
$antwortid = $row["antwortid"] ?? null;
$date_created = $row["create_time"];
$vorname = $row["vorname"];
$nachname = $row["nachname"];
$mail = $row["email"];
$tel = $row["tele"];
// Patientendaten maskieren - sie werden unten in HTML-Strings
// eingesetzt und ausgegeben.
$vorname = e((string)$row["vorname"]);
$nachname = e((string)$row["nachname"]);
$mail = e((string)$row["email"]);
$tel = e((string)$row["tele"]);
$geburtstag = $row["geburtstag"];
$geburtstag = e((string)$row["geburtstag"]);
$ausgabegeburstag = $geburtstag;
$ort = $row["ort"];
$plz = $row["plz"];
$strasse = $row["strasse"];
$ort = e((string)$row["ort"]);
$plz = e((string)$row["plz"]);
$strasse = e((string)$row["strasse"]);
$ordnungsid = $row["ordnungsid"];
$ordnungsstring = GetOrdnungsid($ordnungsid);
$nachricht = $row["nachricht"];
$nachricht = e((string)$row["nachricht"]);
$medikamenteins = $row["medikament1"];
$medikamentzwei = $row["medikament2"];
$medikamentdrei = $row["medikament3"];
$medikamentvier = $row["medikament4"];
$medikamentfuenf = $row["medikament5"];
$medikamentsechs = $row["medikament6"];
$medikamenteins = e((string)$row["medikament1"]);
$medikamentzwei = e((string)$row["medikament2"]);
$medikamentdrei = e((string)$row["medikament3"]);
$medikamentvier = e((string)$row["medikament4"]);
$medikamentfuenf = e((string)$row["medikament5"]);
$medikamentsechs = e((string)$row["medikament6"]);
$anfrageart = $row["artname"];
$anfrageart = e((string)$row["artname"]);
$antworttext = $row["antworttext"] ?? null;
$WeitereInfos = "";
$WeitereInfos .= GetAnruferZeile($row["source"] ?? "mail", $row["anrufernummer"] ?? null);
$WeitereInfos .= GetSprachnachrichtHinweis($row["source"] ?? "mail");
if ($medikamenteins) $WeitereInfos .= "Medikament1: $medikamenteins<br>";
if ($medikamentzwei) $WeitereInfos .= "Medikament2: $medikamentzwei<br>";
if ($medikamentdrei) $WeitereInfos .= "Medikament3: $medikamentdrei<br>";
@@ -464,12 +522,12 @@ if(!check_worker()){
echo "<h1>E-Mail bearbeiten</h1><br>";
echo "<form action='". $_SERVER['PHP_SELF'] . "' method=POST>";
echo "<form action='". self_action() . "' method=POST>";
echo "Betreff:<br>";
echo "<input class='form-control' name=betreff type=text value='$betreff' id='betreff' onclick='checkTemplate()'><br>";
echo '<textarea height=400 name="body" id="trumbowyg-demo" ></textarea>';
echo "<input name=aktion type=hidden value=3>";
echo "<input name=anfrageid type=hidden value='". $_POST["anfrageid"] ."' id='anfrageid'><br><br>";
echo "<input name=anfrageid type=hidden value='". (int)($_POST["anfrageid"] ?? 0) ."' id='anfrageid'><br><br>";
echo "<input type=submit class='btn btn-primary' value='Mail versenden'><br>";
echo '</form>';
echo "<br><br>";
@@ -624,7 +682,7 @@ if(!check_worker()){
So sollten Sie bei Urlaub ab Montag hier den Starttermin auf Freitag oder Samstag stellen. <br>
Gleiches gilt für ein Ende an einem Freitag. Tragen Sie dann hier als Ende Sonntag ein.<br><br><br>";
echo '<form action="' . htmlspecialchars($_SERVER['PHP_SELF'], ENT_QUOTES, 'UTF-8') . '" method="POST">';
echo '<form action="' . self_action() . '" method="POST">';
foreach ($rows as $row) {
$start = (string)$row["start"];
@@ -754,7 +812,7 @@ if(!check_worker()){
echo "<h3>Aktuell gibt es folgende Einträge für die Notfallsprechstunde</h3>";
echo "Soll die Notfallsprechstunde deaktiviert / gelöscht werden, dann das Anfangs- und Enddatum in die Vergangenheit legen und den Eintrag speichern.<br><br><br>";
echo '<form action="' . htmlspecialchars($_SERVER['PHP_SELF'], ENT_QUOTES, 'UTF-8') . '" method="POST">';
echo '<form action="' . self_action() . '" method="POST">';
foreach ($rows as $row) {
$start = $row["start"];
@@ -840,7 +898,7 @@ if(!check_worker()){
}
die("Fehler beim Eintragen in der Datenbank: " . $e->getMessage());
}
echo' <form action="'. $_SERVER["PHP_SELF"] .'" method=POST>
echo' <form action="'. self_action() .'" method=POST>
<input type=hidden name=aktion value=6>
<input type=submit class="btn btn-primary" value="Zurück">
</form>';
@@ -853,7 +911,7 @@ if(!check_worker()){
$stmt = $pdo->prepare("
SELECT *
FROM anfragen a
INNER JOIN persons p ON p.person_id = a.requester_person_id
LEFT JOIN persons p ON p.person_id = a.requester_person_id
WHERE a.anfrageid = :anfrageid
LIMIT 1
");
@@ -870,17 +928,23 @@ if(!check_worker()){
$vorname = $rowtime["vorname"];
$nachname = $rowtime["nachname"];
$userausgabe = $vorname . " " . $nachname;
// Telefonanfragen ohne erkannten Anrufer haben keine Person.
$hatPerson = ($vorname !== null || $nachname !== null);
$userausgabe = $hatPerson ? ($vorname . " " . $nachname) : "Kein Patient zugeordnet";
echo "Name:<br>$userausgabe<br>Anforderung: $impfstofftext<br>";
echo "Wollen Sie wirklich diesen Eintrag löschen?<br>Dieses ist nicht rückgängig zu machen!<br>Dann bestätigen Sie die Abmeldung:<br>";
echo "<form action='". $_SERVER['PHP_SELF'] . "' method=POST>";
echo '<input type="hidden" name="aktion" id="aktion" value="9" />';
echo '<input type="hidden" name="anfrageid" id="anfrageid" value="'. $anfrageid .'" /><br>';
echo '<input type="submit" class="btn btn-primary" id="submitbox" value="Anforderung löschen (mit Mail)" />';
echo "<br>";
echo "<form action='". $_SERVER['PHP_SELF'] . "' method=POST>";
// Loeschen mit Mail setzt eine Mailadresse voraus.
if ($hatPerson) {
echo "<form action='". self_action() . "' method=POST>";
echo '<input type="hidden" name="aktion" id="aktion" value="9" />';
echo '<input type="hidden" name="anfrageid" id="anfrageid" value="'. $anfrageid .'" /><br>';
echo '<input type="submit" class="btn btn-primary" id="submitbox" value="Anforderung löschen (mit Mail)" />';
echo "</form>";
echo "<br>";
}
echo "<form action='". self_action() . "' method=POST>";
echo '<input type="hidden" name="aktion" id="aktion" value="10" />';
echo '<input type="hidden" name="anfrageid" id="anfrageid" value="'. $anfrageid .'" /><br>';
echo '<input type="submit" class="btn btn-primary" id="submitbox" value="Anforderung löschen (ohne Mail)" />';
@@ -966,7 +1030,7 @@ if(!check_worker()){
p.tele,
p.geburtstag
FROM anfragen a
INNER JOIN persons p ON p.person_id = a.requester_person_id
LEFT JOIN persons p ON p.person_id = a.requester_person_id
WHERE a.anfrageid = :anfrageid
LIMIT 1
");
@@ -1033,6 +1097,8 @@ if(!check_worker()){
a.medikament2,
a.sicherenachricht,
a.abholort,
a.source,
a.anrufernummer,
p.person_id,
p.vorname,
@@ -1046,8 +1112,8 @@ if(!check_worker()){
aa.artname AS anfrageart_name
FROM anfragen a
INNER JOIN persons p ON p.person_id = a.requester_person_id
INNER JOIN anfrageart aa ON a.anforderungart = aa.artid
LEFT JOIN persons p ON p.person_id = a.requester_person_id
LEFT JOIN anfrageart aa ON a.anforderungart = aa.artid
WHERE a.anfrageid = :anfrageid
LIMIT 1
");
@@ -1089,6 +1155,9 @@ if(!check_worker()){
/* --- Anzeige-Strings --- */
$WeitereInfos = "";
$WeitereInfos .= GetAnruferZeile($row["source"] ?? "mail", $row["anrufernummer"] ?? null);
$WeitereInfos .= GetPatientZuRufnummerAusgabe($pdo, $row["source"] ?? "mail", $row["anrufernummer"] ?? null, $row["person_id"] ?? null);
$WeitereInfos .= GetSprachnachrichtHinweis($row["source"] ?? "mail");
if (!empty($medikamenteins)) {
$WeitereInfos .= "Medikament1: " . htmlspecialchars($medikamenteins, ENT_QUOTES, 'UTF-8') . "<br>";
}
@@ -1196,7 +1265,7 @@ if(!check_worker()){
$stmt->execute([':term' => $like]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo "<form action='" . $_SERVER['PHP_SELF'] . "' id='formbenutzersuche' method='POST'>";
echo "<form action='" . self_action() . "' id='formbenutzersuche' method='POST'>";
echo '<input type="hidden" name="aktion" value="benutzersuche" />';
echo '<label>Benutzersuche:</label>';
echo '<select class="form-control" name="userid_input" required>';
@@ -1279,6 +1348,8 @@ if(!check_worker()){
a.medikament4,
a.medikament5,
a.medikament6,
a.source,
a.anrufernummer,
aa.artname AS anfrageart_name,
@@ -1339,6 +1410,8 @@ if(!check_worker()){
$ordnungsstring = GetOrdnungsid($ordnungsid);
$WeitereInfos = "";
$WeitereInfos .= GetAnruferZeile($row["source"] ?? "mail", $row["anrufernummer"] ?? null);
$WeitereInfos .= GetSprachnachrichtHinweis($row["source"] ?? "mail");
if (!empty($med1)) $WeitereInfos .= "Medikament1: " . htmlspecialchars($med1, ENT_QUOTES, 'UTF-8') . "<br>";
if (!empty($med2)) $WeitereInfos .= "Medikament2: " . htmlspecialchars($med2, ENT_QUOTES, 'UTF-8') . "<br>";
if (!empty($med3)) $WeitereInfos .= "Medikament3: " . htmlspecialchars($med3, ENT_QUOTES, 'UTF-8') . "<br>";
@@ -1374,7 +1447,7 @@ if(!check_worker()){
. "</td>";
echo "<td>
<form action='" . $_SERVER["PHP_SELF"] . "' method='POST'>
<form action='" . self_action() . "' method='POST'>
<input type='hidden' name='aktion' value='11'>
<input type='hidden' name='anfrageid' value='" . (int)$anfrageid . "'>
<button type='submit' class='btn btn-primary btn-sm'>Antwort einsehen</button>
@@ -1429,7 +1502,7 @@ if(!check_worker()){
// Umbuchen-Form (person_id weitergeben)
echo "
<form action='" . $_SERVER["PHP_SELF"] . "' method='POST'>
<form action='" . self_action() . "' method='POST'>
<input type='hidden' name='aktion' value='umbuchen'>
<input type='hidden' name='person_id' value='" . (int)$personId . "'>
<input type='submit' class='btn btn-primary btn-sm' value='Neuer Termin'>
@@ -1798,7 +1871,7 @@ if(!check_worker()){
echo "<br><br>";
echo "<form action='". $_SERVER['PHP_SELF'] . "' method=POST>";
echo "<form action='". self_action() . "' method=POST>";
echo "Betreff:<br>";
echo "<input class='form-control' name=betreff type=text value='". $betreff . "'> <br>";
echo '<textarea height=200 name="body" id="mytextarea"> '. $body . '</textarea>';
@@ -1832,7 +1905,7 @@ if(!check_worker()){
// Auswahl der E-Mail Vorlagen
} else if (($_POST["aktion"] ?? '') == "16") {
echo "<form action='" . $_SERVER['PHP_SELF'] . "' method='POST'>";
echo "<form action='" . self_action() . "' method='POST'>";
echo "Wählen Sie die zu bearbeitende Mailvorlage aus:<br><br>";
$stmt = $pdo->prepare("
@@ -1903,7 +1976,7 @@ if(!check_worker()){
echo "<br><br>";
echo "<form action='". $_SERVER['PHP_SELF'] . "' method=POST>";
echo "<form action='". self_action() . "' method=POST>";
echo "Name der Vorlage:<br>";
echo "<input class='form-control' name=name type=text value='$name' required><br>";
echo "Betreff:<br>";
@@ -1979,25 +2052,25 @@ if(!check_worker()){
echo '<h3>Welche Aktion möchtest du durchführen?</h3>
<table width=100%><tr><td width=40%>
<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>
<form action="'. self_action() .'" method=POST>
<input type=hidden name=aktion value=1>
<input type=submit class="btn btn-primary btn-lg" value="Formular-Anfragen bearbeiten">
</form>
</td></tr>
<tr><td><br><br>
<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>
<form action="'. self_action() .'" method=POST>
<input type=hidden name=aktion value=18>
<input type=submit class="btn btn-primary" value="Mailvorlagen anlegen">
</form>
</td></tr>
<tr><td><br><br>
<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>
<form action="'. self_action() .'" method=POST>
<input type=hidden name=aktion value=12>
<input type=submit class="btn btn-primary" value="Formular Auswertung">
</form>
</td></tr>
<tr><td><br><br>
<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>
<form action="'. self_action() .'" method=POST>
<input type=hidden name=aktion value=16>
<input type=submit class="btn btn-primary" value="Mailvorlagen anpassen">
</form>
@@ -2005,13 +2078,13 @@ if(!check_worker()){
<tr></tr>
<tr><td><h3>Urlaub / Hinweis planen</h3></td><td></td></tr>
<tr><td>
<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>
<form action="'. self_action() .'" method=POST>
<input type=hidden name=aktion value=4>
<input type=submit class="btn btn-primary btn-lg" value="Urlaub eintragen">
</form>
</td></tr>
<tr><td><br><br>
<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>
<form action="'. self_action() .'" method=POST>
<input type=hidden name=aktion value=6>
<input type=submit class="btn btn-primary" value="Notfallsprechstunde eintragen">
</form>
@@ -2043,7 +2116,7 @@ if(!check_worker()){
}
?>
<br><br><br><input type="button" class='btn btn-secondary' value="Zum Hauptmenü" onClick="location.href='<?php echo $_SERVER['PHP_SELF'];?>'">
<br><br><br><input type="button" class='btn btn-secondary' value="Zum Hauptmenü" onClick="location.href='<?php echo self_action();?>'">
</section>
@@ -2092,7 +2165,8 @@ if(!check_worker()){
url: 'mailtemplate.php',
data: {
templetid: templetid,
anfrageid: anfrageid
anfrageid: anfrageid,
csrf_token: "<?php echo csrf_token(); ?>"
},
dataType: 'json',
success: function (data) {
+1 -2
View File
@@ -1,5 +1,4 @@
<?php
session_start();
require_once(__DIR__ . "/../inc/config.inc.php");
require_once(__DIR__ . "/../inc/functions.inc.php");
@@ -24,7 +23,7 @@ if (!$user) {
<?php
##test2
echo '<div style="float: right; width: 200px; ">';
echo "<form action='". $_SERVER['PHP_SELF'] . "' id='formbenutzersuche' method=POST>";
echo "<form action='". self_action() . "' id='formbenutzersuche' method=POST>";
echo '<input type="hidden" name="aktion" value="benutzersuche" />';
echo '<input type="hidden" name="userid_input" id="userid_input" />';
echo '<label>Benutzersuche Anfragen:</label>
+1 -2
View File
@@ -1,5 +1,4 @@
<?php
session_start();
<?php
require_once __DIR__ . "/../inc/config.inc.php";
require_once __DIR__ . "/../inc/functions.inc.php";
+1 -1
View File
@@ -1,3 +1,3 @@
<?php
<?php
$_GET['view'] = 'event-create';
require_once __DIR__ . '/impfworkflow.php';
+1 -1
View File
@@ -1,3 +1,3 @@
<?php
<?php
$_GET['view'] = 'event-teilnehmer';
require_once __DIR__ . '/impfworkflow.php';
+1 -2
View File
@@ -1,5 +1,4 @@
<?php
session_start();
<?php
require_once __DIR__ . "/../inc/config.inc.php";
require_once __DIR__ . "/../inc/functions.inc.php";
+1 -1
View File
@@ -1,3 +1,3 @@
<?php
<?php
$_GET['view'] = 'teilnehmer';
require_once __DIR__ . '/impfworkflow.php';
+6 -9
View File
@@ -1,7 +1,4 @@
<?php
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
require_once(__DIR__ . "/../inc/config.inc.php");
@@ -67,25 +64,25 @@ include("templates/footer.inc.php");
echo '<h4>Welche Aktion möchtest du durchführen?</h4>
<table width=100%><tr><td width=40%>
<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>
<form action="'. self_action() .'" method=POST>
<input type=hidden name=aktion value=1>
<input type=submit class="btn btn-primary btn-lg" value="Formular-Anfragen bearbeiten">
</form>
</td></tr>
<tr><td><br><br>
<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>
<form action="'. self_action() .'" method=POST>
<input type=hidden name=aktion value=18>
<input type=submit class="btn btn-primary" value="Mailvorlagen anlegen">
</form>
</td></tr>
<tr><td><br><br>
<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>
<form action="'. self_action() .'" method=POST>
<input type=hidden name=aktion value=12>
<input type=submit class="btn btn-primary" value="Formular Auswertung">
</form>
</td></tr>
<tr><td><br><br>
<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>
<form action="'. self_action() .'" method=POST>
<input type=hidden name=aktion value=16>
<input type=submit class="btn btn-primary" value="Mailvorlagen anpassen">
</form>
@@ -93,13 +90,13 @@ include("templates/footer.inc.php");
<tr></tr>
<tr><td><h3>Urlaub / Hinweis planen</h3></td><td></td></tr>
<tr><td>
<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>
<form action="'. self_action() .'" method=POST>
<input type=hidden name=aktion value=4>
<input type=submit class="btn btn-primary btn-lg" value="Urlaub eintragen">
</form>
</td></tr>
<tr><td><br><br>
<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>
<form action="'. self_action() .'" method=POST>
<input type=hidden name=aktion value=6>
<input type=submit class="btn btn-primary" value="Notfallsprechstunde eintragen">
</form>
+1 -2
View File
@@ -1,5 +1,4 @@
<?php
session_start();
require_once(__DIR__ . "/../inc/config.inc.php");
require_once(__DIR__ . "/../inc/functions.inc.php");
@@ -55,7 +54,7 @@ function build_calendar($month, $year) {
$calendar .= "<td class='day' rel='$date' onClick='submitForm(\"".$date ."\")'>$currentDay";
}
$calendar .= "<form action='". $_SERVER['PHP_SELF'] . "' name='".$date ."' method=POST><input type=hidden name=searchdate value='". $date. "'><input type=hidden name=aktion value='4'></form>";
$calendar .= "<form action='". self_action() . "' name='".$date ."' method=POST><input type=hidden name=searchdate value='". $date. "'><input type=hidden name=aktion value='4'></form>";
$calendar .= AuswertungImpfungdailycalendar($date);
$calendar .= "</td>";
+66 -56
View File
@@ -1,9 +1,6 @@
<?php
// admin/bootstrap.php
ob_start(); // fängt zufälligen Output ab, verhindert "headers already sent" Folgeschäden
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
require_once(__DIR__ . "/../inc/config.inc.php");
require_once(__DIR__ . "/../inc/functions.inc.php");
@@ -14,61 +11,74 @@ if (!empty($_POST['email']) && !empty($_POST['passwort'])) {
$email = $_POST['email'];
$passwort = $_POST['passwort'];
$statement = $pdo->prepare("SELECT * FROM users WHERE email = :email LIMIT 1");
$statement->execute(['email' => $email]);
$user = $statement->fetch(PDO::FETCH_ASSOC);
// Brute-Force-Bremse: nach mehreren Fehlversuchen ist die Anmeldung fuer
// dieses Konto bzw. diese IP zeitweise gesperrt. Die Pruefung laeuft vor
// password_verify(), damit gesperrte Versuche gar nicht erst rechnen.
$sperreSekunden = login_gesperrt($pdo, $email);
if ($user && password_verify($passwort, $user['passwort'])) {
// Session IMMER setzen
session_regenerate_id(true);
$_SESSION['auth'] = [
'type' => 'admin',
'id' => (int)$user['id'],
];
// "Angemeldet bleiben" optional
if (!empty($_POST['angemeldet_bleiben'])) {
$identifier = bin2hex(random_bytes(16));
$securitytoken = bin2hex(random_bytes(32)); // stärker als random_string()
$hash = hash('sha256', $securitytoken);
// Ablaufdatum nur setzen, wenn die Spalte existiert - der Code laeuft
// damit vor und nach der Migration. Ablauf per NOW() in SQL, weil
// Webserver und DB-Server in verschiedenen Zeitzonen laufen.
if (securitytokensHatAblaufspalte($pdo)) {
$insert = $pdo->prepare("
INSERT INTO securitytokens (user_id, identifier, securitytoken, user_type, expires_at)
VALUES (:user_id, :identifier, :securitytoken, 'admin', DATE_ADD(NOW(), INTERVAL 30 DAY))
");
} else {
$insert = $pdo->prepare("
INSERT INTO securitytokens (user_id, identifier, securitytoken, user_type)
VALUES (:user_id, :identifier, :securitytoken, 'admin')
");
}
$insert->execute([
'user_id' => (int)$user['id'],
'identifier' => $identifier,
'securitytoken' => $hash
]);
$cookieOpts = [
'expires' => time() + 30*24*3600, // deckungsgleich mit dem Token-Ablauf
'path' => '/',
'secure' => true, // nur wenn HTTPS
'httponly' => true,
'samesite' => 'Lax',
];
setcookie('identifier', $identifier, $cookieOpts);
setcookie('securitytoken', $securitytoken, $cookieOpts);
}
header("Location: index.php");
exit;
if ($sperreSekunden > 0) {
$error_msg = login_sperr_text($sperreSekunden);
} else {
$error_msg = "E-Mail oder Passwort war ungültig<br><br>";
$statement = $pdo->prepare("SELECT * FROM users WHERE email = :email LIMIT 1");
$statement->execute(['email' => $email]);
$user = $statement->fetch(PDO::FETCH_ASSOC);
if ($user && password_verify($passwort, $user['passwort'])) {
login_versuch_merken($pdo, $email, true);
// Session IMMER setzen
session_regenerate_id(true);
$_SESSION['auth'] = [
'type' => 'admin',
'id' => (int)$user['id'],
];
// "Angemeldet bleiben" optional
if (!empty($_POST['angemeldet_bleiben'])) {
$identifier = bin2hex(random_bytes(16));
$securitytoken = bin2hex(random_bytes(32)); // stärker als random_string()
$hash = hash('sha256', $securitytoken);
// Ablaufdatum nur setzen, wenn die Spalte existiert - der Code laeuft
// damit vor und nach der Migration. Ablauf per NOW() in SQL, weil
// Webserver und DB-Server in verschiedenen Zeitzonen laufen.
if (securitytokensHatAblaufspalte($pdo)) {
$insert = $pdo->prepare("
INSERT INTO securitytokens (user_id, identifier, securitytoken, user_type, expires_at)
VALUES (:user_id, :identifier, :securitytoken, 'admin', DATE_ADD(NOW(), INTERVAL 30 DAY))
");
} else {
$insert = $pdo->prepare("
INSERT INTO securitytokens (user_id, identifier, securitytoken, user_type)
VALUES (:user_id, :identifier, :securitytoken, 'admin')
");
}
$insert->execute([
'user_id' => (int)$user['id'],
'identifier' => $identifier,
'securitytoken' => $hash
]);
$cookieOpts = [
'expires' => time() + 30*24*3600, // deckungsgleich mit dem Token-Ablauf
'path' => '/',
'secure' => true, // nur wenn HTTPS
'httponly' => true,
'samesite' => 'Lax',
];
setcookie('identifier', $identifier, $cookieOpts);
setcookie('securitytoken', $securitytoken, $cookieOpts);
}
header("Location: index.php");
exit;
} else {
login_versuch_merken($pdo, $email, false);
$error_msg = "E-Mail oder Passwort war ungültig<br><br>";
}
}
}
+18 -9
View File
@@ -1,15 +1,24 @@
<?php
session_start();
session_destroy();
unset($_SESSION['userid']);
//Remove Cookies
setcookie("identifier","",time()-(3600*24*365));
setcookie("securitytoken","",time()-(3600*24*365));
<?php
require_once(__DIR__ . "/../inc/config.inc.php");
require_once(__DIR__ . "/../inc/functions.inc.php");
// Die Sitzung wird von config.inc.php mit den richtigen Cookie-Parametern
// gestartet, deshalb erst danach abraeumen.
$_SESSION = [];
session_destroy();
// Cookies der Funktion "Angemeldet bleiben" entfernen. Die Parameter muessen
// denen beim Setzen entsprechen, sonst bleibt das Cookie stehen.
$cookieOpts = [
'expires' => time() - 3600,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
];
setcookie('identifier', '', $cookieOpts);
setcookie('securitytoken', '', $cookieOpts);
include("templates/header.inc.php");
?>
+1 -2
View File
@@ -1,5 +1,4 @@
<?php
session_start();
require_once(__DIR__ . "/../inc/config.inc.php");
require_once(__DIR__ . "/../inc/functions.inc.php");
include("templates/header.inc.php");
@@ -44,7 +43,7 @@ dein IT Team';
//echo $text;
$ok = SendMailMessage($pdo, $mail, $betreff, $body);
$ok = SendMailMessage($pdo, $empfaenger, $betreff, $text);
if ($ok) {
echo "<b>Ihre Nachricht wurde erfolgreich versendet!</b><br>";
-1
View File
@@ -1,5 +1,4 @@
<?php
session_start();
require_once(__DIR__ . "/../inc/config.inc.php");
require_once(__DIR__ . "/../inc/functions.inc.php");
if(!isset($_GET['userid']) || !isset($_GET['code'])) {
-1
View File
@@ -1,5 +1,4 @@
<?php
session_start();
require_once(__DIR__ . "/../inc/config.inc.php");
require_once(__DIR__ . "/../inc/functions.inc.php");
-1
View File
@@ -1,5 +1,4 @@
<?php
session_start();
// WICHTIG: Pfade aus /admin heraus korrekt auflösen
require_once __DIR__ . "/../inc/config.inc.php";
+19
View File
@@ -0,0 +1,19 @@
-- Brute-Force-Bremse fuer die Anmeldung.
--
-- Haelt fehlgeschlagene und erfolgreiche Anmeldeversuche fest, damit
-- login_gesperrt() in inc/security.inc.php nach mehreren Fehlversuchen
-- zeitweise sperren kann. Ohne diese Tabelle laeuft der Login unveraendert
-- weiter, nur eben ohne Bremse.
--
-- Aufraeumen passiert im Code (Eintraege aelter als 30 Tage).
CREATE TABLE IF NOT EXISTS login_attempts (
id INT NOT NULL AUTO_INCREMENT,
email VARCHAR(255) NOT NULL,
ip_address VARCHAR(45) NOT NULL,
erfolg TINYINT(1) NOT NULL DEFAULT 0,
attempted_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_email_zeit (email, attempted_at),
KEY idx_ip_zeit (ip_address, attempted_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+1 -1
View File
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
+30 -31
View File
@@ -1,5 +1,4 @@
<?php
session_start();
// WICHTIG: Pfade aus /admin heraus korrekt auflösen
require_once __DIR__ . "/../inc/config.inc.php";
@@ -24,7 +23,7 @@ include("templates/footer.inc.php");
<?php
echo '<div style="float: right; width: 200px; ">';
echo "<form action='". $_SERVER['PHP_SELF'] . "' id='formbenutzersuche' method=POST>";
echo "<form action='". self_action() . "' id='formbenutzersuche' method=POST>";
echo '<input type="hidden" name="aktion" value="benutzersuche" />';
echo '<input type="hidden" name="userid_input" id="userid_input" />';
echo '<label>Benutzersuche Impfen:</label>
@@ -55,7 +54,7 @@ if(!check_worker()){
<?php
echo '<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>';
echo '<form action="'. self_action() .'" method=POST>';
echo "Wochentag:<br>";
echo "<select name=date>";
echo "<option value=Mo>montags</option>";
@@ -91,14 +90,14 @@ if(!check_worker()){
echo "<br><br>";
echo '<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>';
echo '<form action="'. self_action() .'" method=POST>';
echo '<input type=submit class="btn btn-primary btn-sm" value="zurück">';
echo "</form>";
}elseif($_POST["aktion"] == "folgenewtermin"){
echo "<h3>Folge von Terminen anlegen</h3><br>";
echo '<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>';
echo '<form action="'. self_action() .'" method=POST>';
echo "Datum: <input name=date type=date class='form-control' required><br>";
echo "Startzeit: <input name=folgestartzeit type=time class='form-control' required><br>";
echo "Länge eines Termins (Min): <input name=folgelaenge type=number class='form-control' required><br>";
@@ -112,7 +111,7 @@ if(!check_worker()){
echo '<input type=submit class="btn btn-primary" value="Neue Zeitslots anlegen"></form>';
echo "<br><br>";
echo '<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>';
echo '<form action="'. self_action() .'" method=POST>';
echo '<input type=submit class="btn btn-primary btn-sm" value="zurück">';
echo "</form>";
@@ -128,7 +127,7 @@ if(!check_worker()){
$sqlstring = "DELETE FROM togo_termin_teilnehmer WHERE terminteilnehmerid = " . (int)$_GET["uid"];
$query = mysqli_query($con,$sqlstring);
if($query){
$sqlstring = "Update togo_termin SET count=count-1 WHERE terminid = ". $_GET["id"] . "";
$sqlstring = "Update togo_termin SET count=count-1 WHERE terminid = " . (int)($_GET["id"] ?? 0);
$query = mysqli_query($con,$sqlstring);
echo "<div class='infofenster'><h4>Eintrag wurde gelöscht!<h4></div>";
@@ -153,14 +152,14 @@ if(!check_worker()){
$togoid = $row["IP"];
$togodate = $row["create_time"];
echo "<tr><td>" . $i . "</td><td>". $togoname ." - " . $togomail . "</td><td>" . $togoid . "</td><td>" . $togodate . "</td><td> <a href=". $_SERVER["PHP_SELF"] ."?a=showtogotermin&b=removeentry&id=$terminid&uid=$terminteilnehmerid class='glyphicon glyphicon-remove' onclick=\"return confirm('Eintrag ". $togoname ."/". $togomail." wirklich löschen?');\" title='löschen'></a> </td></tr>";
echo "<tr><td>" . $i . "</td><td>". $togoname ." - " . $togomail . "</td><td>" . $togoid . "</td><td>" . $togodate . "</td><td> <a href=". self_action() ."?a=showtogotermin&b=removeentry&id=$terminid&uid=$terminteilnehmerid class='glyphicon glyphicon-remove' onclick=\"return confirm('Eintrag ". $togoname ."/". $togomail." wirklich löschen?');\" title='löschen'></a> </td></tr>";
$i++;
}
echo "</table>";
echo "<br><br>";
echo '<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>';
echo '<form action="'. self_action() .'" method=POST>';
echo "<input type=hidden name=aktion value=togoterminverwaltung >";
echo '<input type=submit class="btn btn-primary btn-sm" value="zurück">';
echo "</form>";
@@ -183,7 +182,7 @@ if(!check_worker()){
}
echo "<h3>Termin Anpassung</h3>";
echo '<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>';
echo '<form action="'. self_action() .'" method=POST>';
echo "Datum:<br>";
echo "<input type=date name=date class='form-control' value=$date>";
@@ -208,14 +207,14 @@ if(!check_worker()){
echo "<input type=int name=error class='form-control' value=$error>";
echo "<br><br>";
echo "<input type=hidden name=save value=saveediteinzeltermin >";
echo "<input type=hidden name=terminid value=". $_GET["id"] . " >";
echo "<input type=hidden name=terminid value=" . (int)($_GET["id"] ?? 0) . " >";
echo '<input type=submit class="btn btn-primary btn-sm" value="Termin speichern">';
echo "</form>";
echo "<br><br>";
echo '<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>';
echo '<form action="'. self_action() .'" method=POST>';
//echo "<input type=hidden name=aktion value=togoterminverwaltung >";
echo '<input type=submit class="btn btn-primary btn-sm" value="zurück">';
@@ -243,14 +242,14 @@ if(!check_worker()){
Standort: $standort<br>
<br>";
echo "<form action='". $_SERVER['PHP_SELF'] . "' method=POST>";
echo "<form action='". self_action() . "' method=POST>";
echo "Betreff:<br>";
echo "<input name=betreff type=text class='form-control' ><br>";
echo "<div id=my-editor></div>";
echo "Inhalt:<br>";
echo '<textarea height=200 name="body" id="trumbowyg-demo"></textarea>';
echo "<input name=save type=hidden value=savesendmail>";
echo "<input name=id type=hidden value='". $_GET["id"] . "'><br><br>";
echo "<input name=id type=hidden value='" . (int)($_GET["id"] ?? 0) . "'><br><br>";
echo "<input type=submit value='Speichern'><br>";
echo '</form>';
}else if($_GET["a"] == "togomaildeletetimes" ){
@@ -261,7 +260,7 @@ if(!check_worker()){
echo "Hiermit werden alle Benutzer mit abgesagten Terminen informiert und die Einträge dann gelöscht.<br>";
echo "<form action='". $_SERVER['PHP_SELF'] . "' method=POST>";
echo "<form action='". self_action() . "' method=POST>";
echo "Betreff:<br>";
echo "<input name=betreff type=text class='form-control' ><br>";
echo "<div id=my-editor></div>";
@@ -288,7 +287,7 @@ if(!check_worker()){
}
echo "<h3>Serientermin Anpassung</h3>";
echo '<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>';
echo '<form action="'. self_action() .'" method=POST>';
echo "<input type=hidden name=aktion value=togoeditserientermin class='form-control'>";
echo "Wochentag:<br>";
@@ -358,7 +357,7 @@ if(!check_worker()){
echo "<br><br>";
echo '<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>';
echo '<form action="'. self_action() .'" method=POST>';
echo "<input type=hidden name=aktion value=togoterminverwaltung >";
echo '<input type=submit class="btn btn-primary btn-sm" value="zurück">';
echo "</form>";
@@ -370,7 +369,7 @@ if(!check_worker()){
}elseif($_GET["a"] == "togoAddUser" || $_POST["aktion"] == "togoAddUser" ){
$textausgabe .= "<h3>ToGo Anmeldung</h3>";
$textausgabe .= '<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>';
$textausgabe .= '<form action="'. self_action() .'" method=POST>';
$textausgabe .= "Melden Sie sich unverbindlich an.<br>Es wird Ihre IP-Addresse gespeichert.<br><br><h3>Dieses dient nur zur Planung und ist keine feste Terminzusage!</h3>";
$textausgabe .= "Bitte beachten Sie, dass die angezeigte Anzahl der Online Anmeldungen vom tatsächlichen Andrang vor Ort abweichen kann.<br>Auch ist eine Verschiebung der Impfung für einzelne Impflinge immer möglich!<br><br>";
$textausgabe .= "";
@@ -384,7 +383,7 @@ if(!check_worker()){
$textausgabe .= "<input type=hidden name=save value=saveadduser >";
//$textausgabe .= "<input type=hidden name=aktion value=togoAddUser >";
$textausgabe .= "<input type=hidden name=ip value='PraxisTeam Eintrag' >";
$textausgabe .= "<input type=hidden name=terminid value='".$_GET["id"] ."' >";
$textausgabe .= "<input type=hidden name=terminid value='" . (int)($_GET["id"] ?? 0) . "' >";
$textausgabe .= '<input type=submit class="btn btn-primary btn-sm" value="Unverbindlich anmelden"><br>';
$textausgabe .= "</form>";
$textausgabe .= "<br><br>Bitte beachten Sie, dass die angezeigte Anzahl der Online Anmeldungen vom tatsächlichen Andrang vor Ort abweichen kann.<br>Auch ist eine Verschiebung der Impfung für einzelne Impflinge immer möglich!<br><br>";
@@ -399,7 +398,7 @@ if(!check_worker()){
echo '<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>';
echo '<form action="'. self_action() .'" method=POST>';
echo "<input type=date name=date class='form-control'>";
echo "<br><br>";
@@ -423,7 +422,7 @@ if(!check_worker()){
echo "<br><br>";
echo '<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>';
echo '<form action="'. self_action() .'" method=POST>';
echo '<input type=submit class="btn btn-primary btn-sm" value="zurück">';
echo "</form>";
@@ -453,7 +452,7 @@ if(!check_worker()){
if($_GET["a"] == "activetogotermin" ){
$sqlstring = "Update togo_termin SET count='0' WHERE terminid = ". $_GET["id"] . "";
$sqlstring = "Update togo_termin SET count='0' WHERE terminid = " . (int)($_GET["id"] ?? 0);
$query = mysqli_query($con,$sqlstring);
if($query){
echo "<div class='infofenster'><h4>Termin wurde aktiviert!<h4></div>";
@@ -465,7 +464,7 @@ if(!check_worker()){
}
if($_GET["a"] == "removetogotermin" ){
$sqlstring = "Update togo_termin SET count='-1' WHERE terminid = ". $_GET["id"] . "";
$sqlstring = "Update togo_termin SET count='-1' WHERE terminid = " . (int)($_GET["id"] ?? 0);
$query = mysqli_query($con,$sqlstring);
if($query){
@@ -815,11 +814,11 @@ $queryinsert = $stmt->execute([
$togostandort = $row["standort"];
$togowarning = $row["warning"];
$togoerror = $row["error"];
echo "<tr><td>" . $ausgabedate . "</td><td>" . date("H:i", strtotime($togostart)) . " - " . date("H:i", strtotime($togoende)) . "</td><td>" . $togostandort . " - Gelb:" . $togowarning . " Rot:" . $togoerror . "</td><td> <a href=". $_SERVER["PHP_SELF"] ."?a=removeserie&id=$togovorgabenid class='glyphicon glyphicon-remove' title='löschen' onclick=\"return confirm('Eintrag " . $ausgabedate . " " . date("H:i", strtotime($togostart)) . " - " . date("H:i", strtotime($togoende)) . " wirklich löschen?');\" ></a></td><td>";
echo "<tr><td>" . $ausgabedate . "</td><td>" . date("H:i", strtotime($togostart)) . " - " . date("H:i", strtotime($togoende)) . "</td><td>" . $togostandort . " - Gelb:" . $togowarning . " Rot:" . $togoerror . "</td><td> <a href=". self_action() ."?a=removeserie&id=$togovorgabenid class='glyphicon glyphicon-remove' title='löschen' onclick=\"return confirm('Eintrag " . $ausgabedate . " " . date("H:i", strtotime($togostart)) . " - " . date("H:i", strtotime($togoende)) . " wirklich löschen?');\" ></a></td><td>";
}
echo "</table><br>";
echo '<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>';
echo '<form action="'. self_action() .'" method=POST>';
echo "<input type=hidden name=aktion value=togoterminverwaltungnewserie >";
echo '<input type=submit class="btn btn-primary btn-sm" value="neuer Serientermin">';
echo "</form>";
@@ -842,18 +841,18 @@ $queryinsert = $stmt->execute([
$togoerror = $row["error"];
if($togocount >= 0){
echo "<tr><td>" . date("d.m.y", strtotime($togodate)) . "</td><td>" . date("H:i", strtotime($togostart)) . " - " . date("H:i", strtotime($togoende)) . "</td><td>" . $togostandort . "</td><td>Meldung:" . $togocount . " - Gelb:" . $togowarning . " Rot:" . $togoerror . "</td><td><a href=". $_SERVER["PHP_SELF"] ."?a=togoAddUser&id=$togoterminid class='glyphicon glyphicon-plus' title='Patient hinzufügen'></a> <a href=". $_SERVER["PHP_SELF"] ."?a=showtogotermin&id=$togoterminid class='glyphicon glyphicon-eye-open' title='einsehen'></a> <a href=". $_SERVER["PHP_SELF"] ."?a=togoedittermin&id=$togoterminid class='glyphicon glyphicon-pencil' title='anpassen'></a> <a href=". $_SERVER["PHP_SELF"] ."?a=togosendmail&id=$togoterminid class='glyphicon glyphicon-envelope' title='Mail an alle Teilnehmer'></a> <a href=". $_SERVER["PHP_SELF"] ."?a=removetogotermin&id=$togoterminid class='glyphicon glyphicon-remove' title='deaktiveren'></a> </td></tr>";
echo "<tr><td>" . date("d.m.y", strtotime($togodate)) . "</td><td>" . date("H:i", strtotime($togostart)) . " - " . date("H:i", strtotime($togoende)) . "</td><td>" . $togostandort . "</td><td>Meldung:" . $togocount . " - Gelb:" . $togowarning . " Rot:" . $togoerror . "</td><td><a href=". self_action() ."?a=togoAddUser&id=$togoterminid class='glyphicon glyphicon-plus' title='Patient hinzufügen'></a> <a href=". self_action() ."?a=showtogotermin&id=$togoterminid class='glyphicon glyphicon-eye-open' title='einsehen'></a> <a href=". self_action() ."?a=togoedittermin&id=$togoterminid class='glyphicon glyphicon-pencil' title='anpassen'></a> <a href=". self_action() ."?a=togosendmail&id=$togoterminid class='glyphicon glyphicon-envelope' title='Mail an alle Teilnehmer'></a> <a href=". self_action() ."?a=removetogotermin&id=$togoterminid class='glyphicon glyphicon-remove' title='deaktiveren'></a> </td></tr>";
}else{
echo "<tr><td>" . date("d.m.y", strtotime($togodate)) . "</td><td>" . date("H:i", strtotime($togostart)) . " - " . date("H:i", strtotime($togoende)) . "</td><td>" . $togostandort . "</td><td>deaktivert - Gelb:" . $togowarning . " Rot:" . $togoerror . "</td><td> <a href=". $_SERVER["PHP_SELF"] ."?a=activetogotermin&id=$togoterminid class='glyphicon glyphicon-ok' title='aktivieren'></a> <a href=". $_SERVER["PHP_SELF"] ."?a=showtogotermin&id=$togoterminid class='glyphicon glyphicon-eye-open' title='einsehen'></a> <a href=". $_SERVER["PHP_SELF"] ."?a=togosendmail&id=$togoterminid class='glyphicon glyphicon-envelope' title='Mail an alle Teilnehmer'></a> <a href=". $_SERVER["PHP_SELF"] ."?a=deletetogotermin&id=$togoterminid class='glyphicon glyphicon-remove' title='Dauerhaft löschen' onclick=\"return confirm('Eintrag " . $togodate . " " . date("H:i", strtotime($togostart)) . " - " . date("H:i", strtotime($togoende)) . " wirklich löschen? Serientermine dürfen nicht gelöscht werden, da diese sonst wieder erstellt werden.');\"></a> </td></tr>";
echo "<tr><td>" . date("d.m.y", strtotime($togodate)) . "</td><td>" . date("H:i", strtotime($togostart)) . " - " . date("H:i", strtotime($togoende)) . "</td><td>" . $togostandort . "</td><td>deaktivert - Gelb:" . $togowarning . " Rot:" . $togoerror . "</td><td> <a href=". self_action() ."?a=activetogotermin&id=$togoterminid class='glyphicon glyphicon-ok' title='aktivieren'></a> <a href=". self_action() ."?a=showtogotermin&id=$togoterminid class='glyphicon glyphicon-eye-open' title='einsehen'></a> <a href=". self_action() ."?a=togosendmail&id=$togoterminid class='glyphicon glyphicon-envelope' title='Mail an alle Teilnehmer'></a> <a href=". self_action() ."?a=deletetogotermin&id=$togoterminid class='glyphicon glyphicon-remove' title='Dauerhaft löschen' onclick=\"return confirm('Eintrag " . $togodate . " " . date("H:i", strtotime($togostart)) . " - " . date("H:i", strtotime($togoende)) . " wirklich löschen? Serientermine dürfen nicht gelöscht werden, da diese sonst wieder erstellt werden.');\"></a> </td></tr>";
}
}
echo "</table><br>";
echo '<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>';
echo '<form action="'. self_action() .'" method=POST>';
echo "<input type=hidden name=aktion value=togoterminverwaltungnewtogo >";
echo '<input type=submit class="btn btn-primary btn-sm" value="neuer Einzeltermin">';
echo "</form>";
echo "<br>";
echo '<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>';
echo '<form action="'. self_action() .'" method=POST>';
echo "<input type=hidden name=aktion value=folgenewtermin >";
echo '<input type=submit class="btn btn-primary btn-sm" value="neue Terminfolge">';
echo "</form>";
@@ -907,10 +906,10 @@ $queryinsert = $stmt->execute([
if( $anzahlzeilen != 0){
echo "<h4>Abgesagte Termine</h4>";
echo "<a href=". $_SERVER["PHP_SELF"] ."?a=togomaildeletetimes title='Mail an alle Teilnehmer' class='btn btn-primary btn-sm'>Mail an alle Teilnehmer bei abgesagten Terminen</a> ";
echo "<a href=". self_action() ."?a=togomaildeletetimes title='Mail an alle Teilnehmer' class='btn btn-primary btn-sm'>Mail an alle Teilnehmer bei abgesagten Terminen</a> ";
}
echo "<br><br>";
echo '<form action="'. $_SERVER["PHP_SELF"] .'" method=POST>';
echo '<form action="'. self_action() .'" method=POST>';
echo "<input type=hidden name=aktion value=togoterminverwaltung >";
echo '<input type=submit class="btn btn-primary btn-sm" value="zurück">';
echo "</form>";
+4 -5
View File
@@ -1,5 +1,4 @@
<?php
session_start();
// WICHTIG: Pfade aus /admin heraus korrekt auflösen
require_once __DIR__ . "/../inc/config.inc.php";
@@ -24,7 +23,7 @@ include("templates/footer.inc.php");
<?php
echo '<div style="float: right; width: 200px; ">';
echo "<form action='" . htmlspecialchars($_SERVER['PHP_SELF'], ENT_QUOTES, 'UTF-8') . "' id='formbenutzersuche' method='POST'>";
echo "<form action='" . self_action() . "' id='formbenutzersuche' method='POST'>";
echo '<input type="hidden" name="aktion" value="benutzersuche" />';
echo '<input type="hidden" name="userid_input" id="userid_input" />';
echo '<label>Benutzersuche Impfen:</label>
@@ -111,7 +110,7 @@ if (!check_worker()) {
echo "<h4>Vorlage: " . htmlspecialchars($webseitentitel, ENT_QUOTES, 'UTF-8') . "</h4>";
echo "<br><br>";
echo "<form action='" . htmlspecialchars($_SERVER['PHP_SELF'], ENT_QUOTES, 'UTF-8') . "' method='POST'>";
echo "<form action='" . self_action() . "' method='POST'>";
echo "<label>Titel</label><br>";
echo "<input name='webseitentitel' type='text' class='form-control' value='" . htmlspecialchars($webseitentitel, ENT_QUOTES, 'UTF-8') . "'><br><br>";
echo "<label>Beschreibung</label><br>";
@@ -128,7 +127,7 @@ if (!check_worker()) {
echo "</form>";
echo "<br><br>";
echo "<form action='" . htmlspecialchars($_SERVER['PHP_SELF'], ENT_QUOTES, 'UTF-8') . "' method='POST'>";
echo "<form action='" . self_action() . "' method='POST'>";
echo "<input type='submit' class='btn btn-primary btn-sm' value='zurück'>";
echo "</form>";
}
@@ -144,7 +143,7 @@ if (!check_worker()) {
// =========================
} else {
echo "<form action='" . htmlspecialchars($_SERVER['PHP_SELF'], ENT_QUOTES, 'UTF-8') . "' method='POST'>";
echo "<form action='" . self_action() . "' method='POST'>";
echo "Wählen Sie die zu bearbeitenden Webseiteninhalt aus:<br><br>";
try {
-1
View File
@@ -1,5 +1,4 @@
<?php
session_start();
require_once __DIR__ . "/../inc/config.inc.php";
require_once __DIR__ . "/../inc/functions.inc.php";
+1 -1
View File
@@ -1,2 +1,2 @@
<?php
<?php
require_once __DIR__ . '/impfbestaetigung.php';
+1 -1
View File
@@ -1,2 +1,2 @@
<?php
<?php
require_once __DIR__ . '/impfwarteliste.php';
+1 -1
View File
@@ -1,2 +1,2 @@
<?php
<?php
require_once __DIR__ . '/impfwarteliste.php';
+1 -1
View File
@@ -1,4 +1,4 @@
<!DOCTYPE HTML>
<!DOCTYPE HTML>
<!--
Alpha by HTML5 UP
html5up.net | @n33co
+1 -1
View File
@@ -1,2 +1,2 @@
<?php
<?php
require_once __DIR__ . '/impfwarteliste.php';
+26 -1
View File
@@ -12,6 +12,8 @@ if (!is_readable($credentials)) {
}
require $credentials; // liefert $dbhost, $dbuser, $dbpassword, $dbname
require_once __DIR__ . '/security.inc.php';
$organisationsname = "Praxis Creutzburg";
$organisationsurl = "https://www.praxis-creutzburg.de/";
@@ -79,4 +81,27 @@ ini_set('session.use_strict_mode', '1');
ini_set('session.cookie_secure', '1'); // bei HTTPS
ini_set('session.cookie_httponly', '1');
session_start();
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
// CSRF-Schutz fuer die angemeldeten Bereiche: Token in jedes POST-Formular
// einsetzen und eingehende POSTs pruefen. Muss nach session_start() und vor
// jeder Ausgabe der Seite laufen. Der oeffentliche Bereich bleibt aussen vor,
// dort gibt es keine Sitzung, die missbraucht werden koennte.
$geschuetzteBereiche = [
realpath(__DIR__ . '/../admin'),
realpath(__DIR__ . '/../intern'),
];
$laufendesSkript = realpath($_SERVER['SCRIPT_FILENAME'] ?? '');
foreach ($geschuetzteBereiche as $verzeichnis) {
if ($verzeichnis === false || $laufendesSkript === false) {
continue;
}
if (strncmp($laufendesSkript, $verzeichnis . DIRECTORY_SEPARATOR, strlen($verzeichnis) + 1) === 0) {
csrf_autoinject();
csrf_require();
break;
}
}
+267
View File
@@ -611,6 +611,273 @@ function GetOrdnungsid($wert){
}
/**
* Herkunft einer Anfrage als Text.
*
* Nur der Fall "telefon" kommt aus anfragen.source. Die Unterscheidung
* intern/Mail bleibt bewusst an sicherenachricht haengen: source="portal"
* ist dafuer unbrauchbar, weil insertAnfrage() kein source setzt und interne
* Anfragen deshalb als source="mail" in der Tabelle stehen.
* Seit 02.09.2026 legt die Telefonanlage Anfragen mit source="telefon" an;
* diese haben oft keine requester_person_id, weil der Anrufer nicht
* zugeordnet werden konnte.
*/
function GetAnfrageHerkunft($source, $sicherenachricht){
if($source == "telefon"){
$text = "Telefonanfrage";
}else if($sicherenachricht == "1"){
$text = "Interne Anfrage";
}else{
$text = "Mailanfrage";
}
return $text;
}
/**
* Hinweis auf die Sprachnachricht einer Telefonanfrage.
*
* Die Telefonanlage legt in anfragen.nachricht nur das automatisch erzeugte
* Transkript ab, das Erkennungsfehler enthalten kann. Die Aufnahme selbst
* liegt in PraxisKI und ist nur aus dem Praxisnetz erreichbar.
* Fuer alle anderen Herkuenfte leer.
*/
function GetSprachnachrichtHinweis($source){
if($source != "telefon"){
return "";
}
return "Transkript der Sprachnachricht - Aufnahme anhören: "
. "<a href=\"https://ki-praxis.praxis.local/anfragen/\" target=\"_blank\" rel=\"noopener\">ki-praxis.praxis.local/anfragen</a>"
. " (nur im Praxisnetz)<br>";
}
/**
* Telefonnummer als anklickbaren tel:-Link ausgeben.
*
* Der Link vertraegt nur Ziffern und ein fuehrendes Plus; Klammern,
* Leerzeichen und Bindestriche fliegen fuer das Ziel raus, angezeigt wird
* die Nummer unveraendert. Nummern ohne jede Ziffer - etwa "unterdrueckt"
* bei unterdrueckter Rufnummer - werden nur als Text ausgegeben.
* Erwartet den Rohwert aus der Datenbank und maskiert selbst.
*/
function GetTelefonLink($nummer){
$nummer = trim((string)$nummer);
if($nummer === ""){
return "";
}
$ziel = preg_replace('/[^0-9]/', '', $nummer);
if($ziel === ""){
return htmlspecialchars($nummer, ENT_QUOTES, 'UTF-8');
}
if(substr($nummer, 0, 1) === "+"){
$ziel = "+" . $ziel;
}
return "<a href=\"tel:" . htmlspecialchars($ziel, ENT_QUOTES, 'UTF-8') . "\">"
. htmlspecialchars($nummer, ENT_QUOTES, 'UTF-8') . "</a>";
}
/**
* Rufnummer des Anrufers als beschriftete Zeile.
*
* anfragen.anrufernummer fuellt die Telefonanlage; steht dort nichts, gibt
* es keine Zeile. Fuer alle Herkuenfte ausser "telefon" leer.
*/
function GetAnruferZeile($source, $anrufernummer){
if($source != "telefon"){
return "";
}
$link = GetTelefonLink($anrufernummer);
if($link === ""){
return "";
}
return "Anruf von: " . $link . "<br>";
}
/**
* Rufnummer auf eine vergleichbare Form bringen.
*
* Nur Ziffern; +49 und 0049 werden zur nationalen Schreibweise mit
* fuehrender 0, damit die Anzeige der Telefonanlage und die von Hand
* gepflegten Nummern in persons zusammenfinden.
*/
function NormalisiereRufnummer($nummer){
$ziffern = preg_replace('/[^0-9]/', '', (string)$nummer);
if($ziffern === ""){
return "";
}
if(strpos($ziffern, "0049") === 0){
$ziffern = "0" . substr($ziffern, 4);
}elseif(strpos(trim((string)$nummer), "+") === 0 && strpos($ziffern, "49") === 0){
$ziffern = "0" . substr($ziffern, 2);
}
return $ziffern;
}
/**
* Patienten zu einer Rufnummer suchen.
*
* Gesucht wird in persons UND im Altbestand user_old. persons allein reicht
* nicht: bei 122 der 2552 migrierten Paare weicht die Nummer ab, 87 Nummern
* stehen ausschliesslich in user_old, und 176 alte Zeilen wurden nie
* gemappt. Genau so ein Fall war Anfrage 13037 - die Mobilnummer stand nur
* im Altbestand, in persons nur eine aeltere siebenstellige.
*
* Gibt es zu einer alten Zeile einen aktuellen Datensatz, werden dessen
* Daten gezeigt: die Nummer ist nur der Schluessel, angezeigt gehoert der
* gepflegte Stand. Jeder Patient taucht je Nummer hoechstens einmal auf.
*
* Liefert bewusst alle Treffer, denn 521 der gepflegten Nummern gehoeren zu
* mehr als einer Person (Familien, zwei sogar zu 16 bzw. 18). Die Auswahl
* trifft die Mitarbeiterin, nicht der Code.
*
* Nummern mit weniger als 7 Ziffern werden nicht gesucht: in den Stammdaten
* stehen ein paar Platzhalter mit 1 bis 5 Ziffern, die sonst wahllos
* zusammenfallen.
*
* Der Index wird beim ersten Treffer einmal je Seitenaufruf aufgebaut und
* danach wiederverwendet - die Anfrageliste zeigt bis zu einige tausend
* Zeilen, eine Abfrage je Zeile waere zu teuer.
*/
function FindePatientenZuRufnummer($pdo, $nummer){
static $index = null;
$gesucht = NormalisiereRufnummer($nummer);
if(strlen($gesucht) < 7){
return array();
}
if($index === null){
$index = array();
$gesehen = array();
$aufnehmen = function($nummerRoh, $schluessel, $eintrag) use (&$index, &$gesehen){
$n = NormalisiereRufnummer($nummerRoh);
if(strlen($n) < 7){
return;
}
// Zweiter Schluessel ueber Name und Geburtstag: nicht gemappte
// Altzeilen haben eine eigene userid, sind aber oft dieselbe
// Person wie ein persons-Datensatz. Ohne das stuende sie zweimal
// in der Liste. Der Vergleich traegt hier, weil ohnehin nur
// Eintraege zu ein und derselben Rufnummer verglichen werden.
$identitaet = "id:" . mb_strtolower(trim((string)$eintrag["vorname"]) . "|"
. trim((string)$eintrag["nachname"]) . "|" . (string)$eintrag["geburtstag"]);
if(isset($gesehen[$n][$schluessel]) || isset($gesehen[$n][$identitaet])){
return;
}
$gesehen[$n][$schluessel] = true;
$gesehen[$n][$identitaet] = true;
$index[$n][] = $eintrag;
};
$stmt = $pdo->query("SELECT person_id, vorname, nachname, geburtstag, email, tele
FROM persons WHERE tele IS NOT NULL AND tele <> ''");
foreach($stmt as $p){
$aufnehmen($p["tele"], "p" . $p["person_id"], array(
"person_id" => $p["person_id"],
"vorname" => $p["vorname"],
"nachname" => $p["nachname"],
"geburtstag" => $p["geburtstag"],
"email" => $p["email"],
"altbestand" => false,
));
}
$stmt = $pdo->query("SELECT u.userid, u.tele,
p.person_id AS person_id,
COALESCE(p.vorname, u.vorname) AS vorname,
COALESCE(p.nachname, u.nachname) AS nachname,
COALESCE(p.geburtstag, u.geburtstag) AS geburtstag,
COALESCE(p.email, u.mail) AS email
FROM user_old u
LEFT JOIN tmp_user_person_map m ON m.old_userid = u.userid
LEFT JOIN persons p ON p.person_id = m.person_id
WHERE u.tele IS NOT NULL AND u.tele <> ''");
foreach($stmt as $u){
$pid = $u["person_id"] !== null ? (int)$u["person_id"] : null;
$aufnehmen($u["tele"], $pid !== null ? "p" . $pid : "u" . $u["userid"], array(
"person_id" => $pid,
"vorname" => $u["vorname"],
"nachname" => $u["nachname"],
"geburtstag" => $u["geburtstag"],
"email" => $u["email"],
"altbestand" => true,
));
}
}
return isset($index[$gesucht]) ? $index[$gesucht] : array();
}
/**
* Zu einer Telefonanfrage ohne zugeordneten Patienten anzeigen, wer hinter
* der Rufnummer stecken koennte.
*
* Bewusst nur ein Vorschlag: die Rufnummer beweist nicht, wer angerufen hat.
* Bei mehreren Treffern werden die Namen aufgezaehlt, statt einen zu waehlen.
* Leer, sobald die Anfrage bereits einen Patienten hat oder nichts passt.
*/
function GetPatientZuRufnummerAusgabe($pdo, $source, $anrufernummer, $personId = null){
if($source != "telefon"){
return "";
}
if(!empty($personId)){
return "";
}
$treffer = FindePatientenZuRufnummer($pdo, $anrufernummer);
if(!$treffer){
return "";
}
$namen = array();
$ausAltbestand = false;
foreach($treffer as $p){
$name = trim(((string)$p["vorname"]) . " " . ((string)$p["nachname"]));
$namen[] = htmlspecialchars($name, ENT_QUOTES, 'UTF-8');
if(!empty($p["altbestand"])){
$ausAltbestand = true;
}
}
if(count($treffer) === 1){
$p = $treffer[0];
$text = "Rufnummer passt zu: <b>" . $namen[0] . "</b>";
$geburtstag = (string)($p["geburtstag"] ?? "");
if($geburtstag !== "" && $geburtstag !== "0000-00-00"){
$zeit = strtotime($geburtstag);
if($zeit !== false){
$geburtstag = date("d.m.Y", $zeit);
}
$text .= ", geb. " . htmlspecialchars($geburtstag, ENT_QUOTES, 'UTF-8');
}
$mail = trim((string)($p["email"] ?? ""));
if($mail !== ""){
$text .= "<br>" . htmlspecialchars($mail, ENT_QUOTES, 'UTF-8');
}
if(!empty($p["altbestand"])){
$text .= $p["person_id"] === null
? "<br><i>nur im alten Bestand, kein aktueller Patientendatensatz</i>"
: "<br><i>Rufnummer stammt aus dem alten Bestand</i>";
}
return $text . "<br><i>nicht bestätigt - nur über die Rufnummer erkannt</i><br>";
}
$gezeigt = array_slice($namen, 0, 6);
$text = "Rufnummer passt zu " . count($treffer) . " Patienten: " . implode(", ", $gezeigt);
if(count($namen) > count($gezeigt)){
$text .= " und " . (count($namen) - count($gezeigt)) . " weiteren";
}
if($ausAltbestand){
$text .= "<br><i>teilweise über den alten Bestand gefunden</i>";
}
return $text . "<br><i>nicht zugeordnet - bitte selbst prüfen</i><br>";
}
function GetPatientenartName($Patientenart){
if($Patientenart == "0"){
$impfenangebot = "Neupatient";
+258
View File
@@ -0,0 +1,258 @@
<?php
/**
* Sicherheitsfunktionen: CSRF-Schutz, Brute-Force-Bremse, sichere Formularziele.
*
* Wird von inc/config.inc.php eingebunden und steht damit ueberall zur
* Verfuegung, wo auch $pdo verfuegbar ist.
*/
// ---------------------------------------------------------------------------
// CSRF
// ---------------------------------------------------------------------------
/**
* Liefert das CSRF-Token der aktuellen Sitzung und legt es beim ersten
* Aufruf an.
*/
function csrf_token(): string
{
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
/**
* Fertiges Hidden-Feld fuer Formulare.
*/
function csrf_field(): string
{
return '<input type="hidden" name="csrf_token" value="'
. htmlspecialchars(csrf_token(), ENT_QUOTES, 'UTF-8') . '">';
}
/**
* Prueft das mitgeschickte Token. Akzeptiert das Formularfeld und den
* Header X-CSRF-Token, damit auch AJAX-Aufrufe funktionieren.
*/
function csrf_valid(): bool
{
if (empty($_SESSION['csrf_token'])) {
return false;
}
$token = $_POST['csrf_token'] ?? ($_SERVER['HTTP_X_CSRF_TOKEN'] ?? '');
if (!is_string($token) || $token === '') {
return false;
}
return hash_equals($_SESSION['csrf_token'], $token);
}
/**
* Bricht jeden POST ohne gueltiges Token ab. Fuer GET-Anfragen wirkungslos.
*/
function csrf_require(): void
{
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
return;
}
if (csrf_valid()) {
return;
}
http_response_code(403);
$istAjax = ($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '') === 'XMLHttpRequest';
if ($istAjax) {
header('Content-Type: application/json');
echo json_encode(['error' => 'Sicherheitstoken ungueltig. Bitte neu anmelden.']);
exit;
}
header('Content-Type: text/html; charset=utf-8');
echo '<h3>Die Anfrage wurde abgelehnt.</h3>';
echo 'Das Sicherheitstoken war ungültig oder die Sitzung ist abgelaufen.<br>';
echo 'Bitte die Seite neu laden und den Vorgang wiederholen.<br><br>';
echo '<a href="index.php">Zurück zur Übersicht</a>';
exit;
}
/**
* Haengt das Token an jedes POST-Formular der Ausgabe. Wird als Callback von
* ob_start() benutzt, damit die rund 60 bestehenden Formulare nicht einzeln
* angefasst werden muessen.
*/
function csrf_inject_output(string $html): string
{
$ersetzt = preg_replace_callback(
'/<form\b[^>]*>/i',
static function (array $treffer): string {
$tag = $treffer[0];
// Nur POST-Formulare brauchen ein Token.
if (!preg_match('/method\s*=\s*["\']?\s*post/i', $tag)) {
return $tag;
}
return $tag . csrf_field();
},
$html
);
// preg_replace_callback liefert bei einem Fehler null - dann lieber die
// unveraenderte Seite ausliefern als eine leere.
return $ersetzt ?? $html;
}
/**
* Aktiviert das automatische Einfuegen. Muss vor jeder Ausgabe laufen.
*/
function csrf_autoinject(): void
{
ob_start('csrf_inject_output');
}
/**
* Sicheres Ziel fuer Formulare und Links auf die eigene Seite.
*
* Ersetzt $_SERVER['PHP_SELF']: das enthaelt bei Aufrufen wie
* /admin/anfragen.php/"><script> auch den angehaengten Pfad und landete
* ungeprueft im HTML.
*/
function self_action(): string
{
return htmlspecialchars(basename($_SERVER['SCRIPT_NAME'] ?? ''), ENT_QUOTES, 'UTF-8');
}
// ---------------------------------------------------------------------------
// Brute-Force-Bremse fuer Anmeldungen
// ---------------------------------------------------------------------------
const LOGIN_MAX_VERSUCHE_KONTO = 5; // pro E-Mail-Adresse
const LOGIN_MAX_VERSUCHE_IP = 20; // pro IP-Adresse
const LOGIN_ZEITFENSTER_MIN = 15; // Minuten
/**
* Existiert die Tabelle login_attempts? Ohne sie laeuft der Login wie bisher
* weiter - gleiche Vorgehensweise wie bei securitytokensHatAblaufspalte().
*/
function loginAttemptsTabelleVorhanden(PDO $pdo): bool
{
static $vorhanden = null;
if ($vorhanden !== null) {
return $vorhanden;
}
try {
$st = $pdo->query("SHOW TABLES LIKE 'login_attempts'");
$vorhanden = (bool)$st->fetchColumn();
} catch (Throwable $e) {
$vorhanden = false;
}
return $vorhanden;
}
function login_ip(): string
{
return substr((string)($_SERVER['REMOTE_ADDR'] ?? ''), 0, 45);
}
/**
* Ist die Anmeldung gerade gesperrt? Liefert die Restdauer in Sekunden,
* sonst 0.
*/
function login_gesperrt(PDO $pdo, string $email): int
{
if (!loginAttemptsTabelleVorhanden($pdo)) {
return 0;
}
$st = $pdo->prepare("
SELECT
SUM(email = :email) AS konto,
SUM(ip_address = :ip) AS ip,
MAX(attempted_at) AS letzter
FROM login_attempts
WHERE erfolg = 0
AND attempted_at > DATE_SUB(NOW(), INTERVAL :fenster MINUTE)
AND (email = :email2 OR ip_address = :ip2)
");
$st->bindValue(':email', $email);
$st->bindValue(':email2', $email);
$st->bindValue(':ip', login_ip());
$st->bindValue(':ip2', login_ip());
$st->bindValue(':fenster', LOGIN_ZEITFENSTER_MIN, PDO::PARAM_INT);
$st->execute();
$row = $st->fetch(PDO::FETCH_ASSOC);
if (!$row || $row['letzter'] === null) {
return 0;
}
$ueberschritten = (int)$row['konto'] >= LOGIN_MAX_VERSUCHE_KONTO
|| (int)$row['ip'] >= LOGIN_MAX_VERSUCHE_IP;
if (!$ueberschritten) {
return 0;
}
// Sperre laeuft aus, sobald der letzte Fehlversuch aus dem Zeitfenster
// faellt. Zeitrechnung in SQL, weil Web- und DB-Server in
// unterschiedlichen Zeitzonen laufen.
$st = $pdo->prepare("
SELECT GREATEST(0, TIMESTAMPDIFF(SECOND, NOW(),
DATE_ADD(:letzter, INTERVAL :fenster MINUTE)))
");
$st->bindValue(':letzter', $row['letzter']);
$st->bindValue(':fenster', LOGIN_ZEITFENSTER_MIN, PDO::PARAM_INT);
$st->execute();
return (int)$st->fetchColumn();
}
/**
* Haelt einen Anmeldeversuch fest.
*/
function login_versuch_merken(PDO $pdo, string $email, bool $erfolg): void
{
if (!loginAttemptsTabelleVorhanden($pdo)) {
return;
}
try {
$st = $pdo->prepare("
INSERT INTO login_attempts (email, ip_address, erfolg)
VALUES (:email, :ip, :erfolg)
");
$st->execute([
':email' => substr($email, 0, 255),
':ip' => login_ip(),
':erfolg' => $erfolg ? 1 : 0,
]);
if ($erfolg) {
// Nach erfolgreicher Anmeldung ist die Bremse fuer dieses Konto
// wieder offen.
$del = $pdo->prepare("DELETE FROM login_attempts WHERE email = :email AND erfolg = 0");
$del->execute([':email' => $email]);
}
// Alte Eintraege gelegentlich aufraeumen.
if (random_int(1, 50) === 1) {
$pdo->exec("DELETE FROM login_attempts WHERE attempted_at < DATE_SUB(NOW(), INTERVAL 30 DAY)");
}
} catch (Throwable $e) {
error_log('login_versuch_merken: ' . $e->getMessage());
}
}
/**
* Einheitlicher Text fuer eine aktive Sperre.
*/
function login_sperr_text(int $sekunden): string
{
$minuten = (int)ceil($sekunden / 60);
return 'Zu viele fehlgeschlagene Anmeldeversuche. '
. 'Bitte in ' . $minuten . ' Minute' . ($minuten === 1 ? '' : 'n') . ' erneut versuchen.<br><br>';
}
+1 -1
View File
@@ -1,4 +1,4 @@
<?php
<?php
/**
* @package Patientsuche
* @copyright Copyright (C) 2021 ctb-it.de Inc. All rights reserved.
-1
View File
@@ -1,5 +1,4 @@
<?php
session_start();
require_once(__DIR__ . "/../inc/config.inc.php");
require_once(__DIR__ . "/../inc/functions.inc.php");
-1
View File
@@ -1,5 +1,4 @@
<?php
session_start();
require_once(__DIR__ . "/../inc/config.inc.php");
require_once(__DIR__ . "/../inc/functions.inc.php");
include("templates/header.inc.php");
-1
View File
@@ -2,7 +2,6 @@
// Ausgabe puffern: check_intern_user() setzt Cookies und leitet per header() um,
// obwohl das Header-Template weiter unten schon Ausgabe erzeugt hat.
ob_start();
session_start();
require_once(__DIR__ . "/../inc/config.inc.php");
require_once(__DIR__ . "/../inc/functions.inc.php");
+5 -3
View File
@@ -7,10 +7,12 @@
ob_start();
require_once(__DIR__ . "/../inc/config.inc.php");
require_once(__DIR__ . "/../inc/functions.inc.php");
session_start();
ini_set('session.gc_maxlifetime', 30*24*60*60); // 30 Tage
ini_set('session.cookie_lifetime', 30*24*60*60); // 30 Tage
// Hinweis: hier standen zwei ini_set() auf session.gc_maxlifetime und
// session.cookie_lifetime. Beide liefen ins Leere, weil die Sitzung zu
// diesem Zeitpunkt schon laeuft - PHP ignoriert Session-Einstellungen
// dann und meldet eine Warnung. Die Lebensdauer des Cookies kommt aus
// session_set_cookie_params() in inc/config.inc.php (30 Tage).
include(__DIR__ . "/../inc/password.inc.php");
$error_msg = "";
-1
View File
@@ -1,5 +1,4 @@
<?php
session_start();
require_once(__DIR__ . "/../inc/config.inc.php");
require_once(__DIR__ . "/../inc/functions.inc.php");
-1
View File
@@ -1,5 +1,4 @@
<?php
session_start();
require_once(__DIR__ . "/../inc/config.inc.php");
require_once(__DIR__ . "/../inc/functions.inc.php");
-1
View File
@@ -1,5 +1,4 @@
<?php
session_start();
require_once(__DIR__ . "/../inc/config.inc.php");
require_once(__DIR__ . "/../inc/functions.inc.php");
-1
View File
@@ -1,5 +1,4 @@
<?php
session_start();
require_once(__DIR__ . "/../inc/config.inc.php");
require_once(__DIR__ . "/../inc/functions.inc.php");
-1
View File
@@ -1,5 +1,4 @@
<?php
session_start();
require_once(__DIR__ . "/../inc/config.inc.php");
require_once(__DIR__ . "/../inc/functions.inc.php");
-1
View File
@@ -1,5 +1,4 @@
<?php
session_start();
require_once(__DIR__ . "/../inc/config.inc.php");
require_once(__DIR__ . "/../inc/functions.inc.php");
-1
View File
@@ -1,6 +1,5 @@
<?php
ob_start();
session_start();
require_once(__DIR__ . "/../inc/config.inc.php");
require_once(__DIR__ . "/../inc/functions.inc.php");