2024-03-27 15:15:46 +01:00
|
|
|
#!/usr/bin/php
|
2021-04-06 16:34:12 +02:00
|
|
|
<?php
|
|
|
|
|
2021-11-11 14:25:26 +01:00
|
|
|
define('WEBROOT', realpath("."));
|
|
|
|
|
2022-11-18 18:06:46 +01:00
|
|
|
include_once 'Core/core.php';
|
|
|
|
require_once 'Core/datetime.php';
|
|
|
|
include_once 'Core/constants.php';
|
|
|
|
|
2024-04-04 12:46:58 +02:00
|
|
|
use Core\API\Request;
|
2022-11-18 18:06:46 +01:00
|
|
|
use Core\Driver\SQL\Column\Column;
|
|
|
|
use Core\Driver\SQL\Condition\Compare;
|
|
|
|
use Core\Driver\SQL\Condition\CondIn;
|
|
|
|
use Core\Driver\SQL\Expression\DateSub;
|
|
|
|
use Core\Driver\SQL\SQL;
|
2024-05-09 13:42:39 +02:00
|
|
|
use Core\Objects\Context;
|
2022-11-18 18:06:46 +01:00
|
|
|
use Core\Objects\ConnectionData;
|
2024-03-27 13:05:37 +01:00
|
|
|
|
|
|
|
// TODO: is this available in all installations?
|
2024-03-24 17:36:16 +01:00
|
|
|
use JetBrains\PhpStorm\NoReturn;
|
2021-04-06 16:34:12 +02:00
|
|
|
|
2024-03-24 17:36:16 +01:00
|
|
|
function printLine(string $line = ""): void {
|
2021-04-07 00:03:14 +02:00
|
|
|
echo $line . PHP_EOL;
|
|
|
|
}
|
|
|
|
|
2024-03-24 17:36:16 +01:00
|
|
|
#[NoReturn]
|
|
|
|
function _exit(string $line = ""): void {
|
2021-04-07 00:03:14 +02:00
|
|
|
printLine($line);
|
2021-04-06 16:34:12 +02:00
|
|
|
die();
|
|
|
|
}
|
|
|
|
|
|
|
|
function getDatabaseConfig(): ConnectionData {
|
2022-11-18 18:54:22 +01:00
|
|
|
$configClass = "\\Site\\Configuration\\Database";
|
2021-04-06 16:34:12 +02:00
|
|
|
$file = getClassPath($configClass);
|
|
|
|
if (!file_exists($file) || !is_readable($file)) {
|
2021-04-07 00:03:14 +02:00
|
|
|
_exit("Database configuration does not exist or is not readable");
|
2021-04-06 16:34:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
include_once $file;
|
|
|
|
return new $configClass();
|
|
|
|
}
|
|
|
|
|
2024-05-09 13:42:39 +02:00
|
|
|
$context = Context::instance();
|
2022-06-20 19:52:31 +02:00
|
|
|
if (!$context->isCLI()) {
|
|
|
|
_exit("Can only be executed via CLI");
|
|
|
|
}
|
|
|
|
|
2024-03-27 13:05:37 +01:00
|
|
|
$dockerYaml = null;
|
2022-06-20 19:52:31 +02:00
|
|
|
$database = $context->getConfig()->getDatabase();
|
2024-03-27 14:12:01 +01:00
|
|
|
if ($database !== null) {
|
|
|
|
if ($database->getProperty("isDocker", false) && !is_file("/.dockerenv")) {
|
|
|
|
if (function_exists("yaml_parse")) {
|
|
|
|
$dockerYaml = yaml_parse(file_get_contents("./docker-compose.yml"));
|
|
|
|
} else {
|
|
|
|
_exit("yaml_parse not found but required for docker file parsing.");
|
|
|
|
}
|
2023-01-16 21:47:23 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-20 19:52:31 +02:00
|
|
|
function connectSQL(): ?SQL {
|
|
|
|
global $context;
|
|
|
|
$sql = $context->initSQL();
|
|
|
|
if (!$sql || !$sql->isConnected()) {
|
|
|
|
printLine("Could not establish database connection");
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $sql;
|
2021-04-06 16:34:12 +02:00
|
|
|
}
|
|
|
|
|
2024-03-24 17:36:16 +01:00
|
|
|
function printHelp(array $argv): void {
|
2024-04-04 12:46:58 +02:00
|
|
|
global $registeredCommands;
|
2024-03-24 17:36:16 +01:00
|
|
|
printLine("=== WebBase CLI tool ===");
|
2024-04-04 12:46:58 +02:00
|
|
|
printLine("Usage: " . $argv[0] . " [action] <args>");
|
|
|
|
foreach ($registeredCommands as $command => $data) {
|
|
|
|
$description = $data["description"] ?? "";
|
|
|
|
printLine(" - $command: $description");
|
|
|
|
}
|
2021-04-06 16:34:12 +02:00
|
|
|
}
|
|
|
|
|
2024-05-04 15:07:24 +02:00
|
|
|
function applyPatch(SQL $sql, string $patchFile): bool {
|
|
|
|
$queries = [];
|
|
|
|
@include_once $patchFile;
|
2021-04-09 12:37:24 +02:00
|
|
|
foreach ($queries as $query) {
|
|
|
|
if (!$query->execute($sql)) {
|
|
|
|
printLine($sql->getLastError());
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2024-03-24 17:36:16 +01:00
|
|
|
function handleDatabase(array $argv): void {
|
2023-01-16 21:47:23 +01:00
|
|
|
global $dockerYaml;
|
2021-04-06 16:34:12 +02:00
|
|
|
$action = $argv[2] ?? "";
|
|
|
|
|
2021-04-06 20:59:55 +02:00
|
|
|
if ($action === "migrate") {
|
2024-05-11 16:12:15 +02:00
|
|
|
$fileName = $argv[3] ?? "";
|
|
|
|
if (empty($fileName)) {
|
|
|
|
_exit("Usage: cli.php db migrate <file>");
|
|
|
|
}
|
|
|
|
|
|
|
|
$filePath = realpath($fileName);
|
|
|
|
if (!$filePath) {
|
|
|
|
_exit("File not found: $fileName");
|
|
|
|
}
|
|
|
|
|
|
|
|
$corePatches = implode(DIRECTORY_SEPARATOR, [WEBROOT, "Core", "Configuration", "Patch", ""]);
|
|
|
|
$sitePatches = implode(DIRECTORY_SEPARATOR, [WEBROOT, "Site", "Configuration", "Patch", ""]);
|
|
|
|
if (!endsWith($filePath, ".php") || (!startsWith($filePath, $corePatches) && !startsWith($filePath, $sitePatches))) {
|
|
|
|
_exit("invalid patch file: $filePath. Must be located in either Core or Site patch folder and have '.php' as extension");
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-06-20 19:52:31 +02:00
|
|
|
$sql = connectSQL() or die();
|
2024-05-11 16:12:15 +02:00
|
|
|
$queries = [];
|
|
|
|
@include_once $filePath;
|
|
|
|
|
|
|
|
if (empty($queries)) {
|
|
|
|
_exit("No queries loaded.");
|
|
|
|
}
|
|
|
|
|
|
|
|
$success = true;
|
|
|
|
$queryCount = count($queries);
|
|
|
|
$logger = new \Core\Driver\Logger\Logger("CLI", $sql);
|
|
|
|
$logger->info("Migrating DB with: " . $fileName);
|
|
|
|
printLine("Executing $queryCount queries");
|
|
|
|
|
|
|
|
$sql->startTransaction();
|
|
|
|
$queryIndex = 1;
|
|
|
|
foreach ($queries as $query) {
|
|
|
|
if ($query->execute() === false) {
|
|
|
|
$success = false;
|
|
|
|
printLine("Error executing query: " . $sql->getLastError());
|
|
|
|
$logger->error("Error while migrating db: " . $sql->getLastError());
|
|
|
|
$sql->rollback();
|
|
|
|
break;
|
|
|
|
} else {
|
|
|
|
printLine("$queryIndex/$queryCount: success!");
|
|
|
|
$queryIndex++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($success) {
|
|
|
|
$sql->commit();
|
|
|
|
}
|
|
|
|
|
|
|
|
printLine("Done.");
|
2022-05-31 16:14:49 +02:00
|
|
|
} else if (in_array($action, ["export", "import", "shell"])) {
|
2021-04-06 20:59:55 +02:00
|
|
|
|
|
|
|
// database config
|
2022-02-21 15:57:15 +01:00
|
|
|
$config = getDatabaseConfig();
|
|
|
|
$dbType = $config->getProperty("type") ?? null;
|
|
|
|
$user = $config->getLogin();
|
|
|
|
$password = $config->getPassword();
|
|
|
|
$database = $config->getProperty("database");
|
|
|
|
$host = $config->getHost();
|
|
|
|
$port = $config->getPort();
|
2021-04-06 20:59:55 +02:00
|
|
|
|
|
|
|
// 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) {
|
2021-04-07 00:03:14 +02:00
|
|
|
_exit("Usage: cli.php db import <path>");
|
2021-04-06 20:31:52 +02:00
|
|
|
}
|
|
|
|
|
2021-04-06 20:59:55 +02:00
|
|
|
if (!file_exists($file) || !is_readable($file)) {
|
2021-04-07 00:03:14 +02:00
|
|
|
_exit("File not found or not readable");
|
2021-04-06 20:31:52 +02:00
|
|
|
}
|
|
|
|
|
2021-04-06 20:59:55 +02:00
|
|
|
$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";
|
2021-04-06 20:31:52 +02:00
|
|
|
}
|
2021-04-06 20:59:55 +02:00
|
|
|
} else if ($action === "import") {
|
|
|
|
$command_bin = "mysql";
|
|
|
|
$descriptorSpec[0] = ["pipe", "r"];
|
2022-05-31 16:14:49 +02:00
|
|
|
} else if ($action === "shell") {
|
|
|
|
$command_bin = "mysql";
|
|
|
|
$descriptorSpec = [];
|
2021-04-06 20:31:52 +02:00
|
|
|
}
|
2021-04-06 20:59:55 +02:00
|
|
|
} else if ($dbType === "postgres") {
|
2021-04-06 20:31:52 +02:00
|
|
|
|
2021-04-06 20:59:55 +02:00
|
|
|
$env["PGPASSWORD"] = $password;
|
|
|
|
$command_args = ["-U", $user, '-h', $host, '-p', $port];
|
2021-04-06 19:01:20 +02:00
|
|
|
|
2021-04-06 20:59:55 +02:00
|
|
|
if ($action === "export") {
|
|
|
|
$command_bin = "/usr/bin/pg_dump";
|
|
|
|
if ($dataOnly) {
|
|
|
|
$command_args[] = "--data-only";
|
2021-04-06 19:01:20 +02:00
|
|
|
}
|
2021-04-06 20:59:55 +02:00
|
|
|
} else if ($action === "import") {
|
|
|
|
$command_bin = "/usr/bin/psql";
|
|
|
|
$descriptorSpec[0] = ["pipe", "r"];
|
2022-05-31 16:14:49 +02:00
|
|
|
} else if ($action === "shell") {
|
|
|
|
$command_bin = "/usr/bin/psql";
|
|
|
|
$descriptorSpec = [];
|
2021-04-06 16:34:12 +02:00
|
|
|
}
|
|
|
|
|
2021-04-06 20:59:55 +02:00
|
|
|
} else {
|
2021-04-07 00:03:14 +02:00
|
|
|
_exit("Unsupported database type");
|
2021-04-06 20:59:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($database) {
|
|
|
|
$command_args[] = $database;
|
|
|
|
}
|
|
|
|
|
|
|
|
$command = array_merge([$command_bin], $command_args);
|
2022-02-21 15:57:15 +01:00
|
|
|
if ($config->getProperty("isDocker", false)) {
|
2023-01-09 17:32:14 +01:00
|
|
|
$containerName = $dockerYaml["services"]["db"]["container_name"];
|
|
|
|
$command = array_merge(["docker", "exec", "-it", $containerName], $command);
|
2022-02-21 15:54:37 +01:00
|
|
|
}
|
|
|
|
|
2021-04-06 20:59:55 +02:00
|
|
|
$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]);
|
2021-04-06 16:34:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
proc_close($process);
|
2021-04-06 20:59:55 +02:00
|
|
|
}
|
2021-04-08 19:08:05 +02:00
|
|
|
} else if ($action === "clean") {
|
2022-06-20 19:52:31 +02:00
|
|
|
$sql = connectSQL() or die();
|
2021-04-08 19:08:05 +02:00
|
|
|
printLine("Deleting user related data older than 90 days...");
|
|
|
|
|
|
|
|
// 1st: Select all related tables and entities
|
|
|
|
$tables = [];
|
|
|
|
$res = $sql->select("entityId", "tableName")
|
|
|
|
->from("EntityLog")
|
2021-04-08 19:48:04 +02:00
|
|
|
->where(new Compare("modified", new DateSub($sql->now(), new Column("lifetime"), "DAY"), "<="))
|
|
|
|
->dump()
|
2021-04-08 19:08:05 +02:00
|
|
|
->execute();
|
|
|
|
|
|
|
|
$success = ($res !== false);
|
|
|
|
if (!$success) {
|
|
|
|
_exit("Error querying data: " . $sql->getLastError());
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($res as $row) {
|
|
|
|
$tableName = $row["tableName"];
|
|
|
|
$uid = $row["entityId"];
|
|
|
|
if (!isset($tables[$tableName])) {
|
|
|
|
$tables[$tableName] = [];
|
|
|
|
}
|
|
|
|
$tables[$tableName][] = $uid;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 2nd: delete!
|
2022-06-20 19:52:31 +02:00
|
|
|
foreach ($tables as $table => $ids) {
|
2021-04-08 19:08:05 +02:00
|
|
|
$success = $sql->delete($table)
|
2022-06-20 19:52:31 +02:00
|
|
|
->where(new CondIn(new Column("id"), $ids))
|
2021-04-08 19:08:05 +02:00
|
|
|
->execute();
|
|
|
|
|
|
|
|
if (!$success) {
|
|
|
|
printLine("Error deleting data: " . $sql->getLastError());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
printLine("Done!");
|
2021-04-06 23:05:02 +02:00
|
|
|
} else {
|
2022-05-31 16:14:49 +02:00
|
|
|
_exit("Usage: cli.php db <migrate|import|export|shell> [options...]");
|
2021-04-06 23:05:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-07 13:09:50 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2024-03-24 17:36:16 +01:00
|
|
|
function onMaintenance(array $argv): void {
|
2021-04-06 23:05:02 +02:00
|
|
|
$action = $argv[2] ?? "status";
|
|
|
|
$maintenanceFile = "MAINTENANCE";
|
|
|
|
$isMaintenanceEnabled = file_exists($maintenanceFile);
|
2024-04-14 20:31:16 +02:00
|
|
|
$sql = connectSQL();
|
|
|
|
$logger = new \Core\Driver\Logger\Logger("CLI", $sql);
|
2021-04-06 23:05:02 +02:00
|
|
|
|
|
|
|
if ($action === "status") {
|
2021-04-07 00:03:14 +02:00
|
|
|
_exit("Maintenance: " . ($isMaintenanceEnabled ? "on" : "off"));
|
2021-04-06 23:05:02 +02:00
|
|
|
} else if ($action === "on") {
|
2021-04-07 00:03:14 +02:00
|
|
|
$file = fopen($maintenanceFile, 'w') or _exit("Unable to create maintenance file");
|
2021-04-06 23:05:02 +02:00
|
|
|
fclose($file);
|
2024-04-14 20:31:16 +02:00
|
|
|
$logger->info("Maintenance mode enabled");
|
2021-04-07 00:03:14 +02:00
|
|
|
_exit("Maintenance enabled");
|
2021-04-06 23:05:02 +02:00
|
|
|
} else if ($action === "off") {
|
|
|
|
if (file_exists($maintenanceFile)) {
|
|
|
|
if (!unlink($maintenanceFile)) {
|
2021-04-07 00:03:14 +02:00
|
|
|
_exit("Unable to delete maintenance file");
|
2021-04-06 23:05:02 +02:00
|
|
|
}
|
|
|
|
}
|
2024-04-14 20:31:16 +02:00
|
|
|
$logger->info("Maintenance mode disabled");
|
2021-04-07 00:03:14 +02:00
|
|
|
_exit("Maintenance disabled");
|
2021-04-07 00:55:53 +02:00
|
|
|
} else if ($action === "update") {
|
2024-04-14 20:31:16 +02:00
|
|
|
$logger->info("Update started");
|
2024-05-04 15:07:24 +02:00
|
|
|
$oldPatchFiles = array_merge(
|
|
|
|
glob('Core/Configuration/Patch/*.php'),
|
|
|
|
glob('Site/Configuration/Patch/*.php')
|
|
|
|
);
|
2021-04-07 13:09:50 +02:00
|
|
|
printLine("$ git remote -v");
|
|
|
|
exec("git remote -v", $gitRemote, $ret);
|
|
|
|
if ($ret !== 0) {
|
2024-04-14 20:31:16 +02:00
|
|
|
$logger->warning("Update stopped. git remote returned:\n" . implode("\n", $gitRemote));
|
2021-04-07 13:09:50 +02:00
|
|
|
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");
|
|
|
|
}
|
2021-04-07 00:55:53 +02:00
|
|
|
|
|
|
|
printLine("$ git fetch " . str_replace("/", " ", $pullBranch));
|
|
|
|
exec("git fetch " . str_replace("/", " ", $pullBranch), $gitFetch, $ret);
|
|
|
|
if ($ret !== 0) {
|
2024-04-14 20:31:16 +02:00
|
|
|
$logger->warning("Update stopped. git fetch returned:\n" . implode("\n", $gitFetch));
|
2021-04-07 01:00:52 +02:00
|
|
|
die();
|
2021-04-07 00:55:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
printLine("$ git log HEAD..$pullBranch --oneline");
|
2024-05-09 13:42:39 +02:00
|
|
|
exec("git log HEAD..$pullBranch --oneline 2>&1", $gitLog, $ret);
|
2021-04-07 00:55:53 +02:00
|
|
|
if ($ret !== 0) {
|
2024-04-14 20:31:16 +02:00
|
|
|
$logger->warning("Update stopped. git log returned:\n" . implode("\n", $gitLog));
|
2021-04-07 01:00:52 +02:00
|
|
|
die();
|
2021-04-07 00:55:53 +02:00
|
|
|
} 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
|
2024-05-09 13:42:39 +02:00
|
|
|
exec("git diff-index --quiet HEAD -- 2>&1", $gitDiff, $ret);
|
2021-04-07 00:55:53 +02:00
|
|
|
if ($ret !== 0) {
|
2024-04-14 20:31:16 +02:00
|
|
|
$logger->warning("Update stopped due to uncommitted changes");
|
2021-04-07 00:55:53 +02:00
|
|
|
_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");
|
2024-04-14 20:31:16 +02:00
|
|
|
$logger->info("Maintenance mode enabled");
|
2021-04-07 01:00:52 +02:00
|
|
|
$file = fopen($maintenanceFile, 'w') or _exit("Unable to create maintenance file");
|
|
|
|
fclose($file);
|
2021-04-07 00:55:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
printLine("Ready to update, pulling and merging");
|
2021-04-07 01:00:52 +02:00
|
|
|
printLine("$ git pull " . str_replace("/", " ", $pullBranch) . " --no-ff");
|
2024-05-09 13:42:39 +02:00
|
|
|
exec("git pull " . str_replace("/", " ", $pullBranch) . " --no-ff 2>&1", $gitPull, $ret);
|
2021-04-07 00:55:53 +02:00
|
|
|
if ($ret !== 0) {
|
2021-04-07 01:00:52 +02:00
|
|
|
printLine();
|
2021-04-07 12:57:00 +02:00
|
|
|
printLine("Update could not be applied, check the git output.");
|
2021-04-07 00:55:53 +02:00
|
|
|
printLine("Follow the instructions and afterwards turn off the maintenance mode again using:");
|
|
|
|
printLine("cli.php maintenance off");
|
2021-04-09 12:37:24 +02:00
|
|
|
printLine("Also don't forget to apply new database patches using: cli.php db migrate");
|
2024-04-14 20:31:16 +02:00
|
|
|
$logger->error("Update stopped. git pull returned:\n" . implode("\n", $gitPull));
|
2021-04-07 01:00:52 +02:00
|
|
|
die();
|
2021-04-07 00:55:53 +02:00
|
|
|
}
|
|
|
|
|
2024-05-04 15:07:24 +02:00
|
|
|
// TODO: how to handle modified database entities?
|
|
|
|
$newPatchFiles = array_merge(
|
|
|
|
glob('Core/Configuration/Patch/*.php'),
|
|
|
|
glob('Site/Configuration/Patch/*.php')
|
|
|
|
);
|
2021-04-09 12:37:24 +02:00
|
|
|
$newPatchFiles = array_diff($newPatchFiles, $oldPatchFiles);
|
|
|
|
if (count($newPatchFiles) > 0) {
|
2022-06-20 19:52:31 +02:00
|
|
|
if ($sql) {
|
2024-04-14 20:31:16 +02:00
|
|
|
printLine("Applying new database patches");
|
2024-05-04 15:07:24 +02:00
|
|
|
sort($newPatchFiles);
|
2021-04-09 12:37:24 +02:00
|
|
|
foreach ($newPatchFiles as $patchFile) {
|
2024-05-04 15:07:24 +02:00
|
|
|
if (preg_match("/(Core|Site)\/Configuration\/Patch\/(.*)\.php/", $patchFile, $match)) {
|
|
|
|
applyPatch($sql, $patchFile);
|
2021-04-09 12:37:24 +02:00
|
|
|
}
|
|
|
|
}
|
2024-04-14 20:31:16 +02:00
|
|
|
} else {
|
|
|
|
printLine("Cannot apply database patches, since the database connection failed.");
|
|
|
|
$logger->warning("Cannot apply database patches, since the database connection failed.");
|
2021-04-09 12:37:24 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-07 00:55:53 +02:00
|
|
|
// disable maintenance mode again
|
|
|
|
if (!$isMaintenanceEnabled) {
|
|
|
|
printLine("Turning off maintenance mode");
|
2021-04-07 01:00:52 +02:00
|
|
|
if (file_exists($maintenanceFile)) {
|
|
|
|
if (!unlink($maintenanceFile)) {
|
|
|
|
_exit("Unable to delete maintenance file");
|
2024-04-14 20:31:16 +02:00
|
|
|
} else {
|
|
|
|
$logger->info("Maintenance mode disabled");
|
2021-04-07 01:00:52 +02:00
|
|
|
}
|
|
|
|
}
|
2021-04-07 00:55:53 +02:00
|
|
|
}
|
2024-04-14 20:31:16 +02:00
|
|
|
|
|
|
|
$logger->info("Update completed.");
|
2021-04-07 00:03:14 +02:00
|
|
|
} else {
|
2021-04-07 00:55:53 +02:00
|
|
|
_exit("Usage: cli.php maintenance <status|on|off|update>");
|
2021-04-07 00:03:14 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-08 16:53:43 +01:00
|
|
|
function getConsoleWidth(): int {
|
|
|
|
$width = getenv('COLUMNS');
|
|
|
|
if (!$width) {
|
|
|
|
$width = exec('tput cols');
|
|
|
|
if (!$width) {
|
|
|
|
$width = 80; // default gnome-terminal column count
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return intval($width);
|
|
|
|
}
|
|
|
|
|
2024-03-24 17:36:16 +01:00
|
|
|
function printTable(array $head, array $body): void {
|
2021-04-07 00:03:14 +02:00
|
|
|
|
|
|
|
$columns = [];
|
|
|
|
foreach ($head as $key) {
|
|
|
|
$columns[$key] = strlen($key);
|
|
|
|
}
|
|
|
|
|
2021-12-08 16:53:43 +01:00
|
|
|
$maxWidth = getConsoleWidth();
|
2021-04-07 00:03:14 +02:00
|
|
|
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) {
|
2021-12-08 16:53:43 +01:00
|
|
|
$line = 0;
|
2021-04-07 00:03:14 +02:00
|
|
|
foreach ($head as $key) {
|
2021-12-08 16:53:43 +01:00
|
|
|
$width = min(max($maxWidth - $line, 0), $columns[$key]);
|
|
|
|
$line += $width;
|
|
|
|
echo str_pad($row[$key] ?? "", $width) . ' ';
|
2021-04-07 00:03:14 +02:00
|
|
|
}
|
|
|
|
printLine();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-24 17:36:16 +01:00
|
|
|
function onSettings(array $argv): void {
|
2022-06-20 19:52:31 +02:00
|
|
|
global $context;
|
2022-11-19 01:15:34 +01:00
|
|
|
connectSQL() or die();
|
2021-12-08 16:53:43 +01:00
|
|
|
$action = $argv[2] ?? "list";
|
|
|
|
|
|
|
|
if ($action === "list" || $action === "get") {
|
|
|
|
$key = (($action === "list" || count($argv) < 4) ? null : $argv[3]);
|
2022-11-18 18:06:46 +01:00
|
|
|
$req = new \Core\API\Settings\Get($context);
|
2021-12-08 16:53:43 +01:00
|
|
|
$success = $req->execute(["key" => $key]);
|
|
|
|
if (!$success) {
|
|
|
|
_exit("Error listings settings: " . $req->getLastError());
|
|
|
|
} else {
|
|
|
|
$settings = [];
|
|
|
|
foreach ($req->getResult()["settings"] as $key => $value) {
|
|
|
|
$settings[] = ["key" => $key, "value" => $value];
|
|
|
|
}
|
|
|
|
printTable(["key", "value"], $settings);
|
|
|
|
}
|
|
|
|
} else if ($action === "set" || $action === "update") {
|
|
|
|
if (count($argv) < 5) {
|
|
|
|
_exit("Usage: $argv[0] settings $argv[2] <key> <value>");
|
|
|
|
} else {
|
|
|
|
$key = $argv[3];
|
|
|
|
$value = $argv[4];
|
2022-11-18 18:06:46 +01:00
|
|
|
$req = new \Core\API\Settings\Set($context);
|
2021-12-08 16:53:43 +01:00
|
|
|
$success = $req->execute(["settings" => [$key => $value]]);
|
|
|
|
if (!$success) {
|
|
|
|
_exit("Error updating settings: " . $req->getLastError());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if ($action === "unset" || $action === "delete") {
|
|
|
|
if (count($argv) < 4) {
|
|
|
|
_exit("Usage: $argv[0] settings $argv[2] <key>");
|
|
|
|
} else {
|
|
|
|
$key = $argv[3];
|
2022-11-18 18:06:46 +01:00
|
|
|
$req = new \Core\API\Settings\Set($context);
|
2021-12-08 16:53:43 +01:00
|
|
|
$success = $req->execute(["settings" => [$key => null]]);
|
|
|
|
if (!$success) {
|
|
|
|
_exit("Error updating settings: " . $req->getLastError());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
_exit("Usage: $argv[0] settings <get|set|unset>");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-24 17:36:16 +01:00
|
|
|
function onRoutes(array $argv): void {
|
2022-06-20 19:52:31 +02:00
|
|
|
global $context;
|
2022-11-19 01:15:34 +01:00
|
|
|
connectSQL() or die();
|
2021-04-07 00:03:14 +02:00
|
|
|
$action = $argv[2] ?? "list";
|
|
|
|
|
|
|
|
if ($action === "list") {
|
2023-01-16 21:47:23 +01:00
|
|
|
$sql = $context->getSQL();
|
|
|
|
$routes = \Core\Objects\DatabaseEntity\Route::findAll($sql);
|
|
|
|
if ($routes === false || $routes === null) {
|
|
|
|
_exit("Error fetching routes: " . $sql->getLastError());
|
2021-04-07 00:03:14 +02:00
|
|
|
} else {
|
2022-12-01 11:01:49 +01:00
|
|
|
$head = ["id", "pattern", "type", "target", "extra", "active", "exact"];
|
2021-04-07 00:03:14 +02:00
|
|
|
|
|
|
|
// strict boolean
|
2023-01-16 21:47:23 +01:00
|
|
|
$tableRows = [];
|
|
|
|
foreach ($routes as $route) {
|
|
|
|
$jsonData = $route->jsonSerialize(["id", "pattern", "type", "target", "extra", "active", "exact"]);
|
|
|
|
// strict bool conversion
|
|
|
|
$jsonData["active"] = $jsonData["active"] ? "true" : "false";
|
|
|
|
$jsonData["exact"] = $jsonData["exact"] ? "true" : "false";
|
|
|
|
$tableRows[] = $jsonData;
|
2021-04-07 00:03:14 +02:00
|
|
|
}
|
|
|
|
|
2023-01-16 21:47:23 +01:00
|
|
|
printTable($head, $tableRows);
|
2021-04-07 00:03:14 +02:00
|
|
|
}
|
2023-01-25 14:15:34 +01:00
|
|
|
} else if ($action === "generate_cache") {
|
|
|
|
$req = new \Core\API\Routes\GenerateCache($context);
|
|
|
|
$success = $req->execute();
|
|
|
|
if (!$success) {
|
|
|
|
_exit("Error generating cache: " . $req->getLastError());
|
|
|
|
}
|
2021-04-07 00:03:14 +02:00
|
|
|
} else if ($action === "add") {
|
2022-12-01 11:01:49 +01:00
|
|
|
if (count($argv) < 7) {
|
|
|
|
_exit("Usage: cli.php routes add <pattern> <type> <target> <exact> [extra]");
|
2021-04-07 00:03:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$params = array(
|
2022-12-01 11:01:49 +01:00
|
|
|
"pattern" => $argv[3],
|
|
|
|
"type" => $argv[4],
|
2021-04-07 00:03:14 +02:00
|
|
|
"target" => $argv[5],
|
2022-12-01 11:01:49 +01:00
|
|
|
"exact" => $argv[6],
|
2022-06-01 09:47:31 +02:00
|
|
|
"extra" => $argv[7] ?? "",
|
2021-04-07 00:03:14 +02:00
|
|
|
);
|
|
|
|
|
2022-11-18 18:06:46 +01:00
|
|
|
$req = new \Core\API\Routes\Add($context);
|
2021-04-07 00:03:14 +02:00
|
|
|
$success = $req->execute($params);
|
|
|
|
if (!$success) {
|
|
|
|
_exit($req->getLastError());
|
|
|
|
} else {
|
|
|
|
_exit("Route added successfully");
|
2021-04-07 12:57:00 +02:00
|
|
|
}
|
2021-04-07 00:03:14 +02:00
|
|
|
} else if (in_array($action, ["remove","modify","enable","disable"])) {
|
2022-12-01 11:01:49 +01:00
|
|
|
$routeId = $argv[3] ?? null;
|
|
|
|
if ($routeId === null || ($action === "modify" && count($argv) < 8)) {
|
2021-04-07 00:03:14 +02:00
|
|
|
if ($action === "modify") {
|
2022-12-01 11:01:49 +01:00
|
|
|
_exit("Usage: cli.php routes $action <id> <pattern> <type> <target> <exact> [extra]");
|
2021-04-07 00:03:14 +02:00
|
|
|
} else {
|
2022-06-20 19:52:31 +02:00
|
|
|
_exit("Usage: cli.php routes $action <id>");
|
2021-04-07 00:03:14 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-01 11:01:49 +01:00
|
|
|
$params = ["id" => $routeId];
|
2021-04-07 00:03:14 +02:00
|
|
|
if ($action === "remove") {
|
|
|
|
$input = null;
|
|
|
|
do {
|
|
|
|
if ($input === "n") {
|
|
|
|
die();
|
|
|
|
}
|
2022-12-01 11:01:49 +01:00
|
|
|
echo "Remove route #$routeId? (y|n): ";
|
2021-04-07 00:03:14 +02:00
|
|
|
} while(($input = trim(fgets(STDIN))) !== "y");
|
|
|
|
|
2022-11-18 18:06:46 +01:00
|
|
|
$req = new \Core\API\Routes\Remove($context);
|
2021-04-07 00:03:14 +02:00
|
|
|
} else if ($action === "enable") {
|
2022-11-18 18:06:46 +01:00
|
|
|
$req = new \Core\API\Routes\Enable($context);
|
2021-04-07 00:03:14 +02:00
|
|
|
} else if ($action === "disable") {
|
2022-11-18 18:06:46 +01:00
|
|
|
$req = new \Core\API\Routes\Disable($context);
|
2021-04-07 00:03:14 +02:00
|
|
|
} else if ($action === "modify") {
|
2022-11-18 18:06:46 +01:00
|
|
|
$req = new \Core\API\Routes\Update($context);
|
2022-12-01 11:01:49 +01:00
|
|
|
$params["pattern"] = $argv[4];
|
|
|
|
$params["type"] = $argv[5];
|
2021-04-07 00:03:14 +02:00
|
|
|
$params["target"] = $argv[6];
|
2022-12-01 11:01:49 +01:00
|
|
|
$params["exact"] = $argv[7];
|
|
|
|
$params["extra"] = $argv[8] ?? "";
|
2021-04-07 00:03:14 +02:00
|
|
|
} else {
|
|
|
|
_exit("Unsupported action");
|
|
|
|
}
|
|
|
|
|
|
|
|
$success = $req->execute($params);
|
|
|
|
if (!$success) {
|
|
|
|
_exit($req->getLastError());
|
|
|
|
} else {
|
|
|
|
_exit("Route updated successfully");
|
|
|
|
}
|
2021-04-06 23:05:02 +02:00
|
|
|
} else {
|
2023-01-25 14:15:34 +01:00
|
|
|
_exit("Usage: cli.php routes <list|enable|disable|add|remove|modify|generate_cache> [options...]");
|
2021-04-06 16:34:12 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-24 17:36:16 +01:00
|
|
|
function onTest($argv): void {
|
2021-12-08 16:53:43 +01:00
|
|
|
$files = glob(WEBROOT . '/test/*.test.php');
|
|
|
|
$requestedTests = array_filter(array_slice($argv, 2), function ($t) {
|
|
|
|
return !startsWith($t, "-");
|
|
|
|
});
|
2022-05-31 16:14:49 +02:00
|
|
|
$verbose = in_array("-v", $argv);
|
2021-12-08 16:53:43 +01:00
|
|
|
|
|
|
|
foreach ($files as $file) {
|
|
|
|
include_once $file;
|
|
|
|
$baseName = substr(basename($file), 0, - strlen(".test.php"));
|
|
|
|
if (!empty($requestedTests) && !in_array($baseName, $requestedTests)) {
|
|
|
|
continue;
|
|
|
|
}
|
2021-04-09 12:37:24 +02:00
|
|
|
|
2021-12-08 16:53:43 +01:00
|
|
|
$className = $baseName . "Test";
|
|
|
|
if (class_exists($className)) {
|
2024-03-24 17:36:16 +01:00
|
|
|
printLine("=== Running $className ===");
|
2021-12-08 16:53:43 +01:00
|
|
|
$testClass = new \PHPUnit\Framework\TestSuite();
|
|
|
|
$testClass->addTestSuite($className);
|
|
|
|
$result = $testClass->run();
|
2024-03-24 17:36:16 +01:00
|
|
|
printLine("Done after " . $result->time() . "s");
|
2021-12-08 16:53:43 +01:00
|
|
|
$stats = [
|
|
|
|
"total" => $result->count(),
|
|
|
|
"skipped" => $result->skippedCount(),
|
|
|
|
"error" => $result->errorCount(),
|
|
|
|
"failure" => $result->failureCount(),
|
|
|
|
"warning" => $result->warningCount(),
|
|
|
|
];
|
|
|
|
|
|
|
|
// Summary
|
2024-03-24 17:36:16 +01:00
|
|
|
printLine(
|
|
|
|
implode(", ", array_map(function ($key) use ($stats) {
|
2021-12-08 16:53:43 +01:00
|
|
|
return "$key: " . $stats[$key];
|
2024-03-24 17:36:16 +01:00
|
|
|
}, array_keys($stats)))
|
|
|
|
);
|
2021-12-08 16:53:43 +01:00
|
|
|
|
|
|
|
$reports = array_merge($result->errors(), $result->failures());
|
|
|
|
foreach ($reports as $error) {
|
|
|
|
$exception = $error->thrownException();
|
|
|
|
echo $error->toString();
|
|
|
|
if ($verbose) {
|
2024-03-24 17:36:16 +01:00
|
|
|
printLine(". Stacktrace:");
|
|
|
|
printLine($exception->getTraceAsString());
|
2021-12-08 16:53:43 +01:00
|
|
|
} else {
|
|
|
|
$location = array_filter($exception->getTrace(), function ($t) use ($file) {
|
|
|
|
return isset($t["file"]) && $t["file"] === $file;
|
|
|
|
});
|
|
|
|
$location = array_reverse($location);
|
|
|
|
$location = array_pop($location);
|
|
|
|
if ($location) {
|
2024-03-24 17:36:16 +01:00
|
|
|
printLine(" in " . substr($location["file"], strlen(WEBROOT)) . "#" . $location["line"]);
|
2021-12-08 16:53:43 +01:00
|
|
|
} else {
|
2024-03-24 17:36:16 +01:00
|
|
|
printLine();
|
2021-12-08 16:53:43 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-04-09 12:37:24 +02:00
|
|
|
}
|
|
|
|
|
2024-03-24 17:36:16 +01:00
|
|
|
function onMail($argv): void {
|
2022-06-20 19:52:31 +02:00
|
|
|
global $context;
|
2021-04-09 16:05:36 +02:00
|
|
|
$action = $argv[2] ?? null;
|
2022-11-18 18:06:46 +01:00
|
|
|
if ($action === "send_queue") {
|
2022-11-19 01:15:34 +01:00
|
|
|
connectSQL() or die();
|
2022-11-18 18:06:46 +01:00
|
|
|
$req = new \Core\API\Mail\SendQueue($context);
|
2022-02-20 16:53:26 +01:00
|
|
|
$debug = in_array("debug", $argv);
|
|
|
|
if (!$req->execute(["debug" => $debug])) {
|
|
|
|
_exit("Error processing mail queue: " . $req->getLastError());
|
|
|
|
}
|
2024-04-06 19:09:12 +02:00
|
|
|
} else if ($action === "test") {
|
|
|
|
$recipient = $argv[3] ?? null;
|
|
|
|
$gpgFingerprint = $argv[4] ?? null;
|
|
|
|
if (!$recipient) {
|
|
|
|
_exit("Usage: cli.php mail test <recipient> [gpg-fingerprint]");
|
|
|
|
}
|
|
|
|
|
|
|
|
connectSQL() or die();
|
|
|
|
$req = new \Core\API\Mail\Test($context);
|
|
|
|
$success = $req->execute([
|
|
|
|
"receiver" => $recipient,
|
|
|
|
"gpgFingerprint" => $gpgFingerprint,
|
|
|
|
]);
|
|
|
|
|
|
|
|
$result = $req->getResult();
|
|
|
|
if ($success) {
|
|
|
|
printLine("Test email sent successfully");
|
|
|
|
} else {
|
|
|
|
printLine("Test email failed to sent: " . $req->getLastError());
|
|
|
|
}
|
|
|
|
|
|
|
|
if (array_key_exists("output", $result)) {
|
|
|
|
printLine();
|
|
|
|
printLine($result["output"]);
|
|
|
|
}
|
2021-04-09 16:05:36 +02:00
|
|
|
} else {
|
2024-04-06 19:09:12 +02:00
|
|
|
_exit("Usage: cli.php mail <send_queue|test> [options...]");
|
2021-04-09 16:05:36 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-24 17:36:16 +01:00
|
|
|
function onImpersonate($argv): void {
|
2022-06-20 19:52:31 +02:00
|
|
|
global $context;
|
|
|
|
|
2022-02-20 16:53:26 +01:00
|
|
|
if (count($argv) < 3) {
|
2022-02-21 14:04:49 +01:00
|
|
|
_exit("Usage: cli.php impersonate <user_id|user_name>");
|
2022-02-20 16:53:26 +01:00
|
|
|
}
|
|
|
|
|
2022-06-20 19:52:31 +02:00
|
|
|
$sql = connectSQL() or die();
|
2022-02-21 14:04:49 +01:00
|
|
|
$userId = $argv[2];
|
|
|
|
if (!is_numeric($userId)) {
|
2022-06-20 19:52:31 +02:00
|
|
|
$res = $sql->select("id")
|
2022-02-21 14:04:49 +01:00
|
|
|
->from("User")
|
2022-11-26 23:57:28 +01:00
|
|
|
->whereEq("name", $userId)
|
2022-02-21 14:04:49 +01:00
|
|
|
->execute();
|
|
|
|
if ($res === false) {
|
|
|
|
_exit("SQL error: " . $sql->getLastError());
|
|
|
|
} else {
|
2022-06-20 19:52:31 +02:00
|
|
|
$userId = $res[0]["id"];
|
2022-02-21 14:04:49 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-18 18:06:46 +01:00
|
|
|
$user = new \Core\Objects\DatabaseEntity\User($userId);
|
|
|
|
$session = new \Core\Objects\DatabaseEntity\Session($context, $user);
|
2022-02-20 16:53:26 +01:00
|
|
|
$session->setData(["2faAuthenticated" => true]);
|
2022-06-20 19:52:31 +02:00
|
|
|
$session->update();
|
2024-03-27 15:15:46 +01:00
|
|
|
echo "Cookie: session=" . $session->getUUID() . PHP_EOL .
|
|
|
|
"CSRF-Token: " . $session->getCsrfToken() . PHP_EOL;
|
2022-02-20 16:53:26 +01:00
|
|
|
}
|
|
|
|
|
2024-03-25 17:03:46 +01:00
|
|
|
function onFrontend(array $argv): void {
|
|
|
|
if (count($argv) < 3) {
|
2024-04-04 12:46:58 +02:00
|
|
|
_exit("Usage: cli.php frontend <build|add|rm|ls> [options...]");
|
2024-03-25 17:03:46 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$reactRoot = realpath(WEBROOT . "/react/");
|
|
|
|
if (!$reactRoot) {
|
|
|
|
_exit("React root directory not found!");
|
|
|
|
}
|
|
|
|
|
|
|
|
$action = $argv[2] ?? null;
|
|
|
|
if ($action === "build") {
|
|
|
|
$proc = proc_open(["yarn", "run", "build"], [1 => STDOUT, 2 => STDERR], $pipes, $reactRoot);
|
|
|
|
exit(proc_close($proc));
|
|
|
|
} else if ($action === "add") {
|
|
|
|
if (count($argv) < 4) {
|
|
|
|
_exit("Usage: cli.php frontend add <module-name>");
|
|
|
|
}
|
|
|
|
|
|
|
|
$moduleName = strtolower($argv[3]);
|
|
|
|
if (!preg_match("/[a-z0-9_-]/", $moduleName)) {
|
|
|
|
_exit("Module name should only be [a-zA-Z0-9_-]");
|
2024-03-25 18:37:08 +01:00
|
|
|
} else if (in_array($moduleName, ["_tmpl", "dist", "shared"])) {
|
|
|
|
_exit("Invalid module name");
|
2024-03-25 17:03:46 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$templatePath = implode(DIRECTORY_SEPARATOR, [$reactRoot, "_tmpl"]);
|
|
|
|
$modulePath = implode(DIRECTORY_SEPARATOR, [$reactRoot, $moduleName]);
|
|
|
|
if (file_exists($modulePath)) {
|
|
|
|
_exit("File or module does already exist: " . $modulePath);
|
|
|
|
}
|
|
|
|
|
|
|
|
$rootPackageJsonPath = implode(DIRECTORY_SEPARATOR, [$reactRoot, "package.json"]);
|
|
|
|
$rootPackageJson = @json_decode(@file_get_contents($rootPackageJsonPath), true);
|
|
|
|
if (!$rootPackageJson) {
|
|
|
|
_exit("Unable to read root package.json");
|
|
|
|
}
|
|
|
|
|
|
|
|
$reactVersion = $rootPackageJson["dependencies"]["react"];
|
|
|
|
if (!array_key_exists($moduleName, $rootPackageJson["targets"])) {
|
|
|
|
$rootPackageJson["targets"][$moduleName] = [
|
|
|
|
"source" => "./$moduleName/src/index.js",
|
|
|
|
"distDir" => "./dist/$moduleName"
|
|
|
|
];
|
|
|
|
file_put_contents($rootPackageJsonPath, json_encode($rootPackageJson, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
|
|
|
|
}
|
|
|
|
|
|
|
|
mkdir($modulePath, 0775, true);
|
|
|
|
$placeHolders = [
|
|
|
|
"MODULE_NAME" => $moduleName,
|
|
|
|
"REACT_VERSION" => $reactVersion
|
|
|
|
];
|
|
|
|
|
|
|
|
$it = new RecursiveDirectoryIterator($templatePath);
|
|
|
|
foreach (new RecursiveIteratorIterator($it) as $file) {
|
|
|
|
$fileName = $file->getFilename();
|
|
|
|
$relDir = substr($file->getPath(), strlen($templatePath) + 1);
|
|
|
|
$targetFile = implode(DIRECTORY_SEPARATOR, [$modulePath, $relDir, $fileName]);
|
|
|
|
if ($file->isFile()) {
|
|
|
|
$contents = file_get_contents($file);
|
|
|
|
foreach ($placeHolders as $key => $value) {
|
|
|
|
$contents = str_replace("{{{$key}}}", $value, $contents);
|
|
|
|
}
|
|
|
|
$directory = dirname($targetFile);
|
|
|
|
if (!is_dir($directory)) {
|
|
|
|
mkdir($directory, 0775, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
file_put_contents($targetFile, $contents);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
printLine("Successfully added react module: $moduleName");
|
|
|
|
printLine("Run `php cli.php frontend build` to create a production build or");
|
|
|
|
printLine("run `php cli.php frontend dev $moduleName` to start a dev-server with your module");
|
|
|
|
} else if ($action === "dev") {
|
|
|
|
if (count($argv) < 4) {
|
|
|
|
_exit("Usage: cli.php frontend add <module-name>");
|
|
|
|
}
|
|
|
|
|
|
|
|
$moduleName = strtolower($argv[3]);
|
|
|
|
$proc = proc_open(["yarn", "workspace", $moduleName, "run", "dev"], [1 => STDOUT, 2 => STDERR], $pipes, $reactRoot);
|
|
|
|
exit(proc_close($proc));
|
|
|
|
} else if ($action === "rm") {
|
|
|
|
if (count($argv) < 4) {
|
|
|
|
_exit("Usage: cli.php frontend add <module-name>");
|
|
|
|
}
|
|
|
|
|
|
|
|
$moduleName = strtolower($argv[3]);
|
|
|
|
if (!preg_match("/[a-z0-9_-]/", $moduleName)) {
|
|
|
|
_exit("Module name should only be [a-zA-Z0-9_-]");
|
2024-03-25 18:37:08 +01:00
|
|
|
} else if (in_array($moduleName, ["_tmpl", "dist", "shared"])) {
|
|
|
|
_exit("This module cannot be removed");
|
2024-03-25 17:03:46 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$modulePath = implode(DIRECTORY_SEPARATOR, [$reactRoot, $moduleName]);
|
|
|
|
if (!is_dir($modulePath)) {
|
|
|
|
_exit("Module not found: $modulePath");
|
|
|
|
}
|
|
|
|
|
|
|
|
$rootPackageJsonPath = implode(DIRECTORY_SEPARATOR, [$reactRoot, "package.json"]);
|
|
|
|
$rootPackageJson = @json_decode(@file_get_contents($rootPackageJsonPath), true);
|
|
|
|
if (!$rootPackageJson) {
|
|
|
|
_exit("Unable to read root package.json");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (array_key_exists($moduleName, $rootPackageJson["targets"])) {
|
|
|
|
unset($rootPackageJson["targets"][$moduleName]);
|
|
|
|
file_put_contents($rootPackageJsonPath, json_encode($rootPackageJson, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
|
|
|
|
}
|
|
|
|
|
|
|
|
$input = strtolower(trim(readline("Do you want to disable the module only and keep the files? [Y|n]: ")));
|
|
|
|
if ($input === "n") {
|
|
|
|
rrmdir($modulePath);
|
|
|
|
printLine("Disabled and deleted module: $moduleName");
|
|
|
|
} else {
|
|
|
|
printLine("Disabled module: $moduleName");
|
|
|
|
}
|
|
|
|
} else if ($action === "ls") {
|
|
|
|
printLine("Current available modules:");
|
|
|
|
foreach (glob(implode(DIRECTORY_SEPARATOR, [$reactRoot, "*"]), GLOB_ONLYDIR) as $directory) {
|
|
|
|
if (basename($directory) === "_tmpl") {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$packageJson = realpath(implode(DIRECTORY_SEPARATOR, [$directory, "package.json"]));
|
|
|
|
if ($packageJson) {
|
|
|
|
$packageJsonContents = @json_decode(@file_get_contents($packageJson), true);
|
|
|
|
if (!$packageJsonContents) {
|
|
|
|
printLine("$directory: Unable to read package.json");
|
|
|
|
} else {
|
|
|
|
$packageName = $packageJsonContents["name"];
|
|
|
|
$packageVersion = $packageJsonContents["version"];
|
|
|
|
printLine("- $packageName version: $packageVersion");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
_exit("Usage: cli.php frontend <build|ls|add|rm|dev> [options...]");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-04 12:46:58 +02:00
|
|
|
function onAPI(array $argv): void {
|
|
|
|
if (count($argv) < 3) {
|
|
|
|
_exit("Usage: cli.php api <ls|add> [options...]");
|
|
|
|
}
|
|
|
|
|
|
|
|
$action = $argv[2] ?? null;
|
|
|
|
if ($action === "ls") {
|
|
|
|
$endpoints = Request::getApiEndpoints();
|
|
|
|
foreach ($endpoints as $endpoint => $class) {
|
|
|
|
$className = $class->getName();
|
|
|
|
printLine(" - $className: $endpoint");
|
|
|
|
}
|
|
|
|
// var_dump($endpoints);
|
|
|
|
} else if ($action === "add") {
|
|
|
|
echo "API-Name: ";
|
|
|
|
$methodNames = [];
|
|
|
|
$apiName = ucfirst(trim(fgets(STDIN)));
|
|
|
|
if (!preg_match("/[a-zA-Z_-]/", $apiName)) {
|
|
|
|
_exit("Invalid API-Name, should be [a-zA-Z_-]");
|
|
|
|
}
|
|
|
|
|
|
|
|
printLine("Do you want to add nested methods? Leave blank to skip.");
|
|
|
|
while (true) {
|
|
|
|
echo "Method name: ";
|
|
|
|
$methodName = ucfirst(trim(fgets(STDIN)));
|
|
|
|
if ($methodName) {
|
|
|
|
if (!preg_match("/[a-zA-Z_-]/", $methodName)) {
|
|
|
|
printLine("Invalid method name, should be [a-zA-Z_-]");
|
|
|
|
} else if (in_array($methodName, $methodNames)) {
|
|
|
|
printLine("You already added this method.");
|
|
|
|
} else {
|
|
|
|
$methodNames[] = $methodName;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!empty($methodNames)) {
|
|
|
|
$fileName = "{$apiName}API.class.php";
|
|
|
|
$methods = implode("\n\n", array_map(function ($methodName) use ($apiName) {
|
|
|
|
return " class $methodName extends {$apiName}API {
|
|
|
|
|
|
|
|
public function __construct(Context \$context, bool \$externalCall = false) {
|
|
|
|
parent::__construct(\$context, \$externalCall, []);
|
|
|
|
// TODO: auto-generated method stub
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function _execute(): bool {
|
|
|
|
// TODO: auto-generated method stub
|
|
|
|
return \$this->success;
|
|
|
|
}
|
2024-04-05 13:01:15 +02:00
|
|
|
|
2024-04-24 16:58:18 +02:00
|
|
|
public static function getDescription(): string {
|
|
|
|
// TODO: auto generated endpoint description
|
|
|
|
return \"Short description, what users are able to do with this endpoint.\";
|
2024-04-05 13:01:15 +02:00
|
|
|
}
|
2024-04-04 12:46:58 +02:00
|
|
|
}";
|
|
|
|
}, $methodNames));
|
|
|
|
$content = "<?php
|
|
|
|
|
|
|
|
namespace Site\API {
|
|
|
|
|
|
|
|
use Core\API\Request;
|
|
|
|
use Core\Objects\Context;
|
|
|
|
|
|
|
|
abstract class {$apiName}API extends Request {
|
|
|
|
public function __construct(Context \$context, bool \$externalCall = false, array \$params = []) {
|
|
|
|
parent::__construct(\$context, \$externalCall, \$params);
|
|
|
|
// TODO: auto-generated method stub
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
namespace Site\API\\$apiName {
|
|
|
|
|
|
|
|
use Core\Objects\Context;
|
|
|
|
use Site\API\TestAPI;
|
|
|
|
|
|
|
|
$methods
|
|
|
|
}";
|
|
|
|
} else {
|
|
|
|
$fileName = "$apiName.class.php";
|
|
|
|
$content = "<?php
|
|
|
|
|
|
|
|
namespace Site\API;
|
|
|
|
|
|
|
|
use Core\API\Request;
|
|
|
|
use Core\Objects\Context;
|
|
|
|
|
|
|
|
class $apiName extends Request {
|
|
|
|
|
|
|
|
public function __construct(Context \$context, bool \$externalCall = false) {
|
|
|
|
parent::__construct(\$context, \$externalCall, []);
|
|
|
|
// TODO: auto-generated method stub
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function _execute(): bool {
|
|
|
|
// TODO: auto-generated method stub
|
|
|
|
return \$this->success;
|
|
|
|
}
|
2024-04-24 16:58:18 +02:00
|
|
|
|
|
|
|
public static function getDescription(): string {
|
|
|
|
// TODO: auto generated endpoint description
|
|
|
|
return \"Short description, what users are able to do with this endpoint.\";
|
2024-04-05 13:01:15 +02:00
|
|
|
}
|
2024-04-04 12:46:58 +02:00
|
|
|
}
|
|
|
|
";
|
|
|
|
}
|
|
|
|
|
|
|
|
$path = implode(DIRECTORY_SEPARATOR, [WEBROOT, "Site", "API", $fileName]);
|
|
|
|
file_put_contents($path, $content);
|
|
|
|
printLine("Successfully created API-template: $path");
|
|
|
|
} else {
|
|
|
|
_exit("Usage: cli.php api <ls|add> [options...]");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-06 16:34:12 +02:00
|
|
|
$argv = $_SERVER['argv'];
|
2024-03-25 17:03:46 +01:00
|
|
|
$registeredCommands = [
|
2024-04-04 12:46:58 +02:00
|
|
|
"help" => ["handler" => "printHelp", "description" => "prints this help page"],
|
2024-05-11 16:12:15 +02:00
|
|
|
"db" => ["handler" => "handleDatabase", "description" => "database actions like importing, exporting and shell", "requiresDocker" => ["migrate"]],
|
2024-04-14 20:31:16 +02:00
|
|
|
"routes" => ["handler" => "onRoutes", "description" => "view and modify routes", "requiresDocker" => true],
|
|
|
|
"maintenance" => ["handler" => "onMaintenance", "description" => "toggle maintenance mode", "requiresDocker" => true],
|
2024-04-04 12:46:58 +02:00
|
|
|
"test" => ["handler" => "onTest", "description" => "run unit and integration tests", "requiresDocker" => true],
|
2024-04-06 19:09:12 +02:00
|
|
|
"mail" => ["handler" => "onMail", "description" => "send mails and process the pipeline", "requiresDocker" => true],
|
2024-04-04 12:46:58 +02:00
|
|
|
"settings" => ["handler" => "onSettings", "description" => "change and view settings"],
|
|
|
|
"impersonate" => ["handler" => "onImpersonate", "description" => "create a session and print cookies and csrf tokens", "requiresDocker" => true],
|
2024-04-11 17:51:50 +02:00
|
|
|
"frontend" => ["handler" => "onFrontend", "description" => "build and manage frontend modules"],
|
2024-04-04 12:46:58 +02:00
|
|
|
"api" => ["handler" => "onAPI", "description" => "view and create API endpoints"],
|
2024-03-25 17:03:46 +01:00
|
|
|
];
|
|
|
|
|
|
|
|
|
2021-04-06 16:34:12 +02:00
|
|
|
if (count($argv) < 2) {
|
2024-03-25 17:03:46 +01:00
|
|
|
_exit("Usage: cli.php <" . implode("|", array_keys($registeredCommands)) . "> [options...]");
|
|
|
|
} else {
|
|
|
|
$command = $argv[1];
|
|
|
|
if (array_key_exists($command, $registeredCommands)) {
|
|
|
|
|
2024-03-27 14:12:01 +01:00
|
|
|
if ($database !== null && $database->getProperty("isDocker", false) && !is_file("/.dockerenv")) {
|
2024-03-27 15:15:46 +01:00
|
|
|
$requiresDockerArgs = $registeredCommands[$command]["requiresDocker"] ?? [];
|
|
|
|
$requiresDocker = $requiresDockerArgs === true || in_array($argv[2] ?? null, $requiresDockerArgs);
|
2024-03-25 17:03:46 +01:00
|
|
|
if ($requiresDocker) {
|
|
|
|
$containerName = $dockerYaml["services"]["php"]["container_name"];
|
2024-03-27 13:05:37 +01:00
|
|
|
printLine("Detected docker environment in config, running docker exec for container: $containerName");
|
2024-03-25 17:03:46 +01:00
|
|
|
$command = array_merge(["docker", "exec", "-it", $containerName, "php"], $argv);
|
|
|
|
$proc = proc_open($command, [1 => STDOUT, 2 => STDERR], $pipes);
|
|
|
|
exit(proc_close($proc));
|
|
|
|
}
|
|
|
|
}
|
2021-04-06 16:34:12 +02:00
|
|
|
|
2024-03-25 17:03:46 +01:00
|
|
|
call_user_func($registeredCommands[$command]["handler"], $argv);
|
|
|
|
} else {
|
2021-04-07 00:03:14 +02:00
|
|
|
printLine("Unknown command '$command'");
|
|
|
|
printLine();
|
2024-03-24 17:36:16 +01:00
|
|
|
printHelp($argv);
|
2021-04-06 16:34:12 +02:00
|
|
|
exit;
|
2024-03-25 17:03:46 +01:00
|
|
|
}
|
|
|
|
}
|