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 $config): PDO { $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}"); } } 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(): self { if (null === static::$conn) { static::$conn = new static(); } return static::$conn; } public static function close() { static::$conn = null; return true; } } class Database { protected const PREPARE_OPTIONS_CURSOR_FWDONLY = [ PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY, ]; /** * Stellt das Datenbankobjekt und die Methoden zum Arbeiten mit der * Datenbank zur verfügung. */ /** * @var PDO */ private $pdo; /** * @var Logger */ private $log; /** * Database constructor. * @param PDO $pdo * @param Logger $log */ public function __construct(PDO $pdo, Logger $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 bei jedem Request kontrolliert, * ob die Tabelle vorhanden ist. Anderseits gibt eine Konstruktor * nichts zurück. Ergo wird bei einem Fehler beim Erstellen der * Tabelle auch nicht abgebrochen, sondern erst beim Versuch etwas * hineinzuschreiben. */ $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. */ $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"); $driverName = $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME); if ($driverName === "pgsql") { $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);"; } else if ($driverName === "sqlite") { // ungetestet $stmt = "CREATE TABLE IF NOT EXISTS request ( id INTEGER PRIMARY KEY AUTOINCREMENT, nick TEXT NUT NULL UNIQUE; email TEXT NOT NULL, token TEXT NOT NULL UNIQUE, ip BLOB, time INTEGER NOT NULL);"; } else { $stmt = ''; } if ($stmt) { try { $response = $this->pdo->exec($stmt); } catch (PDOException $e) { $this->log->e("Failed to create table requests"); $this->log->e("Error: {$e->getMessage()}"); return false; } if ($response !== false) { $this->log->n("Table requests successfull created"); } $this->log->n("Could not create table requests"); } return false; } 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. Nutzt für die Suche die * Klassenfunktion searchUser(). Braucht getNick() common.php! */ $userFound = false; $users = []; $this->log->d("Search for localpart {$nick} in users"); $query = "SELECT name FROM users WHERE name = :nick"; $name = "@" . $nick . ":matrix.kraut.space"; $users = $this->searchUser($query, $name); $count = count($users); if ($count == 0) { $this->log->d("Nothing found"); return $userFound; } else { foreach ($users as $user) { $uid = getNick($user['name']); $this->log->d("Compare {$nick} with {$uid}"); if ($uid === $nick) { $this->log->i("MXID localpart already exists: {$nick}"); $userFound = true; break; } else { $this->log->d("False"); } } } return $userFound; } 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. Nutzt für die Suche die eigene * Funktion searchUser(). */ $response = []; $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"); } $count = count($response); if ($count > 0) { $this->log->n("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, self::PREPARE_OPTIONS_CURSOR_FWDONLY ); $stmt->execute(array(':nick' => $nick)); // gibt bool zurück return $stmt->fetchAll(); } catch (PDOException $e) { $errormsg = $e->getMessage(); $this->log->e("A PDO-Exception occurres"); $this->log->e("Error: {$errormsg}"); throw $e; } } public function getTimestamps(string $ip): array { /** * Schaut in der Datenbank, ob es bereits Einträge mit der aktuellen * Remote IP gibt und gibt alle Einträge des dazu gehörendem * Zeitstempels zurück. Braucht getRemoteHexIP() aus common.php! * TODO: flexibler gestalten? IP als Parameter übergeben? */ $this->log->i("Search for IP: {$_SERVER['REMOTE_ADDR']}"); $stmt = $this->pdo->prepare( "SELECT time FROM requests WHERE ip = :ip" ); try { $stmt->BindValue(':ip', $ip, PDO::PARAM_LOB); $stmt->execute(); $response = $stmt->fetchAll(); } catch (PDOException $e) { $this->log->e("getIPs: ERROR: {$e->getMessage()}"); } $count = count($response); $this->log->d("{$count} Timestamps found");; return $response; } public function saveRequest(string $token, string $ip): bool { /** * Speichert den gewünschten Nick, die Emailadresse, das Token, die * IP und einen Zeitstempel in der Tabelle Requests. Braucht die * Funktionen getRemoteHexIP() und getNow() aus common.php. * 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? */ $nick = $_POST['login']; $email = $_POST['email']; $time = getNow(); $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) { $this->log->n("Request saved successfull"); return true; } 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, self::PREPARE_OPTIONS_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, self::PREPARE_OPTIONS_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(); } } ?>