From a51b427c2ed4fa1d2f7fb01fc2313c8ba70b3069 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 6 Apr 2021 16:34:12 +0200 Subject: [PATCH 01/12] CLI --- cli.php | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++ core/core.php | 11 ++++++ index.php | 10 ------ 3 files changed, 104 insertions(+), 10 deletions(-) create mode 100644 cli.php diff --git a/cli.php b/cli.php new file mode 100644 index 0000000..ea98dde --- /dev/null +++ b/cli.php @@ -0,0 +1,93 @@ +isConnected()) { + if ($db instanceof SQL) { + die($db->getLastError() . "\n"); + } else { + $msg = (is_string($db) ? $db : "Unknown Error"); + die("Database error: $msg\n"); + } + } + + return $db; +} + +function printHelp() { + +} + +function handleDatabase($argv) { + $action = $argv[2] ?? ""; + + switch ($action) { + case 'migrate': + $db = connectDatabase(); + break; + case 'dump': + $config = getDatabaseConfig(); + $output = $argv[3] ?? null; + $user = $config->getLogin(); + $password = $config->getPassword(); + $database = $config->getProperty("database"); + $command = ["mysqldump", "-u", $user, "--password=$password"]; + $descriptorSpec = [STDIN, STDOUT, STDOUT]; + + if ($database) { + $command[] = $database; + } + + if ($output) { + $descriptorSpec[1] = ["file", $output, "w"]; + } + + $process = proc_open($command, $descriptorSpec, $pipes); + proc_close($process); + break; + default: + die("Usage: cli.php db \n"); + } +} + +$argv = $_SERVER['argv']; +if (count($argv) < 2) { + die("Usage: cli.php [options...]\n"); +} + +$command = $argv[1]; +switch ($command) { + case 'help': + printHelp(); + exit; + case 'db': + handleDatabase($argv); + break; + case 'routes': + break; + default: + echo "Unknown command '$command'\n\n"; + printHelp(); + exit; +} \ No newline at end of file diff --git a/core/core.php b/core/core.php index 6f57d3c..04563cc 100644 --- a/core/core.php +++ b/core/core.php @@ -2,6 +2,17 @@ define("WEBBASE_VERSION", "1.2.3"); +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"; } diff --git a/index.php b/index.php index 294b30a..7830ffc 100644 --- a/index.php +++ b/index.php @@ -14,16 +14,6 @@ if (!is_readable(getClassPath(Configuration::class))) { 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(); From 8d408046ded31ba16d4569b4fc7d7d597aa3e504 Mon Sep 17 00:00:00 2001 From: Roman Hergenreder Date: Tue, 6 Apr 2021 17:39:21 +0200 Subject: [PATCH 02/12] install.js port fix --- js/install.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/js/install.js b/js/install.js index 174ffd3..64789c2 100644 --- a/js/install.js +++ b/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(); }); }); From b3bded23325154fbf6d828e2b3fd56331053b57b Mon Sep 17 00:00:00 2001 From: Roman Hergenreder Date: Tue, 6 Apr 2021 19:01:20 +0200 Subject: [PATCH 03/12] pg_dump --- cli.php | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/cli.php b/cli.php index ea98dde..ef8e15b 100644 --- a/cli.php +++ b/cli.php @@ -46,28 +46,46 @@ function handleDatabase($argv) { case 'migrate': $db = connectDatabase(); break; - case 'dump': + case 'export': $config = getDatabaseConfig(); - $output = $argv[3] ?? null; + $dbType = $config->getProperty("type") ?? null; $user = $config->getLogin(); $password = $config->getPassword(); $database = $config->getProperty("database"); - $command = ["mysqldump", "-u", $user, "--password=$password"]; + $host = $config->getHost(); + $port = $config->getPort(); + + $env = []; + $output = $argv[3] ?? null; $descriptorSpec = [STDIN, STDOUT, STDOUT]; - if ($database) { - $command[] = $database; + if ($dbType === "mysql") { + + $command = ["mysqldump", "-u", $user, '-h', $host, '-P', $port, "--password=$password"]; + if ($database) { + $command[] = $database; + } + + } else if ($dbType === "postgres") { + $command = ["pg_dump", "-U", $user, '-h', $host, '-p', $port]; + if ($database) { + $command[] = $database; + } + + $env["PGPASSWORD"] = $password; + } else { + die("Unsupported database type\n"); } if ($output) { $descriptorSpec[1] = ["file", $output, "w"]; } - $process = proc_open($command, $descriptorSpec, $pipes); + $process = proc_open($command, $descriptorSpec, $pipes, null, $env); proc_close($process); break; default: - die("Usage: cli.php db \n"); + die("Usage: cli.php db \n"); } } From 186083a3151dcaf86b76e843b18bbc9d5a117621 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 6 Apr 2021 20:31:52 +0200 Subject: [PATCH 04/12] psql fix --- cli.php | 28 +++++++++++++++++++++ core/Configuration/CreateDatabase.class.php | 3 ++- core/Documents/Install.class.php | 2 -- core/Driver/SQL/PostgreSQL.class.php | 2 +- 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/cli.php b/cli.php index ef8e15b..8b5c023 100644 --- a/cli.php +++ b/cli.php @@ -1,7 +1,9 @@ \n"); + } + + $class = str_replace('/', '\\', $class); + $className = "\\Configuration\\$class"; + $classPath = getClassPath($className); + if (!file_exists($classPath) || !is_readable($classPath)) { + die("Database script file does not exist or is not readable\n"); + } + + include_once $classPath; + $obj = new $className(); + if (!($obj instanceof DatabaseScript)) { + die("Not a database script\n"); + } + $db = connectDatabase(); + $queries = $obj->createQueries($db); + foreach ($queries as $query) { + if (!$query->execute($db)) { + die($db->getLastError()); + } + } + + $db->close(); break; case 'export': $config = getDatabaseConfig(); diff --git a/core/Configuration/CreateDatabase.class.php b/core/Configuration/CreateDatabase.class.php index 5de3e77..314a719 100755 --- a/core/Configuration/CreateDatabase.class.php +++ b/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) diff --git a/core/Documents/Install.class.php b/core/Documents/Install.class.php index d873da5..861ded5 100644 --- a/core/Documents/Install.class.php +++ b/core/Documents/Install.class.php @@ -280,9 +280,7 @@ namespace Documents\Install { $success = false; $msg = "Unable to write file"; } - } - if ($sql) { $sql->close(); } } diff --git a/core/Driver/SQL/PostgreSQL.class.php b/core/Driver/SQL/PostgreSQL.class.php index e3e8794..d5cc4cf 100644 --- a/core/Driver/SQL/PostgreSQL.class.php +++ b/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; From ebdece7144eef05fa65f2e412445928f0ab1be89 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 6 Apr 2021 20:59:55 +0200 Subject: [PATCH 05/12] patch sql -> cli --- README.md | 4 +- cli.php | 157 ++++++++++++-------- core/Api/PatchSQL.class.php | 65 -------- core/Configuration/CreateDatabase.class.php | 3 +- 4 files changed, 102 insertions(+), 127 deletions(-) delete mode 100644 core/Api/PatchSQL.class.php diff --git a/README.md b/README.md index 90fef4e..e5d5709 100644 --- a/README.md +++ b/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 diff --git a/cli.php b/cli.php index 8b5c023..69ac2f1 100644 --- a/cli.php +++ b/cli.php @@ -44,76 +44,117 @@ function printHelp() { function handleDatabase($argv) { $action = $argv[2] ?? ""; - switch ($action) { - case 'migrate': - $class = $argv[3] ?? null; - if (!$class) { - die("Usage: cli.php db migrate \n"); + if ($action === "migrate") { + $class = $argv[3] ?? null; + if (!$class) { + die("Usage: cli.php db migrate \n"); + } + + $class = str_replace('/', '\\', $class); + $className = "\\Configuration\\$class"; + $classPath = getClassPath($className); + if (!file_exists($classPath) || !is_readable($classPath)) { + die("Database script file does not exist or is not readable\n"); + } + + include_once $classPath; + $obj = new $className(); + if (!($obj instanceof DatabaseScript)) { + die("Not a database script\n"); + } + + $db = connectDatabase(); + $queries = $obj->createQueries($db); + foreach ($queries as $query) { + if (!$query->execute($db)) { + die($db->getLastError()); + } + } + + $db->close(); + } 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) { + die("Usage: cli.php db import \n"); } - $class = str_replace('/', '\\', $class); - $className = "\\Configuration\\$class"; - $classPath = getClassPath($className); - if (!file_exists($classPath) || !is_readable($classPath)) { - die("Database script file does not exist or is not readable\n"); + if (!file_exists($file) || !is_readable($file)) { + die("File not found or not readable\n"); } - include_once $classPath; - $obj = new $className(); - if (!($obj instanceof DatabaseScript)) { - die("Not a database script\n"); - } + $inputData = file_get_contents($file); + } - $db = connectDatabase(); - $queries = $obj->createQueries($db); - foreach ($queries as $query) { - if (!$query->execute($db)) { - die($db->getLastError()); + 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"; } - } - - $db->close(); - break; - case 'export': - $config = getDatabaseConfig(); - $dbType = $config->getProperty("type") ?? null; - $user = $config->getLogin(); - $password = $config->getPassword(); - $database = $config->getProperty("database"); - $host = $config->getHost(); - $port = $config->getPort(); - - $env = []; - $output = $argv[3] ?? null; - $descriptorSpec = [STDIN, STDOUT, STDOUT]; - - if ($dbType === "mysql") { - - $command = ["mysqldump", "-u", $user, '-h', $host, '-P', $port, "--password=$password"]; - if ($database) { - $command[] = $database; - } - - } else if ($dbType === "postgres") { - $command = ["pg_dump", "-U", $user, '-h', $host, '-p', $port]; - if ($database) { - $command[] = $database; - } - - $env["PGPASSWORD"] = $password; + } else if ($action === "import") { + $command_bin = "mysql"; + $descriptorSpec[0] = ["pipe", "r"]; } else { - die("Unsupported database type\n"); + die("Unsupported action\n"); + } + } 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 { + die("Unsupported action\n"); } - if ($output) { - $descriptorSpec[1] = ["file", $output, "w"]; + } else { + die("Unsupported database type\n"); + } + + 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]); } - $process = proc_open($command, $descriptorSpec, $pipes, null, $env); proc_close($process); - break; - default: - die("Usage: cli.php db \n"); + } } } diff --git a/core/Api/PatchSQL.class.php b/core/Api/PatchSQL.class.php deleted file mode 100644 index 3c12800..0000000 --- a/core/Api/PatchSQL.class.php +++ /dev/null @@ -1,65 +0,0 @@ - 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; - } -} \ No newline at end of file diff --git a/core/Configuration/CreateDatabase.class.php b/core/Configuration/CreateDatabase.class.php index 314a719..1c8af14 100755 --- a/core/Configuration/CreateDatabase.class.php +++ b/core/Configuration/CreateDatabase.class.php @@ -193,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); From 536cae7a902a1670202a565086c58584f64939ed Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 6 Apr 2021 23:05:02 +0200 Subject: [PATCH 06/12] maintenance --- cli.php | 31 ++++++++++++++++++++++++++++++- img/maintenance.png | Bin 0 -> 47633 bytes index.php | 15 +++++++++++---- static/maintenance.html | 36 ++++++++++++++++++++++++++++++++++++ 4 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 img/maintenance.png create mode 100644 static/maintenance.html diff --git a/cli.php b/cli.php index 69ac2f1..8e239d1 100644 --- a/cli.php +++ b/cli.php @@ -38,7 +38,7 @@ function connectDatabase() { } function printHelp() { - + // TODO: help } function handleDatabase($argv) { @@ -155,6 +155,31 @@ function handleDatabase($argv) { proc_close($process); } + } else { + die("Usage: cli.php db [options...]"); + } +} + +function onMaintenance($argv) { + $action = $argv[2] ?? "status"; + $maintenanceFile = "MAINTENANCE"; + $isMaintenanceEnabled = file_exists($maintenanceFile); + + if ($action === "status") { + die("Maintenance: " . ($isMaintenanceEnabled ? "on" : "off") . "\n"); + } else if ($action === "on") { + $file = fopen($maintenanceFile, 'w') or die("Unable to create maintenance file\n"); + fclose($file); + die("Maintenance enabled\n"); + } else if ($action === "off") { + if (file_exists($maintenanceFile)) { + if (!unlink($maintenanceFile)) { + die("Unable to delete maintenance file\n"); + } + } + die("Maintenance disabled\n"); + } else { + die("Usage: cli.php maintenance \n"); } } @@ -172,6 +197,10 @@ switch ($command) { handleDatabase($argv); break; case 'routes': + // TODO: routes + break; + case 'maintenance': + onMaintenance($argv); break; default: echo "Unknown command '$command'\n\n"; diff --git a/img/maintenance.png b/img/maintenance.png new file mode 100644 index 0000000000000000000000000000000000000000..7ca3c0345efcf492252eb2e7dadaa4440832d2d5 GIT binary patch literal 47633 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 z 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 false, "msg" => "Configuration directory is not readable, check permissions before proceeding." ))); diff --git a/static/maintenance.html b/static/maintenance.html new file mode 100644 index 0000000..d954309 --- /dev/null +++ b/static/maintenance.html @@ -0,0 +1,36 @@ + + + + + + + + Maintenance + + + + + +
+
+
+
+

Service unavailable

+

Temporarily down for maintenance

+
+

+ Sorry for the inconvenience but we're performing some maintenance at the moment. + we'll be back online shortly! +

+
+ + Retry + +
+
+
+ [maintenance] +
+
+
+ \ No newline at end of file From 43f44072696fd3319390d0d9caae6ea4648f80aa Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 6 Apr 2021 23:07:01 +0200 Subject: [PATCH 07/12] removed exif data --- img/icons/logo.png | Bin 3475 -> 2998 bytes img/maintenance.png | Bin 47633 -> 47516 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/img/icons/logo.png b/img/icons/logo.png index 1de1b6f7c948deb22a372d8aa116948f56a565dc..832372e32e31fc284bf33e57dc732249bae513b6 100644 GIT binary patch delta 15 WcmbO%y-j?A^5o5oLYsw{8o2>176jV> delta 491 zcmVEX>4Tx04R}tkvmAkKpe)urfNm1B4!YA z$WWauh%X$q3Pq?8YK2xE%%d-8(vYOMI0~)>2Oo=72N!2u9b5%L@B!lF=%nZ(CH^ld zw21NGxF6r$_i^_fz|~5MX0?q0nr@rPcuYuVR)x?jI?#hYOn-rw6w8jFh2gBevdnavLx^J$OAsMI zMg>KbU?WPaPJfDpB<;t1{DZDvB$rICA{25gpbQO?>j(RT-`!fdiE%F}90S^49Oq*g z2<-y(s^fegJ5K!s2tET>dedL70W+VZSDRYo2RdP`(kYX@0 zFf!CNFwr$I3o$aZGBC0-G1WFOure_4H|nZkU|>LK$jwj5OsmAL!7ktE08j&ir>mdK II;Vst0H9eXr2qf` From af27b7c30252779767981b8f97b0a0b8b4400aed Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 7 Apr 2021 00:03:14 +0200 Subject: [PATCH 08/12] some fixes --- cli.php | 204 ++++++++++++++++++++------ core/Api/Request.class.php | 14 +- core/Configuration/Settings.class.php | 2 +- core/core.php | 6 +- 4 files changed, 176 insertions(+), 50 deletions(-) diff --git a/cli.php b/cli.php index 8e239d1..cf59b84 100644 --- a/cli.php +++ b/cli.php @@ -3,75 +3,79 @@ include_once 'core/core.php'; include_once 'core/constants.php'; +use Configuration\Configuration; use Configuration\DatabaseScript; -use Driver\SQL\SQL; 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") { - die(); + _exit("Can only be executed via CLI"); } function getDatabaseConfig(): ConnectionData { $configClass = "\\Configuration\\Database"; $file = getClassPath($configClass); if (!file_exists($file) || !is_readable($file)) { - die("Database configuration does not exist or is not readable\n"); + _exit("Database configuration does not exist or is not readable"); } include_once $file; return new $configClass(); } -function connectDatabase() { - $config = getDatabaseConfig(); - $db = SQL::createConnection($config); - if (!($db instanceof SQL) || !$db->isConnected()) { - if ($db instanceof SQL) { - die($db->getLastError() . "\n"); - } else { - $msg = (is_string($db) ? $db : "Unknown Error"); - die("Database error: $msg\n"); - } +function getUser(): User { + $config = new Configuration(); + $user = new User($config); + if (!$user->getSQL() || !$user->getSQL()->isConnected()) { + _exit("Could not establish database connection"); } - return $db; + return $user; } function printHelp() { // TODO: help } -function handleDatabase($argv) { +function handleDatabase(array $argv) { $action = $argv[2] ?? ""; if ($action === "migrate") { $class = $argv[3] ?? null; if (!$class) { - die("Usage: cli.php db migrate \n"); + _exit("Usage: cli.php db migrate "); } $class = str_replace('/', '\\', $class); $className = "\\Configuration\\$class"; $classPath = getClassPath($className); if (!file_exists($classPath) || !is_readable($classPath)) { - die("Database script file does not exist or is not readable\n"); + _exit("Database script file does not exist or is not readable"); } include_once $classPath; $obj = new $className(); if (!($obj instanceof DatabaseScript)) { - die("Not a database script\n"); + _exit("Not a database script"); } - $db = connectDatabase(); - $queries = $obj->createQueries($db); + $user = getUser(); + $sql = $user->getSQL(); + $queries = $obj->createQueries($sql); foreach ($queries as $query) { - if (!$query->execute($db)) { - die($db->getLastError()); + if (!$query->execute($sql)) { + _exit($sql->getLastError()); } } - - $db->close(); } else if ($action === "export" || $action === "import") { // database config @@ -94,11 +98,11 @@ function handleDatabase($argv) { if ($action === "import") { $file = $argv[3] ?? null; if (!$file) { - die("Usage: cli.php db import \n"); + _exit("Usage: cli.php db import "); } if (!file_exists($file) || !is_readable($file)) { - die("File not found or not readable\n"); + _exit("File not found or not readable"); } $inputData = file_get_contents($file); @@ -116,8 +120,6 @@ function handleDatabase($argv) { } else if ($action === "import") { $command_bin = "mysql"; $descriptorSpec[0] = ["pipe", "r"]; - } else { - die("Unsupported action\n"); } } else if ($dbType === "postgres") { @@ -132,12 +134,10 @@ function handleDatabase($argv) { } else if ($action === "import") { $command_bin = "/usr/bin/psql"; $descriptorSpec[0] = ["pipe", "r"]; - } else { - die("Unsupported action\n"); } } else { - die("Unsupported database type\n"); + _exit("Unsupported database type"); } if ($database) { @@ -156,36 +156,155 @@ function handleDatabase($argv) { proc_close($process); } } else { - die("Usage: cli.php db [options...]"); + _exit("Usage: cli.php db [options...]"); } } -function onMaintenance($argv) { +function onMaintenance(array $argv) { $action = $argv[2] ?? "status"; $maintenanceFile = "MAINTENANCE"; $isMaintenanceEnabled = file_exists($maintenanceFile); if ($action === "status") { - die("Maintenance: " . ($isMaintenanceEnabled ? "on" : "off") . "\n"); + _exit("Maintenance: " . ($isMaintenanceEnabled ? "on" : "off")); } else if ($action === "on") { - $file = fopen($maintenanceFile, 'w') or die("Unable to create maintenance file\n"); + $file = fopen($maintenanceFile, 'w') or _exit("Unable to create maintenance file"); fclose($file); - die("Maintenance enabled\n"); + _exit("Maintenance enabled"); } else if ($action === "off") { if (file_exists($maintenanceFile)) { if (!unlink($maintenanceFile)) { - die("Unable to delete maintenance file\n"); + _exit("Unable to delete maintenance file"); } } - die("Maintenance disabled\n"); + _exit("Maintenance disabled"); } else { - die("Usage: cli.php maintenance \n"); + _exit("Usage: cli.php maintenance "); + } +} + +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 [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 [extra]"); + } else { + _exit("Usage: cli.php routes $action "); + } + } + + $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 [options...]"); } } $argv = $_SERVER['argv']; if (count($argv) < 2) { - die("Usage: cli.php [options...]\n"); + _exit("Usage: cli.php [options...]"); } $command = $argv[1]; @@ -197,13 +316,14 @@ switch ($command) { handleDatabase($argv); break; case 'routes': - // TODO: routes + onRoutes($argv); break; case 'maintenance': onMaintenance($argv); break; default: - echo "Unknown command '$command'\n\n"; + printLine("Unknown command '$command'"); + printLine(); printHelp(); exit; } \ No newline at end of file diff --git a/core/Api/Request.class.php b/core/Api/Request.class.php index c5b8838..4e42e6c 100644 --- a/core/Api/Request.class.php +++ b/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? diff --git a/core/Configuration/Settings.class.php b/core/Configuration/Settings.class.php index 7a981aa..0418823 100644 --- a/core/Configuration/Settings.class.php +++ b/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); diff --git a/core/core.php b/core/core.php index 04563cc..ff01d87 100644 --- a/core/core.php +++ b/core/core.php @@ -14,7 +14,11 @@ spl_autoload_register(function($class) { 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 { From c339ae0584f19616132329172e1472c7bc16942d Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 7 Apr 2021 00:55:53 +0200 Subject: [PATCH 09/12] cli.php update --- cli.php | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/cli.php b/cli.php index cf59b84..66808cb 100644 --- a/cli.php +++ b/cli.php @@ -178,8 +178,63 @@ function onMaintenance(array $argv) { } } _exit("Maintenance disabled"); + } else if ($action === "update") { + + // TODO: find that dynamically + $pullBranch = "root/master"; + $pushBranch = "origin/master"; + + printLine("$ git fetch " . str_replace("/", " ", $pullBranch)); + exec("git fetch " . str_replace("/", " ", $pullBranch), $gitFetch, $ret); + if ($ret !== 0) { + _exit(implode(PHP_EOL, $gitFetch)); + } + + printLine("$ git log HEAD..$pullBranch --oneline"); + exec("git log HEAD..$pullBranch --oneline", $gitLog, $ret); + if ($ret !== 0) { + _exit(implode(PHP_EOL, $gitLog)); + } 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."); + } + + printLine("$ git rev-list HEAD...$pushBranch --ignore-submodules --count"); + exec("git rev-list HEAD...$pushBranch --ignore-submodules --count", $gitRevList, $ret); + if ($ret !== 0) { + _exit("HEAD is behind your local branch. Please push your commits before updating."); + } + + // enable maintenance mode if it wasn't turned on before + if (!$isMaintenanceEnabled) { + printLine("Turning on maintenance mode"); + onMaintenance(["cli.php", "maintenance", "on"]); + } + + printLine("Ready to update, pulling and merging"); + printLine("$ git pull $pullBranch"); + exec("git pull $pullBranch", $gitPull, $ret); + if ($ret !== 0) { + printLine("Update could not be applied, check the following git message."); + printLine("Follow the instructions and afterwards turn off the maintenance mode again using:"); + printLine("cli.php maintenance off"); + printLine(); + _exit(implode(PHP_EOL, $gitLog)); + } + + // disable maintenance mode again + if (!$isMaintenanceEnabled) { + printLine("Turning off maintenance mode"); + onMaintenance(["cli.php", "maintenance", "off"]); + } } else { - _exit("Usage: cli.php maintenance "); + _exit("Usage: cli.php maintenance "); } } From 1a3c4e44f6754bf29d4befecab13bc616ede0450 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 7 Apr 2021 01:00:52 +0200 Subject: [PATCH 10/12] update mechanism --- cli.php | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/cli.php b/cli.php index 66808cb..8c32a7e 100644 --- a/cli.php +++ b/cli.php @@ -187,13 +187,13 @@ function onMaintenance(array $argv) { printLine("$ git fetch " . str_replace("/", " ", $pullBranch)); exec("git fetch " . str_replace("/", " ", $pullBranch), $gitFetch, $ret); if ($ret !== 0) { - _exit(implode(PHP_EOL, $gitFetch)); + die(); } printLine("$ git log HEAD..$pullBranch --oneline"); exec("git log HEAD..$pullBranch --oneline", $gitLog, $ret); if ($ret !== 0) { - _exit(implode(PHP_EOL, $gitLog)); + die(); } else if (count($gitLog) === 0) { _exit("Already up to date."); } @@ -214,24 +214,29 @@ function onMaintenance(array $argv) { // enable maintenance mode if it wasn't turned on before if (!$isMaintenanceEnabled) { printLine("Turning on maintenance mode"); - onMaintenance(["cli.php", "maintenance", "on"]); + $file = fopen($maintenanceFile, 'w') or _exit("Unable to create maintenance file"); + fclose($file); } printLine("Ready to update, pulling and merging"); - printLine("$ git pull $pullBranch"); - exec("git pull $pullBranch", $gitPull, $ret); + 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 following git message."); printLine("Follow the instructions and afterwards turn off the maintenance mode again using:"); printLine("cli.php maintenance off"); - printLine(); - _exit(implode(PHP_EOL, $gitLog)); + die(); } // disable maintenance mode again if (!$isMaintenanceEnabled) { printLine("Turning off maintenance mode"); - onMaintenance(["cli.php", "maintenance", "off"]); + if (file_exists($maintenanceFile)) { + if (!unlink($maintenanceFile)) { + _exit("Unable to delete maintenance file"); + } + } } } else { _exit("Usage: cli.php maintenance "); From ae99ef8a09e9833485df09833e8b5693ec8e5739 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 7 Apr 2021 12:57:00 +0200 Subject: [PATCH 11/12] Routes CLI --- cli.php | 15 ++- core/Api/RoutesAPI.class.php | 192 +++++++++++++++++++++++++++++++++-- 2 files changed, 192 insertions(+), 15 deletions(-) diff --git a/cli.php b/cli.php index 8c32a7e..73de6f8 100644 --- a/cli.php +++ b/cli.php @@ -223,7 +223,7 @@ function onMaintenance(array $argv) { exec("git pull " . str_replace("/", " ", $pullBranch) . " --no-ff", $gitPull, $ret); if ($ret !== 0) { printLine(); - printLine("Update could not be applied, check the following git message."); + 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(); @@ -306,14 +306,13 @@ function onRoutes(array $argv) { "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)) { @@ -334,13 +333,13 @@ function onRoutes(array $argv) { echo "Remove route #$uid? (y|n): "; } while(($input = trim(fgets(STDIN))) !== "y"); - // $req = new Api\Routes\Remove($user); + $req = new Api\Routes\Remove($user); } else if ($action === "enable") { - // $req = new Api\Routes\Enable($user); + $req = new Api\Routes\Enable($user); } else if ($action === "disable") { - // $req = new Api\Routes\Disable($user); + $req = new Api\Routes\Disable($user); } else if ($action === "modify") { - // $req = new Api\Routes\Update($user); + $req = new Api\Routes\Update($user); $params["request"] = $argv[4]; $params["action"] = $argv[5]; $params["target"] = $argv[6]; @@ -349,14 +348,12 @@ function onRoutes(array $argv) { _exit("Unsupported action"); } - /* $success = $req->execute($params); if (!$success) { _exit($req->getLastError()); } else { _exit("Route updated successfully"); } - */ } else { _exit("Usage: cli.php routes [options...]"); } diff --git a/core/Api/RoutesAPI.class.php b/core/Api/RoutesAPI.class.php index 3247d42..da96438 100644 --- a/core/Api/RoutesAPI.class.php +++ b/core/Api/RoutesAPI.class.php @@ -1,8 +1,13 @@ 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); + } + } } From e91edfeb1e62935a57140f38f949a1afd95f20ea Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 7 Apr 2021 13:09:50 +0200 Subject: [PATCH 12/12] CLI + version 1.2.4 --- cli.php | 42 +++++++++++++++++++++++++++++++++--------- core/core.php | 2 +- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/cli.php b/cli.php index 73de6f8..6439613 100644 --- a/cli.php +++ b/cli.php @@ -160,6 +160,24 @@ function handleDatabase(array $argv) { } } +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"; @@ -180,9 +198,21 @@ function onMaintenance(array $argv) { _exit("Maintenance disabled"); } else if ($action === "update") { - // TODO: find that dynamically - $pullBranch = "root/master"; - $pushBranch = "origin/master"; + 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); @@ -205,12 +235,6 @@ function onMaintenance(array $argv) { _exit("You have uncommitted changes. Please commit them before updating."); } - printLine("$ git rev-list HEAD...$pushBranch --ignore-submodules --count"); - exec("git rev-list HEAD...$pushBranch --ignore-submodules --count", $gitRevList, $ret); - if ($ret !== 0) { - _exit("HEAD is behind your local branch. Please push your commits before updating."); - } - // enable maintenance mode if it wasn't turned on before if (!$isMaintenanceEnabled) { printLine("Turning on maintenance mode"); diff --git a/core/core.php b/core/core.php index ff01d87..c41d07e 100644 --- a/core/core.php +++ b/core/core.php @@ -1,6 +1,6 @@