Browse Source

Merge branch 'dev'

Roman 3 years ago
parent
commit
0fc03b394d

+ 2 - 2
README.md

@@ -37,7 +37,7 @@ The compiled dist files will be automatically moved to `/js`.
 
 Each API endpoint has usually one overlying category, for example all user and authorization endpoints belong to the [UserAPI](/core/Api/UserAPI.class.php).
 These endpoints can be accessed by requesting URLs starting with `/api/user`, for example: `/api/user/login`. There are also endpoints, which don't have
-a category, e.g. [PatchSQL](/core/Api/PatchSQL.class.php). These functions can be called directly, for example with `/api/patchSQL`. Both methods have one thing in common:
+a category, e.g. [VerifyCaptcha](/core/Api/VerifyCaptcha.class.php). These functions can be called directly, for example with `/api/verifyCaptcha`. Both methods have one thing in common:
 Each endpoint is represented by a class inheriting the [Request Class](/core/Api/Request.class.php). An example endpoint looks like this:
 
 ```php
@@ -112,7 +112,7 @@ If any result is expected from the api call, the `$req->getResult()` method can
 This step is not really required, as and changes made to the database must not be presented inside the code.
 On the other hand, it is recommended to keep track of any modifications for later use or to deploy the application
 to other systems. Therefore, either the [default installation script](/core/Configuration/CreateDatabase.class.php) or
-an additional patch file, which can be executed using the API (`/api/PatchSQL`), can be created. The patch files are usually
+an additional patch file, which can be executed using the [CLI](/cli.php), can be created. The patch files are usually
 located in [/core/Configuration/Patch](/core/Configuration/Patch) and have the following structure:
 
 ```php

+ 410 - 0
cli.php

@@ -0,0 +1,410 @@
+<?php
+
+include_once 'core/core.php';
+include_once 'core/constants.php';
+
+use Configuration\Configuration;
+use Configuration\DatabaseScript;
+use Objects\ConnectionData;
+use Objects\User;
+
+function printLine(string $line = "") {
+  echo $line . PHP_EOL;
+}
+
+function _exit(string $line = "") {
+  printLine($line);
+  die();
+}
+
+if (php_sapi_name() !== "cli") {
+  _exit("Can only be executed via CLI");
+}
+
+function getDatabaseConfig(): ConnectionData {
+  $configClass = "\\Configuration\\Database";
+  $file = getClassPath($configClass);
+  if (!file_exists($file) || !is_readable($file)) {
+    _exit("Database configuration does not exist or is not readable");
+  }
+
+  include_once $file;
+  return new $configClass();
+}
+
+function getUser(): User {
+  $config = new Configuration();
+  $user = new User($config);
+  if (!$user->getSQL() || !$user->getSQL()->isConnected()) {
+    _exit("Could not establish database connection");
+  }
+
+  return $user;
+}
+
+function printHelp() {
+  // TODO: help
+}
+
+function handleDatabase(array $argv) {
+  $action = $argv[2] ?? "";
+
+  if ($action === "migrate") {
+    $class = $argv[3] ?? null;
+    if (!$class) {
+      _exit("Usage: cli.php db migrate <class name>");
+    }
+
+    $class = str_replace('/', '\\', $class);
+    $className = "\\Configuration\\$class";
+    $classPath = getClassPath($className);
+    if (!file_exists($classPath) || !is_readable($classPath)) {
+      _exit("Database script file does not exist or is not readable");
+    }
+
+    include_once $classPath;
+    $obj = new $className();
+    if (!($obj instanceof DatabaseScript)) {
+      _exit("Not a database script");
+    }
+
+    $user = getUser();
+    $sql = $user->getSQL();
+    $queries = $obj->createQueries($sql);
+    foreach ($queries as $query) {
+      if (!$query->execute($sql)) {
+        _exit($sql->getLastError());
+      }
+    }
+  } else if ($action === "export" || $action === "import") {
+
+    // database config
+    $config = getDatabaseConfig();
+    $dbType = $config->getProperty("type") ?? null;
+    $user = $config->getLogin();
+    $password = $config->getPassword();
+    $database = $config->getProperty("database");
+    $host = $config->getHost();
+    $port = $config->getPort();
+
+    // subprocess config
+    $env = [];
+    $options = array_slice($argv, 3);
+    $dataOnly = in_array("--data-only", $options) || in_array("-d", $options);
+    $descriptorSpec = [STDIN, STDOUT, STDOUT];
+    $inputData = null;
+
+    // argument config
+    if ($action === "import") {
+      $file = $argv[3] ?? null;
+      if (!$file) {
+        _exit("Usage: cli.php db import <path>");
+      }
+
+      if (!file_exists($file) || !is_readable($file)) {
+        _exit("File not found or not readable");
+      }
+
+      $inputData = file_get_contents($file);
+    }
+
+    if ($dbType === "mysql") {
+      $command_args = ["-u", $user, '-h', $host, '-P', $port, "--password=$password"];
+      if ($action === "export") {
+        $command_bin = "mysqldump";
+        if ($dataOnly) {
+          $command_args[] = "--skip-triggers";
+          $command_args[] = "--compact";
+          $command_args[] = "--no-create-info";
+        }
+      } else if ($action === "import") {
+        $command_bin = "mysql";
+        $descriptorSpec[0] = ["pipe", "r"];
+      }
+    } else if ($dbType === "postgres") {
+
+      $env["PGPASSWORD"] = $password;
+      $command_args = ["-U", $user, '-h', $host, '-p', $port];
+
+      if ($action === "export") {
+        $command_bin = "/usr/bin/pg_dump";
+        if ($dataOnly) {
+          $command_args[] = "--data-only";
+        }
+      } else if ($action === "import") {
+        $command_bin = "/usr/bin/psql";
+        $descriptorSpec[0] = ["pipe", "r"];
+      }
+
+    } else {
+      _exit("Unsupported database type");
+    }
+
+    if ($database) {
+      $command_args[] = $database;
+    }
+
+    $command = array_merge([$command_bin], $command_args);
+    $process = proc_open($command, $descriptorSpec, $pipes, null, $env);
+
+    if (is_resource($process)) {
+      if ($action === "import" && $inputData && count($pipes) > 0) {
+        fwrite($pipes[0], $inputData);
+        fclose($pipes[0]);
+      }
+
+      proc_close($process);
+    }
+  } else {
+    _exit("Usage: cli.php db <migrate|import|export> [options...]");
+  }
+}
+
+function findPullBranch(array $output): ?string {
+  foreach ($output as $line) {
+    $parts = preg_split('/\s+/', $line);
+    if (count($parts) >= 3 && $parts[2] === '(fetch)') {
+      $remoteName = $parts[0];
+      $url = $parts[1];
+      if (endsWith($url, "@github.com:rhergenreder/web-base.git") ||
+          endsWith($url, "@romanh.de:Projekte/web-base.git") ||
+          $url === 'https://github.com/rhergenreder/web-base.git' ||
+          $url === 'https://git.romanh.de/Projekte/web-base.git') {
+        return "$remoteName/master";
+      }
+    }
+  }
+
+  return null;
+}
+
+function onMaintenance(array $argv) {
+  $action = $argv[2] ?? "status";
+  $maintenanceFile = "MAINTENANCE";
+  $isMaintenanceEnabled = file_exists($maintenanceFile);
+
+  if ($action === "status") {
+    _exit("Maintenance: " . ($isMaintenanceEnabled ? "on" : "off"));
+  } else if ($action === "on") {
+    $file = fopen($maintenanceFile, 'w') or _exit("Unable to create maintenance file");
+    fclose($file);
+    _exit("Maintenance enabled");
+  } else if ($action === "off") {
+    if (file_exists($maintenanceFile)) {
+      if (!unlink($maintenanceFile)) {
+        _exit("Unable to delete maintenance file");
+      }
+    }
+    _exit("Maintenance disabled");
+  } else if ($action === "update") {
+
+    printLine("$ git remote -v");
+    exec("git remote -v", $gitRemote, $ret);
+    if ($ret !== 0) {
+      die();
+    }
+
+    $pullBranch = findPullBranch($gitRemote);
+    if ($pullBranch === null) {
+      $pullBranch = 'origin/master';
+      printLine("Unable to find remote update branch. Make sure, you are still in a git repository, and one of the remote branches " .
+                      "have the original fetch url");
+      printLine("Trying to continue with '$pullBranch'");
+    } else {
+      printLine("Using remote update branch: $pullBranch");
+    }
+
+    printLine("$ git fetch " . str_replace("/", " ", $pullBranch));
+    exec("git fetch " . str_replace("/", " ", $pullBranch), $gitFetch, $ret);
+    if ($ret !== 0) {
+      die();
+    }
+
+    printLine("$ git log HEAD..$pullBranch --oneline");
+    exec("git log HEAD..$pullBranch --oneline", $gitLog, $ret);
+    if ($ret !== 0) {
+      die();
+    } else if (count($gitLog) === 0) {
+      _exit("Already up to date.");
+    }
+
+    printLine("Found updates, checking repository state");
+    printLine("$ git diff-index --quiet HEAD --"); // check for any uncommitted changes
+    exec("git diff-index --quiet HEAD --", $gitDiff, $ret);
+    if ($ret !== 0) {
+      _exit("You have uncommitted changes. Please commit them before updating.");
+    }
+
+    // enable maintenance mode if it wasn't turned on before
+    if (!$isMaintenanceEnabled) {
+      printLine("Turning on maintenance mode");
+      $file = fopen($maintenanceFile, 'w') or _exit("Unable to create maintenance file");
+      fclose($file);
+    }
+
+    printLine("Ready to update, pulling and merging");
+    printLine("$ git pull " . str_replace("/", " ", $pullBranch) . " --no-ff");
+    exec("git pull " . str_replace("/", " ", $pullBranch) . " --no-ff", $gitPull, $ret);
+    if ($ret !== 0) {
+      printLine();
+      printLine("Update could not be applied, check the git output.");
+      printLine("Follow the instructions and afterwards turn off the maintenance mode again using:");
+      printLine("cli.php maintenance off");
+      die();
+    }
+
+    // disable maintenance mode again
+    if (!$isMaintenanceEnabled) {
+      printLine("Turning off maintenance mode");
+      if (file_exists($maintenanceFile)) {
+        if (!unlink($maintenanceFile)) {
+          _exit("Unable to delete maintenance file");
+        }
+      }
+    }
+  } else {
+    _exit("Usage: cli.php maintenance <status|on|off|update>");
+  }
+}
+
+function printTable(array $head, array $body) {
+
+  $columns = [];
+  foreach ($head as $key) {
+    $columns[$key] = strlen($key);
+  }
+
+  foreach ($body as $row) {
+    foreach ($head as $key) {
+      $value = $row[$key] ?? "";
+      $length = strlen($value);
+      $columns[$key] = max($columns[$key], $length);
+    }
+  }
+
+  // print table
+  foreach ($head as $key) {
+    echo str_pad($key, $columns[$key]) . '   ';
+  }
+  printLine();
+
+  foreach ($body as $row) {
+    foreach ($head as $key) {
+      echo str_pad($row[$key] ?? "", $columns[$key]) . '   ';
+    }
+    printLine();
+  }
+}
+
+// TODO: add missing api functions (should be all internal only i guess)
+function onRoutes(array $argv) {
+
+  $user = getUser();
+  $action = $argv[2] ?? "list";
+
+  if ($action === "list") {
+    $req = new Api\Routes\Fetch($user);
+    $success = $req->execute();
+    if (!$success) {
+      _exit("Error fetching routes: " . $req->getLastError());
+    } else {
+      $routes = $req->getResult()["routes"];
+      $head = ["uid", "request", "action", "target", "extra", "active"];
+
+      // strict boolean
+      foreach ($routes as &$route) {
+        $route["active"] = $route["active"] ? "true" : "false";
+      }
+
+      printTable($head, $routes);
+    }
+  } else if ($action === "add") {
+    if (count($argv) < 6) {
+      _exit("Usage: cli.php routes add <request> <action> <target> [extra]");
+    }
+
+    $params = array(
+      "request" => $argv[3],
+      "action" => $argv[4],
+      "target" => $argv[5],
+      "extra" => $argv[6] ?? ""
+    );
+
+    $req  = new Api\Routes\Add($user);
+    $success = $req->execute($params);
+    if (!$success) {
+      _exit($req->getLastError());
+    } else {
+      _exit("Route added successfully");
+    }
+  } else if (in_array($action, ["remove","modify","enable","disable"])) {
+    $uid = $argv[3] ?? null;
+    if ($uid === null || ($action === "modify" && count($argv) < 7)) {
+      if ($action === "modify") {
+        _exit("Usage: cli.php routes $action <uid> <request> <action> <target> [extra]");
+      } else {
+        _exit("Usage: cli.php routes $action <uid>");
+      }
+    }
+
+    $params = ["uid" => $uid];
+    if ($action === "remove") {
+      $input = null;
+      do {
+        if ($input === "n") {
+          die();
+        }
+        echo "Remove route #$uid? (y|n): ";
+      } while(($input = trim(fgets(STDIN))) !== "y");
+
+      $req = new Api\Routes\Remove($user);
+    } else if ($action === "enable") {
+      $req = new Api\Routes\Enable($user);
+    } else if ($action === "disable") {
+      $req = new Api\Routes\Disable($user);
+    } else if ($action === "modify") {
+      $req = new Api\Routes\Update($user);
+      $params["request"] = $argv[4];
+      $params["action"] = $argv[5];
+      $params["target"] = $argv[6];
+      $params["extra"] = $argv[7] ?? "";
+    } else {
+      _exit("Unsupported action");
+    }
+
+    $success = $req->execute($params);
+    if (!$success) {
+      _exit($req->getLastError());
+    } else {
+      _exit("Route updated successfully");
+    }
+  } else {
+    _exit("Usage: cli.php routes <list|enable|disable|add|remove|modify> [options...]");
+  }
+}
+
+$argv = $_SERVER['argv'];
+if (count($argv) < 2) {
+  _exit("Usage: cli.php <db|routes|settings|maintenance> [options...]");
+}
+
+$command = $argv[1];
+switch ($command) {
+  case 'help':
+    printHelp();
+    exit;
+  case 'db':
+    handleDatabase($argv);
+    break;
+  case 'routes':
+    onRoutes($argv);
+    break;
+  case 'maintenance':
+    onMaintenance($argv);
+    break;
+  default:
+    printLine("Unknown command '$command'");
+    printLine();
+    printHelp();
+    exit;
+}

+ 0 - 65
core/Api/PatchSQL.class.php

@@ -1,65 +0,0 @@
-<?php
-
-namespace Api;
-
-use Api\Parameter\StringType;
-use Configuration\DatabaseScript;
-use Objects\User;
-
-class PatchSQL extends Request {
-
-  public function __construct(User $user, bool $externalCall = false) {
-    parent::__construct($user, $externalCall, array(
-      "className" => new StringType("className", 64)
-    ));
-    $this->loginRequired = true;
-    $this->csrfTokenRequired = false;
-  }
-
-  public function execute($values = array()): bool {
-    if (!parent::execute($values)) {
-      return false;
-    }
-
-    $className = $this->getParam("className");
-    $fullClassName = "\\Configuration\\Patch\\" . $className;
-    $path = getClassPath($fullClassName, true);
-    if (!file_exists($path)) {
-      return $this->createError("File not found");
-    }
-
-    if(!class_exists($fullClassName)) {
-      return $this->createError("Class not found.");
-    }
-
-    try {
-      $reflection = new \ReflectionClass($fullClassName);
-      if (!$reflection->isInstantiable()) {
-        return $this->createError("Class is not instantiable");
-      }
-
-      if (!$reflection->isSubclassOf(DatabaseScript::class)) {
-        return $this->createError("Not a database script.");
-      }
-
-      $sql = $this->user->getSQL();
-      $obj = $reflection->newInstance();
-      $queries = $obj->createQueries($sql);
-      if (!is_array($queries)) {
-        return $this->createError("Database script returned invalid values");
-      }
-
-      foreach($queries as $query) {
-        if (!$query->execute()) {
-          return $this->createError("Query error: " . $sql->getLastError());
-        }
-      }
-
-      $this->success = true;
-    } catch (\ReflectionException $e) {
-      return $this->createError("Error reflecting class: " . $e->getMessage());
-    }
-
-    return $this->success;
-  }
-}

+ 8 - 6
core/Api/Request.class.php

@@ -111,13 +111,15 @@ class Request {
       return false;
     }
 
-    if (!in_array($_SERVER['REQUEST_METHOD'], $this->allowedMethods)) {
-      $this->lastError = 'This method is not allowed';
-      header('HTTP 1.1 405 Method Not Allowed');
-      return false;
-    }
-
     if ($this->externalCall) {
+
+      // check the request method
+      if (!in_array($_SERVER['REQUEST_METHOD'], $this->allowedMethods)) {
+        $this->lastError = 'This method is not allowed';
+        header('HTTP 1.1 405 Method Not Allowed');
+        return false;
+      }
+
       $apiKeyAuthorized = false;
 
       // Logged in or api key authorized?

+ 186 - 6
core/Api/RoutesAPI.class.php

@@ -1,8 +1,13 @@
 <?php
 
 namespace Api {
+
+  use Driver\SQL\Condition\Compare;
+
   abstract class RoutesAPI extends Request {
 
+    const ACTIONS = array("redirect_temporary", "redirect_permanently", "static", "dynamic");
+
     protected function formatRegex(string $input, bool $append) : string {
       $start = startsWith($input, "^");
       $end = endsWith($input, "$");
@@ -16,6 +21,39 @@ namespace Api {
 
       return $input;
     }
+
+    protected function routeExists($uid): bool {
+      $sql = $this->user->getSQL();
+      $res = $sql->select($sql->count())
+        ->from("Route")
+        ->where(new Compare("uid", $uid))
+        ->execute();
+
+      $this->success = ($res !== false);
+      $this->lastError = $sql->getLastError();
+      if ($this->success) {
+        if ($res[0]["count"] === 0) {
+          return $this->createError("Route not found");
+        }
+      }
+
+      return $this->success;
+    }
+
+    protected function toggleRoute($uid, $active): bool {
+      if (!$this->routeExists($uid)) {
+        return false;
+      }
+
+      $sql = $this->user->getSQL();
+      $this->success = $sql->update("Route")
+        ->set("active", $active)
+        ->where(new Compare("uid", $uid))
+        ->execute();
+
+      $this->lastError = $sql->getLastError();
+      return $this->success;
+    }
   }
 }
 
@@ -25,8 +63,10 @@ namespace Api\Routes {
   use Api\Parameter\StringType;
   use Api\RoutesAPI;
   use Driver\SQL\Column\Column;
+  use Driver\SQL\Condition\Compare;
   use Driver\SQL\Condition\CondBool;
   use Driver\SQL\Condition\CondRegex;
+  use Objects\User;
 
   class Fetch extends RoutesAPI {
 
@@ -162,7 +202,7 @@ namespace Api\Routes {
       return $this->success;
     }
 
-    private function validateRoutes() {
+    private function validateRoutes(): bool {
 
       $this->routes = array();
       $keys = array(
@@ -173,10 +213,6 @@ namespace Api\Routes {
         "active" => Parameter::TYPE_BOOLEAN
       );
 
-      $actions = array(
-        "redirect_temporary", "redirect_permanently", "static", "dynamic"
-      );
-
       foreach($this->getParam("routes") as $index => $route) {
         foreach($keys as $key => $expectedType) {
           if (!array_key_exists($key, $route)) {
@@ -193,7 +229,7 @@ namespace Api\Routes {
         }
 
         $action = $route["action"];
-        if (!in_array($action, $actions)) {
+        if (!in_array($action, self::ACTIONS)) {
           return $this->createError("Invalid action: $action");
         }
 
@@ -213,5 +249,149 @@ namespace Api\Routes {
       return true;
     }
   }
+
+  class Add extends RoutesAPI {
+
+    public function __construct(User $user, bool $externalCall = false) {
+      parent::__construct($user, $externalCall, array(
+        "request" => new StringType("request", 128),
+        "action" => new StringType("action"),
+        "target" => new StringType("target", 128),
+        "extra"  => new StringType("extra", 64, true, ""),
+      ));
+      $this->isPublic = false;
+    }
+
+    public function execute($values = array()): bool {
+      if (!parent::execute($values)) {
+        return false;
+      }
+
+      $request = $this->formatRegex($this->getParam("request"), true);
+      $action = $this->getParam("action");
+      $target = $this->getParam("target");
+      $extra = $this->getParam("extra");
+
+      if (!in_array($action, self::ACTIONS)) {
+        return $this->createError("Invalid action: $action");
+      }
+
+      $sql = $this->user->getSQL();
+      $this->success = $sql->insert("Route", ["request", "action", "target", "extra"])
+        ->addRow($request, $action, $target, $extra)
+        ->execute();
+
+      $this->lastError = $sql->getLastError();
+      return $this->success;
+    }
+  }
+
+  class Update extends RoutesAPI {
+    public function __construct(User $user, bool $externalCall = false) {
+      parent::__construct($user, $externalCall, array(
+        "uid" => new Parameter("uid", Parameter::TYPE_INT),
+        "request" => new StringType("request", 128),
+        "action" => new StringType("action"),
+        "target" => new StringType("target", 128),
+        "extra"  => new StringType("extra", 64, true, ""),
+      ));
+      $this->isPublic = false;
+    }
+
+    public function execute($values = array()): bool {
+      if (!parent::execute($values)) {
+        return false;
+      }
+
+      $uid = $this->getParam("uid");
+      if (!$this->routeExists($uid)) {
+        return false;
+      }
+
+      $request = $this->formatRegex($this->getParam("request"), true);
+      $action = $this->getParam("action");
+      $target = $this->getParam("target");
+      $extra = $this->getParam("extra");
+      if (!in_array($action, self::ACTIONS)) {
+        return $this->createError("Invalid action: $action");
+      }
+
+      $sql = $this->user->getSQL();
+      $this->success = $sql->update("Route")
+        ->set("request", $request)
+        ->set("action", $action)
+        ->set("target", $target)
+        ->set("extra", $extra)
+        ->where(new Compare("uid", $uid))
+        ->execute();
+
+      $this->lastError = $sql->getLastError();
+      return $this->success;
+    }
+  }
+
+  class Remove extends RoutesAPI {
+    public function __construct(User $user, bool $externalCall = false) {
+      parent::__construct($user, $externalCall, array(
+        "uid" => new Parameter("uid", Parameter::TYPE_INT)
+      ));
+      $this->isPublic = false;
+    }
+
+    public function execute($values = array()): bool {
+      if (!parent::execute($values)) {
+        return false;
+      }
+
+      $uid = $this->getParam("uid");
+      if (!$this->routeExists($uid)) {
+        return false;
+      }
+
+      $sql = $this->user->getSQL();
+      $this->success = $sql->delete("Route")
+        ->where(new Compare("uid", $uid))
+        ->execute();
+
+      $this->lastError = $sql->getLastError();
+      return $this->success;
+    }
+  }
+
+  class Enable extends RoutesAPI {
+    public function __construct(User $user, bool $externalCall = false) {
+      parent::__construct($user, $externalCall, array(
+        "uid" => new Parameter("uid", Parameter::TYPE_INT)
+      ));
+      $this->isPublic = false;
+    }
+
+    public function execute($values = array()): bool {
+      if (!parent::execute($values)) {
+        return false;
+      }
+
+      $uid = $this->getParam("uid");
+      return $this->toggleRoute($uid, true);
+    }
+  }
+
+  class Disable extends RoutesAPI {
+    public function __construct(User $user, bool $externalCall = false) {
+      parent::__construct($user, $externalCall, array(
+        "uid" => new Parameter("uid", Parameter::TYPE_INT)
+      ));
+      $this->isPublic = false;
+    }
+
+    public function execute($values = array()): bool {
+      if (!parent::execute($values)) {
+        return false;
+      }
+
+      $uid = $this->getParam("uid");
+      return $this->toggleRoute($uid, false);
+    }
+  }
 }
 

+ 3 - 3
core/Configuration/CreateDatabase.class.php

@@ -126,7 +126,8 @@ class CreateDatabase extends DatabaseScript {
       ->addString("target", 128)
       ->addString("extra", 64, true)
       ->addBool("active", true)
-      ->primaryKey("uid");
+      ->primaryKey("uid")
+      ->unique("request");
 
     $queries[] = $sql->insert("Route", array("request", "action", "target", "extra"))
       ->addRow("^/admin(/.*)?$", "dynamic", "\\Documents\\Admin", NULL)
@@ -192,8 +193,7 @@ class CreateDatabase extends DatabaseScript {
       ->addRow("User/edit", array(USER_GROUP_ADMIN), "Allows users to edit details and group memberships of any user")
       ->addRow("User/delete", array(USER_GROUP_ADMIN), "Allows users to delete any other user")
       ->addRow("Permission/fetch", array(USER_GROUP_ADMIN), "Allows users to list all API permissions")
-      ->addRow("Visitors/stats", array(USER_GROUP_ADMIN, USER_GROUP_SUPPORT), "Allows users to see visitor statistics")
-      ->addRow("PatchSQL", array(USER_GROUP_ADMIN), "Allows users to import database patches");
+      ->addRow("Visitors/stats", array(USER_GROUP_ADMIN, USER_GROUP_SUPPORT), "Allows users to see visitor statistics");
 
     self::loadPatches($queries, $sql);
 

+ 1 - 1
core/Configuration/Settings.class.php

@@ -33,7 +33,7 @@ class Settings {
   }
 
   public static function loadDefaults(): Settings {
-    $hostname = $_SERVER["SERVER_NAME"];
+    $hostname = $_SERVER["SERVER_NAME"] ?? "localhost";
     $protocol = getProtocol();
     $jwt = generateRandomString(32);
 

+ 0 - 2
core/Documents/Install.class.php

@@ -280,9 +280,7 @@ namespace Documents\Install {
             $success = false;
             $msg = "Unable to write file";
           }
-        }
 
-        if ($sql) {
           $sql->close();
         }
       }

+ 1 - 1
core/Driver/SQL/PostgreSQL.class.php

@@ -53,7 +53,7 @@ class PostgreSQL extends SQL {
       }
     }
 
-    $this->connection = @pg_connect(implode(" ", $connectionString));
+    $this->connection = @pg_connect(implode(" ", $connectionString), PGSQL_CONNECT_FORCE_NEW);
     if (!$this->connection) {
       $this->lastError = "Failed to connect to Database";
       $this->connection = NULL;

+ 17 - 2
core/core.php

@@ -1,9 +1,24 @@
 <?php
 
-define("WEBBASE_VERSION", "1.2.3");
+define("WEBBASE_VERSION", "1.2.4");
+
+spl_autoload_extensions(".php");
+spl_autoload_register(function($class) {
+  $full_path = getClassPath($class);
+  if(file_exists($full_path)) {
+    include_once $full_path;
+  } else {
+    include_once getClassPath($class, false);
+  }
+});
+
 
 function getProtocol(): string {
-  return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) ? "https" : "http";
+  $isSecure = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ||
+              (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') ||
+              (!empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] == 'on');
+
+  return $isSecure ? 'https' : 'http';
 }
 
 function generateRandomString($length): string {

BIN
img/icons/logo.png


BIN
img/maintenance.png


+ 11 - 14
index.php

@@ -1,29 +1,26 @@
 <?php
 
+include_once 'core/core.php';
+include_once 'core/datetime.php';
+include_once 'core/constants.php';
+
+if (is_file("MAINTENANCE")) {
+  http_response_code(503);
+  $currentDir = dirname(__FILE__);
+  serveStatic($currentDir, "/static/maintenance.html");
+  die();
+}
+
 use Api\Request;
 use Configuration\Configuration;
 use Documents\Document404;
 use Elements\Document;
 
-include_once 'core/core.php';
-include_once 'core/datetime.php';
-include_once 'core/constants.php';
-
 if (!is_readable(getClassPath(Configuration::class))) {
   header("Content-Type: application/json");
   die(json_encode(array( "success" => false, "msg" => "Configuration directory is not readable, check permissions before proceeding." )));
 }
 
-spl_autoload_extensions(".php");
-spl_autoload_register(function($class) {
-  $full_path = getClassPath($class, true);
-  if(file_exists($full_path)) {
-    include_once $full_path;
-  } else {
-    include_once getClassPath($class, false);
-  }
-});
-
 $config = new Configuration();
 $user   = new Objects\User($config);
 $sql    = $user->getSQL();

+ 9 - 5
js/install.js

@@ -138,22 +138,26 @@ $(document).ready(function() {
   });
 
   // DATABASE PORT
-  let prevPort = $("#port").val();
-  let prevDbms = $("#type option:selected").val();
+  let portField = $("#port");
+  let typeField = $("#type");
+
+  let prevPort = parseInt(portField.val());
+  let prevDbms = typeField.find("option:selected").val();
   function updateDefaultPort() {
     let defaultPorts = {
       "mysql": 3306,
       "postgres": 5432
     };
 
-    let curDbms = $("#type option:selected").val();
+    let curDbms = typeField.find("option:selected").val();
     if(defaultPorts[prevDbms] === prevPort) {
-      $("#port").val(defaultPorts[curDbms]);
+      prevDbms = curDbms;
+      portField.val(prevPort = defaultPorts[curDbms]);
     }
   }
 
   updateDefaultPort();
-  $("#type").change(function() {
+  typeField.change(function() {
     updateDefaultPort();
   });
 });

+ 36 - 0
static/maintenance.html

@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<head>
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <meta name="format-detection" content="telephone=yes">
+  <meta charset="utf-8">
+  <meta http-equiv="expires" content="0">
+  <meta name="robots" content="noarchive">
+  <title>Maintenance</title>
+  <link rel="stylesheet" href="/css/bootstrap.min.css" type="text/css">
+  <script type="text/javascript" src="/js/bootstrap.bundle.min.js"></script>
+  <link rel="stylesheet" href="/css/fontawesome.min.css" type="text/css">
+</head>
+<body>
+  <div class="container mt-5">
+    <div class="row">
+      <div class="col-md-6">
+        <div class="error-template pt-5 pl-5">
+          <h1>Service unavailable</h1>
+          <h3>Temporarily down for maintenance</h3>
+          <div>
+            <p>
+              Sorry for the inconvenience but we're performing some maintenance at the moment.
+              we'll be back online shortly!
+            </p>
+          </div>
+          <a href="javascript:document.location.reload()" class="btn btn-info btn-lg mt-3">
+            <i class="fa fa-redo mr-2"></i>Retry
+          </a>
+        </div>
+      </div>
+      <div class="col-md-6 text-center pt-5">
+        <img src="/img/maintenance.png" alt="[maintenance]">
+      </div>
+    </div>
+  </div>
+</body>