matrix-register/lib/db.php

403 lines
12 KiB
PHP

<?php
/**
* file: db.php
* date: 25.01.2021
* author: bernd@nr18.space
* desc: Anbindung an die (Postgres) Datenbank.
*/
error_reporting(E_ALL);
ini_set("display_errors", "on");
ini_set("display_startip_errors", "on");
if (!defined('INCLUDES_ALLOWED'))
die('Access denied.');
function testDriver(): bool
{
print_r(PDO::getAvailableDrivers());
return true;
}
function getDatabase(&$config, $log): Database {
/**
* Erstellt ein Datenbank Objekt und gibt dieses zurück. Im Fehlerfall
* wird eine Exception mit der Meldung der PDOException ausgelöst.
*/
try {
$pdo = Connection::get()->connect($config);
$db = new Database($pdo, $log);
} catch (PDOException $e) {
throw new Exception($e->getMessage());
}
return $db;
}
class Connection {
/**
* Stellt die Verbindung zur Datenbank her. Derzeit sind Postgres, MySQL
* und SQLite Datenbanken möglich.
*/
private static $conn;
public function connect(&$config)
{
$driver = $config->getDbDriver();
$file = $config->getDbFile();
$host = $config->getDbHost();
$port = $config->getDbPort();
$base = $config->getDbBase();
$user = $config->getDbUser();
$pass = $config->getDbPass();
try {
if ($driver === "PDO_PGSQL") {
$pdo = new PDO("pgsql:host=$host;port=$port;dbname=$base", $user, $pass);
}
else if ($driver === "PDO_SQLITE") {
$pdo = new PDO("sqlite:$file");
}
else {
throw new Exception("Wrong driver for database: {$driver}");
return false;
}
} catch (PDOException $e) {
throw new Exception($e->getMessage());
}
if (isset($pdo)) {
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
return $pdo;
}
public static function get()
{
if (null === static::$conn)
{
static::$conn = new static();
}
return static::$conn;
}
public static function close()
{
static::$conn = null;
return true;
}
}
class Database {
/**
* Stellt das Datenbankobjekt und die Methoden zum Arbeiten mit der
* Datenbank zur verfügung.
*/
private $pdo;
private $log;
public function __construct($pdo, $log)
{
/**
* Übernimmt beim Erstellen der Klasse die Connection zur Datenbank
* und die Instanz des Loggers. Läßt kontrollieren ob die Tabelle zum
* Eintragen der Requests vorhanden ist und stößt die Erstellung der
* Tabelle an, wenn sie fehlt.
* TODO: Geht das eleganter? So wird das bei jeder Anfrage
* ausgeführt.
*/
$this->pdo = $pdo;
$this->log = $log;
$this->log->d("Databaseobject successfull created");
try {
if ($this->TableExists() === false) {
$this->createTable();
}
} catch (Exception $e) {
$this->log->e("Database exception: {$e->getMessage()}");
}
}
private function TableExists(): bool
{
/**
* Kontrolliert, ob eine Tabelle zum Eintragen der Requests
* vorhanden ist.
* TODO: Die Query ist Postgres spezifisch. Treiber durchreichen und
* eine Weiche anlegen?
*/
$this->log->d("Check if table requests exists");
$stmt = "SELECT * FROM information_schema.tables WHERE
table_type = 'BASE TABLE' and
table_name = 'requests'";
try {
$response = $this->pdo->query($stmt);
} catch (PDOException $e) {
throw new Exception($e->getMessage());
}
if ($response->rowCount() === 0) {
$this->log->n("No table requests found");
return false;
}
$this->log->d("Table requests found");
return true;
}
public function createTable(): bool
{
/**
* Erstellt die Tabelle Requests. Wir speichern die IP als 16 Byte
* Binary. Damit soll später ein gewisser Schutz gegen Spammer
* erreicht werden. (Wie viele Requests innerhalb welcher Zeit)
*/
$this->log->n("Try to create table requests");
$stmt = "CREATE TABLE IF NOT EXISTS requests (
id serial PRIMARY KEY,
nick varchar(80) NOT NULL UNIQUE,
email varchar(80) NOT NULL,
token char(32) NOT NULL UNIQUE,
ip bytea,
time integer NOT NULL);";
try {
$this->pdo->exec($stmt);
} catch (PDOException $e) {
$this->log->e("Failed to create table requests");
$this->log->e("Error: {$e->getMessage()}");
return false;
}
$this->log->n("Table requests successfull created");
return true;
}
public function UserExistsInUsers(string $nick): bool
{
/**
* Sucht in der Tabelle users nach bereits angelegten Nutzern. Die
* Abfrage der Datenbank wird an die Funktion searchUser delegiert.
* Sollte diese eine Exception werfen, wird true (es gibt einen
* Benutzer) zurück gegeben. Ansonsten wird der Rückgabewert
* von der Funktion getNick ausgewertet, die aus der übergebenen
* Matrix-ID den localpart extrahiert.
*/
$this->log->d("Search for localpart {$nick} in users");
$query = "SELECT name FROM users WHERE name LIKE :nick";
$pattern = "%$nick%";
try {
$response = $this->searchUser($query, $pattern);
} catch (PDOException $e) {
$this->log->e("searchUser() returns true because PDOException");
return true;
}
$count = count($response);
if ($count == 0)
{
$this->log->d("Nothing found");
return false;
}
else
{
foreach ($response as $array) {
$uid = $this->getNick($array['name']);
$this->log->d("Compare {$nick} with {$uid}");
if ($uid === $nick) {
$this->log->i("MXID localpart already exists: {$nick}");
return true;
} else {
$this->log->d("False");
}
}
}
return false;
}
public function UserExistsInRequests(string $nick): bool
{
/**
* Sucht in der Tabelle requests nach breits beantragten
* Nutzernamen. Die Abfrage der Datenbank wird an die Funktion
* searchUser delegiert. Sollte diese eine Exception werfen, wird
* true (es gibt einen benutzer) zurück gegeben. Ansonsten wird die
* Anzahl der Treffer ausgewertet.
*/
$this->log->d("Search for localpart {$nick} in requests");
$query = "SELECT nick FROM requests WHERE nick = :nick";
try {
$response = $this->searchUser($query, $nick);
} catch (PDOException $e) {
$this->log->e("searchUser() returns true because PDOException");
return true;
}
$count = count($response);
if ($count > 0) {
$this->log->d("Search for {$nick}: {$count} hit(s)");
return true;
}
$this->log->d("Nothing found");
return false;
}
private function searchUser(string $query, string $nick): array
{
/**
* Führt die Suchoperartion auf der Datenbank aus. Bekommt dafür den
* Querystring und den Nick übergeben. Sollte Abfrage der Datenbank
* fehlschlagen, wirft die Funktion die Exception an die aufrufende
* Funktion.
*/
try
{
$stmt = $this->pdo->prepare($query,
array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
$stmt->execute(array(':nick' => $nick)); // gibt bool zurück
$response = $stmt->fetchAll();
return $response;
}
catch (PDOException $e)
{
$errormsg = $e->getMessage();
$this->log->e("A PDO-Exception occurres");
$this->log->e("Error: {$errormsg}");
throw new PDOException($errormsg);
}
}
private function getNick(string $mid): string
{
/**
* Extrahiert aus einer Matrix-ID den localpart.
* TODO: In eine bibliothek auslagern? (/lib/common)
*/
$this->log->d("Extract nick from {$mid}");
$uid = "";
$append = false;
$strarray = str_split($mid);
foreach ($strarray as $char)
{
if ($char == '@')
{
$append = true;
}
else if ($char == ':')
{
$this->log->d("Extracted: {$uid}");
return $uid;
}
else
{
if ($append === true)
{
$uid = $uid.$char;
}
}
}
}
public function saveRequest($token): bool
{
/**
* Speichert den gewünschten Nick, die Emailadresse, das Token, die
* IP und einen Zeitstempel in der Tabelle Requests.
* TODO: IP nicht Hexadezimal, sondern Binär speichern. Spart Platz
* und ist schneller. Bin ich leider zu blöd für.
* TODO: Sollten/Müssen Nick und Email noch durch htmlspecialchars()
* oder reichen die prepared Statments?
*/
$bin = inet_pton($_SERVER['REMOTE_ADDR']);
$ip = bin2hex($bin);
$nick = $_POST['login'];
$email = $_POST['email'];
date_default_timezone_set("Europe/Berlin");
$time = time();
$this->log->d("Save request for: {$nick} with {$token} at {$time}");
try {
$stmt = $this->pdo->prepare("INSERT INTO requests
(nick, email, token, ip, time) VALUES
(:nick, :email, :token, :ip, :time)");
$stmt->BindValue(':nick', $nick);
$stmt->BindValue(':email', $email);
$stmt->BindValue(':token', $token);
$stmt->BindValue(':ip', $ip, PDO::PARAM_LOB);
$stmt->BindValue(':time', $time);
$response = $stmt->execute();
} catch (PDOException $e) {
$this->log->e("Saving request failed");
$this->log->e("Error: {$e->getMessage()}");
return false;
}
if ($response === 1) {
$this->log->i("Request saved successfull");
return true;
} else {
$this->log->e("Database returns: {$response}");
}
return false;
}
public function getToken(): array {
/**
* Sucht in der Tabelle requests nach dem Token. Gibt im Erfolgsfall
* ein Array mit ID, Name und Token zurück. Andernfalls ein leeres
* Array. Im Falle eines Datenbankfehlers wird eine Exception
* geworfen.
*/
$token = $_GET['token'];
$query = "SELECT id, nick, token FROM requests WHERE token = :token";
try {
$stmt = $this->pdo->prepare($query,
array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
$stmt->execute(array(':token' => $token)); // gibt bool zurück
$response = $stmt->fetchAll();
} catch (PDOException $e) {
$this->log->e("PDO Exception occures");
throw new Exception($e->getMessage());
}
$this->log->d("Database operation successfull");
return $response;
}
public function removeRequest(int $id): int {
/**
* Entfernt den Request aus der Tabelle requests. Wird aufgerufen,
* wenn die Registrierung des Nicks am Matrixserver erfolgreich war.
*/
$query = "DELETE FROM requests WHERE id = :id";
try {
$stmt = $this->pdo->prepare($query,
array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
$stmt->execute(array(':id' => $id)); // gibt bool zurück
} catch (PDOException $e) {
$this->log->e("PDO Exception occures");
throw new Exception($e->getMessage());
return false;
}
$this->log->d("Database operation successfull");
return $stmt->rowCount();;
}
}
?>