<?php #lib/10_login.php

/********************************************************************************
 *	Content: Login class                                                        *
 *	Author: Nils Otterpohl                                                      *
 *	Last modification: 13.12.2020                                               *
 *	Version: alpha (object, incomplete, uncommented, untested)                  *
 ********************************************************************************/
 
class Login
{
	private static $id = "";			// ID of authenticated user
	private static $loggedIn = false;
	
  private static $user = "";        // Login name of authenticated user
  private static $vornamen = "";     // Prenames of authenticated user
  private static $nachnamen = "";    // Surnames of authenticated user
	
	private static $groups = null;
	private static $departments = null;
	private static $rights = null;
  
  private static $jwt_expires = 0;
  private static $secTokenUse = null;    // (One time) Security token to be issued to client to be used with incoming data
  private static $secTokenVerify = null; // Last Security token issued. Incoming data will be verified against this
  
  /***** Public Static functions *****/

  // Generates a salted password hash of desired length and with the desired algorithm
  public static function GenHash($password, $salt, $iterations, $length = 32, $algo = "sha256") {
    return hash_pbkdf2($algo, $password, $salt, $iterations, $length*2);
  }
  
  // Generates a random salt (or also password) of desired length
  public static function GenSalt($length = 16) {
    return bin2hex(random_bytes($length));
  }
  
  public static function LoggedIn() {return self::$loggedIn;}
  public static function ID() {return self::$id;}
  public static function User() {return self::$user;}
  public static function Vornamen() {return self::$vornamen;}
  public static function Nachnamen() {return self::$nachnamen;}
  
  public static function Login() {
    // Try to login via JWT, if it was submitted
    if (!is_null(Request::JWT())) {
      self::authenticateJWT();
    }
    
    // Try to login via password, if not yet logged in
    if (!self::$loggedIn && !is_null(Request::Input("login")) && !is_null(Request::Input("password"))) {
      self::authenticatePWD(Request::Input("login"), Request::Input("password"));
    }
    
    if (self::$loggedIn) {
      // Erzeuge neues Security-Token
      self::$secTokenUse = self::$secTokenVerify;
      if (Request::IssueNewSecToken() || self::$secTokenUse==null) {
        self::$secTokenUse = self::GenSalt(16);
        Response::Get()->Json("secToken", self::$secTokenUse);
      }
      KV::Get()->set("secToken:".self::$id, self::$secTokenUse);
      KV::Get()->expire("secToken:".self::$id, 60*60);  // secToken ist für 60 min * 60 s/min gültig
      if (!is_null(self::$secTokenVerify)) {
      	Request::VerifyInputSecToken(self::$secTokenVerify);
  } } }
  
  // Blacklists the issued JWT and sets logged in status to false
  public static function Logout() {
    KV::Get()->set("JWT:Blacklist:".self::$id, Request::JWT());
    KV::Get()->expireAt("JWT:Blacklist:".self::$id, self::$jwt_expires);
    self::$loggedIn = false;	// DEPRECATED
    self::$loggedIn = false;
  }
  
  public static function VerifySecToken($secToken) {
  	Response::Get()->Message("sTV: ".self::$secTokenVerify);
    return (self::$secTokenVerify==null || $secToken==self::$secTokenVerify);
  }
  
  // Returns true if logged in user has the (or, if array, one of the) named right(s)
  // If null or empty array is passed, no rights shall be required and return will be true (if logged in).
  // Fetched rights will be buffered, so recurring queries are no performance problem
  static function HasRight($rightNames) {
    if (!self::$loggedIn) {
      return false;
    }
    if ((is_array($rightNames) && empty($rightNames)) || is_null($rightNames)) {
      return true;
    }
    if (is_null(self::$rights)) {
      self::$rights = [];
      $qry = "SELECT r.Name FROM Personal_Verwalter pv "
            ."LEFT JOIN Rechte_Verwalter rv ON rv.Verwalter = pv.Verwalter "
            ."LEFT JOIN Rechte r ON r.ID = rv.Rechte "
            ."WHERE pv.Personal = ".self::$id;
      $res = Db::Get()->query($qry);
      while ($row = $res->fetch_assoc()) {
        self::$rights[] = $row["Name"];
    } }
    if (is_array($rightNames)) {
      foreach ($rightNames as $r) {
        if (in_array($r, self::$rights)) {
          return true;
      } }
      return false;
    } else {
      return in_array($rightNames, self::$rights);
  } }
  
  // Returns true if logged in user is in the (or, if array, one of the) named department(s) ("Abteilungen")
  // Fetched departments will be buffered, so recurring queries are no performance problem
  public static function InDepartment($departmentIDs) {
    if (!self::$loggedIn) {
      return false;
    }
    if ((is_array($departmentIDs) && empty($departmentIDs)) || is_null($departmentIDs)) {
      return true;
    }
    if (!isset(self::$departments)) {
      self::$departments = [];
      $qry = "SELECT Abteilungen FROM Personal_Abteilungen WHERE Personal = ".self::$id;
      $res = Db::Get()->query($qry);
      while ($row = $res->fetch_assoc()) {
        self::$departments[] = $row["Abteilungen"];
    } }
    if (is_array($departmentIDs)) {
      foreach ($departmentIDs as $dID) {
        if (in_array($dID, self::$departments)) {
          return true;
      } }
      return false;
    } else {
      return in_array($departmentIDs, self::$departments);
  } }
  
  // Returns true if logged in user is in the (or, if array, one of the) named group(s) ("Gruppen")
  // Fetched groups will be buffered, so recurring queries are no performance problem
  public static function InGroup($groupIDs) {
    if (!self::$loggedIn) {
      return false;
    }
    if ($groupIDs==null) {
      return true;
    }
    if (!isset(self::$groups)) {
      self::$groups = [];
      $qry = "SELECT Gruppen FROM Personal_Gruppen WHERE Personal = ".self::$id;
      $res = Db::Get()->query($qry);
      while ($row = $res->fetch_assoc()) {
        self::$groups[] = $row["Gruppen"];
    } }
    if (is_array($groupIDs)) {
      foreach ($groupIDs as $gID) {
        if (in_array($gID, self::$groups)) {
          return true;
      } }
      return false;
    } else {
      return in_array($groupIDs, self::$groups);
  } }
  
  /***** Private Static functions *****/
  
  // Tries to decode the submitted JWT, checks blacklisting of JWT and sets logged in status accordingly
  private static function authenticateJWT() {
    try {
      $decoded = \Firebase\JWT\JWT::decode(Request::JWT(), JWT_KEY, ['HS256']);
      // What has been an array before encoding is now an object
      // Read member variables from decoded data
      self::$id = $decoded->data->userID==58 && isset($_GET["emulate"]) ? $_GET["emulate"] : $decoded->data->userID;
      self::$user = $decoded->data->userLogin;
      self::$vornamen = $decoded->data->userVornamen;
      self::$nachnamen = $decoded->data->userNachnamen;
      self::$jwt_expires = $decoded->exp;
    } catch (Exception $e) {
      // JWT will throw exceptions if it cannot decode the JWT. In this case - don't login
      return;
    }
    if (KV::Get()->get("JWT:Blacklist:".self::$id)!=Request::JWT()) {
      // Not blacklisted, can log in
      self::$loggedIn = true;
      self::$secTokenVerify = KV::Get()->get("secToken:".self::$id);
      $now = time();
      if (self::$jwt_expires-$now <= JWT_VALID_TIME*0.5 && Request::AllowJwtRenewal()) {
      	Response::Get()->Message("Extending JWT");
      	self::encodeJWT();
      }
    } else {
      self::$id = -99;
  } }
  
  private static function encodeJWT() {
    $time = time();
    self::$jwt_expires = $time + JWT_VALID_TIME;
    $issuer = "http://fw-innenstadt.de/";
    $token = [
       "iat" => $time,
       "exp" => self::$jwt_expires,
       "iss" => "fw-innenstadt.de",
       "data" => [
           "userID" => self::$id,
           "userLogin" => self::$user,
           "userVornamen" => self::$vornamen,
           "userNachnamen" => self::$nachnamen
       ]
    ];
    Response::Get()->Json("jwt", \Firebase\JWT\JWT::encode($token, JWT_KEY));
  }
  
  private static function authenticatePWD($user, $password) {
    // Copied and adjusted after https://de.wikihow.com/Ein-sicheres-Login-Skript-mit-PHP-und-MySQL-erstellen
    // and https://github.com/nextcloud/user_external/blob/master/lib/webdavauth.php
    // and https://codeofaninja.com/2018/09/rest-api-authentication-example-php-jwt-tutorial.html
    
    // Login zum gewünschten Format zurechtbiegen
    $user = strtolower($user);
    $user = str_replace("@feuerwehr-bs.net", "", $user);
    
    // Das Benutzen vorbereiteter Statements verhindert SQL-Injektion.
    if ($stmt = Db::Get()->prepare("SELECT p.ID, p.login, p.Vornamen, p.Nachnamen FROM Personal p WHERE p.login = ? LIMIT 1")) {
      $stmt->bind_param("s", $user);  // Bind "$user" to parameter.
      $stmt->execute();    // Führe die vorbereitete Anfrage aus.
      $stmt->store_result();
  
      // hole Variablen von result.
      $stmt->bind_result(self::$id, self::$user, self::$vornamen, self::$nachnamen);
      $stmt->fetch();
  
      if ($stmt->num_rows == 1) {
        $url= 'https://'.urlencode(self::$user).':'.urlencode($password).'@feuerwehr-bs.net/webdav';
        $headers = get_headers($url);
        if($headers === false) {
          Response::Get()->Message("ERROR: Not possible to connect to WebDAV Url: https://feuerwehr-bs.net/webdav");
          // THROW
          return;
        }
        $returnCode= substr($headers[0], 9, 3);
  
        if (substr($returnCode, 0, 1) === '2') {
          // Passwort ist korrekt!
          // XSS-Schutz, denn eventuell wird der Wert gedruckt
          self::$id = preg_replace("/[^0-9]+/", "", self::$id);
          self::$user = preg_replace("/[^a-zA-Z0-9_\-]+/", "", self::$user);
          
          // Generiere einen Hash aus dem Passwort mit zufälligem Salt und speichere ihn in der Datenbank
          // Question: Why?
          $hash = Login::GenHash($password, Login::GenSalt(), DESIRED_ITERATIONS);
          Db::Get()->query("REPLACE INTO sys_iservhashes(ID, Hash) VALUES ('".self::$id."', '".$hash."')");
  
          // Login erfolgreich.
          self::encodeJWT();
          self::$loggedIn = true;
          Response::Get()->Message("Login erfolgreich!");
          return;
        } else if ($returnCode === "401") {
          // Passwort ist nicht korrekt
  		    Response::Get()->Message("Das Passwort ist vermutlich nicht korrekt. Bitte erneut probieren oder an Nils wenden.");
        } else if ($returnCode === "503") {
          // Passwort ist nicht korrekt
  		    Response::Get()->Message("IServ verweigert im Moment die Anmeldung. Vermutlich gab es temporär zu viele falsche Passworteingaben "
  		                            ."(auch von anderen Nutzern). Bitte erst in 10 Minuten neu probieren!");
        }  else {
          // Unbekannter Fehler
  		    Response::Get()->Code(500)->Message("Anmeldung konnte aus unbekannten Gründen nicht durchgeführt werden. Fehlercode (bitte an Nils senden): ".$returnCode);
        }
      } else {
        Response::Get()->Message("Benutzername inkorrekt: ".$user);
    } }
    if (Db::Get()->error!="") {
      Response::Get()->Message("Mysql error: ".Db::Get()->error);
    }
    return;
  }
}