<?php #lib/99_manager.php

/********************************************************************************
 *	Content: API Manager class                                                  *
 *	Author: Nils Otterpohl                                                      *
 *	Last modification: 13.12.2020                                               *
 *	Version: alpha (object, incomplete, untested, uncommented)                  *
 ********************************************************************************/

// Missing:
// - Error handling
// - Proper response codes

class Manager
{
  public static $db;                    // Database, i.e. Mysqli
  public static $kv;                    // Key-Value-Storage, i.e. Redis

  public $user;                   // Object that handles login and logout
  public $printer;
  
  private $method = "GET";        // HTTP method used in connection
  private $requestedContentType = "HTML"; // Desired content type of request
  
  // Path used after index.php: index.php/MAIN/FILTER/SUB/SUBFILTER
  private $route = [
    "route" => "",                // Route of page module. Has to be set, at least to "/" [GET(all), POST]
    "mainID" => null,             // Filter of main resource. [GET(one), PUT, PATCH, DELETE] with ID, [GET(all)] with condition
    "sub" => null,                // Route of sub module. Requests are specific to a main resource, so only valid if filter is an ID. [GET(all), POST]
    "subID" => null               // Filter of sub resource. [GET(one), PUT, PATCH, DELETE] with ID, [GET(all)] with condition
  ];
  
  private $availablePages = [];   // Array of scanned and accessible page modules
  private $availableLinks = [];   // Array of scanned and accessible link modules
  private $availableObjects = []; // Array of scanned and accessible object modules
  private $responseCode = 401;    // HTTP code to be returned
  
  public $objects = [];
  public $input = [];             // Submitted JSON data after sanitation. ONLY SET IF Security token was valid.
  public $filter = [];
  public $selected = [];
  private $output = [
    "messages" => [],
    "admin" => false							// TODO DEPRECATED
  ];

  public function __construct($db, $kv, $key) {
    self::$db = $db;
    self::$kv = $kv;
    
    $jwt = $this->readRequest();
    
    if (isset($_SERVER["CONTENT_TYPE"]) && "application/json"==$_SERVER["CONTENT_TYPE"]) {
      $input = $this->cleanInput(json_decode(file_get_contents("php://input"), true));
    } else {
      $input = [];
      if (isset($_GET["filter"])) {
        $input["filter"] = $this->cleanInput(json_decode($_GET["filter"], true));
      }
      if (isset($_GET["selected"])) {
        $input["selected"] = $this->cleanInput(json_decode($_GET["selected"], true));
      } else {
        $input["selected"] = [];
      }
      if (isset($_GET["print"])) {
        $input["print"] = $_GET["print"];
      }
    }
    if ("HTML" == $this->requestedContentType && isset($input["print"])) {
      $this->requestedContentType = "PRINT";
    }

    $this->user = new Login($db, $kv, $key, $this, $jwt, $input, "GET"!=$this->method);
    if (in_array("Logout", $this->route)) {
      $this->user->Logout();
      $this->AddMessage("Logout erkannt!");
    }
    
    // Output status of user login to client
    $this->output["status"] = $this->user->Status();

    // Login was successful?
    if ($this->user->LoggedIn()) {
      // Überprüfe gesendetes Security-Token und veröffentliche ggfs. $input
      if ("GET"==$this->method) {
        // Maximal Filter oder Print als Input erlaubt
        if (isset($input["print"])) {
          $this->input["print"] = $input["print"];
        }
        if (isset($input["filter"])) {
          $this->filter = $input["filter"];
        }
        if (isset($input["selected"])) {
          $this->selected = $input["selected"];
        }
      } else if ("OPTIONS"==$this->method) {
      	// No filter or input at the moment
      	
      } else if (!empty($input)) {
        if (isset($input["secToken"]) && $this->user->VerifySecToken($input["secToken"])) {
          $this->input = $input;
        } else {
          $this->addMessage("Input wird ignoriert. Senden Sie den secToken mit!");
        } 
      } else {
        
      }
      
      $this->detectObjects();
      $this->detectLinks();
      $this->detectPages();
      if ("OPTIONS"==$this->method) {
      	$pages = $this->output["pages"];
      	$this->output["pages"] = [];
      	foreach ($pages as $page) {
      		$this->output["pages"][] = $page["route"];
      	}
      	$this->output["pagesChecksum"] = sha1(implode(",", $this->output["pages"]));
      	$this->responseCode = 200;
      } else {
	      $this->loadPage();
	      $this->detectPrints();
	      $this->output["filter"] = $this->filter;
  } } }
  
  public function JWT() {return $this->jwt;}
  public function Method() {return $this->method;}
  public function RequestedContentType() {return $this->requestedContentType;}
  public function Output() {return $this->output;}
  public function Route() {return $this->route["route"];}
  public function Main() {return $this->route["mainID"];}
  public function Sub() {return $this->route["sub"];}
  public function SubID() {return $this->route["subID"];}
  public function ResponseCode() {return $this->responseCode;}
  public function Filter() {return $this->filter;}
  
  public function AddMessage($message, $ident = null) {$this->output["messages"][] = $message;}
  public function ArrayToString($a) {
    $ret = "";
    foreach ($a as $k => $v) {
      $ret.=$k." => ".(is_array($v) ? "[".$this->ArrayToString($v)."]" : $v).", ";
    }
    return $ret;
  }
  
  /******************** PRIVATE FUNCTIONS ********************/
  
  private function readRequest() {
    $jwt = null;
    // Copied from https://stackoverflow.com/a/40582472
    $headers = null;
    if (isset($_SERVER['Authorization'])) {
      $headers = trim($_SERVER["Authorization"]);
    } else if (isset($_SERVER['HTTP_AUTHORIZATION'])) { //Nginx or fast CGI
      $headers = trim($_SERVER["HTTP_AUTHORIZATION"]);
    } elseif (function_exists('apache_request_headers')) {
      $requestHeaders = apache_request_headers();
      // Server-side fix for bug in old Android versions (a nice side-effect of this fix means we don't care about capitalization for Authorization)
      $requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders));
      //print_r($requestHeaders);
      if (isset($requestHeaders['Authorization'])) {
        $headers = trim($requestHeaders['Authorization']);
    } }
    
    // Get the access token from the header
    if (!empty($headers)) {
      if (preg_match('/Bearer\s(\S+)/', $headers, $matches)) {
        $jwt = $matches[1];
    } }
    // End of copied
    
    // Read accepted/requested content type
    if (isset($_SERVER["HTTP_ACCEPT"]) && "application/json"==$_SERVER["HTTP_ACCEPT"]) {
      $this->requestedContentType = "JSON";
    } elseif (isset($_SERVER["HTTP_ACCEPT"]) && "application/pdf"==$_SERVER["HTTP_ACCEPT"]) {
      $this->requestedContentType = "PDF";
    }
    
    // Read the requested route (resource) and method
    $this->method = $_SERVER['REQUEST_METHOD'];
    if (preg_match('$/([^/]+)/?([^/]+)?/?([^/]+)?/?([^/]+)?$', $_SERVER['PATH_INFO'] ?? "", $matches, PREG_UNMATCHED_AS_NULL)) {
      $this->route["route"] = $matches[1];
      $this->route["mainID"] = $matches[2];
      $this->route["sub"] = $matches[3];
      $this->route["subID"] = $matches[4];
    }
    return $jwt;
  }
  
  private function cleanInput($array) { // Cleans input
    $ret = [];
    if ($array!==null) {
      foreach ($array as $key => $value) {
        $ret[$key] = is_null($value) ? null : (is_array($value) ? $this->cleanInput($value) : (is_string($value) ? self::$db->escape_string($value) : intval($value)));
    } }
    return $ret;
  }
  
  private function detectObjects() {
    $dir = scandir("obj");
    foreach ($dir as $val) {
      if ($val!="." && $val!="..") {
        if (file_exists("obj/".$val."/module.json")) {
          $json = json_decode(file_get_contents("obj/".$val."/module.json"), true);
          $this->availableObjects[$json["name"]] = [
            "name" => $json["name"],
            "class" => $json["class"],
            "path" => $val,
            "useRight" => $json["useRight"] ?? null,
            "adminRight" => $json["adminRight"] ?? null
          ];
  } } } }
  
  private function detectLinks() {
    $dir = scandir("lnk");
    foreach ($dir as $val) {
      if ($val!="." && $val!="..") {
        if (file_exists("lnk/".$val."/module.json")) {
          $json = json_decode(file_get_contents("lnk/".$val."/module.json"), true);
          if ($this->user->HasRight($json["useRight"])) {
            $this->availableLinks[$json["route"]] = [
              "route" => $json["route"],
              "table" => $json["table"] ?? "",
              "path" => $val,
              "dependencies" => $json["dependencies"] ?? [],
              "useRight" => $json["useRight"] ?? null,
              "adminRight" => $json["adminRight"] ?? null,
            ];
  } } } } }
  
  private function detectPages() {
    $dir = scandir("pgs");
    foreach ($dir as $val) {
      if ($val!="." && $val!="..") {
        if (file_exists("pgs/".$val."/module.json")) {
          $json = json_decode(file_get_contents("pgs/".$val."/module.json"), true);
          if ($this->user->HasRight($json["useRight"])) {
            $this->availablePages[$json["route"]] = [
              "title" => $json["title"] ?? "",
              "route" => $json["route"],
              "table" => $json["table"] ?? "",
              "path" => $val,
              "links" => $json["links"] ?? [],
              "dependencies" => $json["dependencies"] ?? [],
              "useRight" => $json["useRight"] ?? null,
              "adminRight" => $json["adminRight"] ?? null,
            ];
            $page = [
              "link" => "/index.php/".$json["route"],
              "route" => $json["route"],
              "isPage" => $json["isPage"] ?? false,
            ];
            if ($page["isPage"]) {
              $page["name"] = $json["title"];
              $page["template"] = isset($json["template"]) ? "/pgs/".$val."/".$json["template"] : null;
              $page["marker"] = $json["marker"];
              $page["mainFields"] = $json["mainFields"];
            }
            $this->output["pages"][] = $page;
  } } } } }
  
  private function loadPage() {
    if (isset($this->availablePages[$this->Route()])) {
      $info = $this->availablePages[$this->Route()];
      
      foreach ($info["dependencies"] as $dep) {
        if (isset($this->availableObjects[$dep]) && !isset($this->objects[$dep])) {
          include "obj/".$this->availableObjects[$dep]["path"]."/object.php";
          $className = "\\".$this->availableObjects[$dep]["name"]."\\".$this->availableObjects[$dep]["class"];
          $this->objects[$dep] = new $className(self::$db, $this, $this->Main());
      } }
      
      $file = "pgs/".$info["path"]."/page.php";
      if (file_exists($file)) {
        include $file;
        $className = $info["route"]."Page";
        $page = new $className(self::$db, self::$kv, $this, $info);
        
        if (isset($this->route["sub"])) {
          if ($this->Main()!==null && $this->SubID()!==null) {
		        $sub = $page->GetLink($this->route["sub"]);
		        if (isset($sub, $this->availableLinks[$sub["route"]])) {
		          $link = $this->availableLinks[$sub["route"]];
		          include "lnk/".$link["path"]."/link.php";
		          $subClassName = $link["route"]."Link";
		          $subClass = new $subClassName(self::$db, self::$kv, $this, $link, $page);
		          $ids[$sub["mainName"]] = $this->Main();
		          $ids[$sub["subName"]] = $this->SubID();
		          
		          $this->responseCode = $subClass->DoRequest($this->method, $ids);
		          $page->DoRequest("GET");
		        } else {
		          $this->responseCode = 404;
		        }
          } else {
            $this->responseCode = 400;
		      }
        } else {
          $this->responseCode = $page->DoRequest($this->method);
        }
        
        if ($this->responseCode>=200 && $this->responseCode<300) {
          $this->output["meta"] = $page->GetMeta();
          $this->output["content"] = $page->GetResult();
          $this->output["template"] = $page->GetTemplate();
          if (isset($info["adminRight"]) && $this->user->HasRight($info["adminRight"])) {
            $this->output["admin"] = true;
          } 
          $this->output["options"] = $page->GetOptions($this->output["admin"]);
        }
      } else {
        $this->responseCode = 500;  // Missing file means misconfiguration
        $this->AddMessage($this->Route()." seems to be misconfigured. A required file is missing!");
      }
    } else if ($this->Route()==null) {
      // Dashboard with Stubs (hopefully)?
      $this->responseCode = 200;
      $this->output["content"] = [];
      $this->output["template"] = "";
    } else {
      // Load nothing, because an unknown page was requested
      $this->responseCode = 404;
      $this->AddMessage($this->Route()." doesn't exist");
      $this->output["content"] = [];
      $this->output["template"] = "";
  } }
  
  private function detectPrints() {
    $this->printer = new Printer(self::$db, $this);
    $this->output["prints"] = $this->printer->GetPrints();
  }
}
