2.4.1: Settings GPG, Localization, CLI DB migrate, minor improvements

This commit is contained in:
2024-05-11 16:12:15 +02:00
parent 7920d3164d
commit 150e4eb195
28 changed files with 636 additions and 241 deletions

View File

@@ -10,7 +10,6 @@ namespace Core\API {
$this->loginRequired = true;
}
}
}
namespace Core\API\GpgKey {
@@ -20,6 +19,7 @@ namespace Core\API\GpgKey {
use Core\API\Parameter\Parameter;
use Core\API\Parameter\StringType;
use Core\API\Template\Render;
use Core\API\Traits\GpgKeyValidation;
use Core\Driver\SQL\Condition\Compare;
use Core\Objects\Context;
use Core\Objects\DatabaseEntity\GpgKey;
@@ -28,36 +28,16 @@ namespace Core\API\GpgKey {
class Import extends GpgKeyAPI {
use GpgKeyValidation;
public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall, [
"pubkey" => new StringType("pubkey")
"publicKey" => new StringType("publicKey")
]);
$this->loginRequired = true;
$this->forbidMethod("GET");
}
private function testKey(string $keyString) {
$res = GpgKey::getKeyInfo($keyString);
if (!$res["success"]) {
return $this->createError($res["error"] ?? $res["msg"]);
}
$keyData = $res["data"];
$keyType = $keyData["type"];
$expires = $keyData["expires"];
if ($keyType === "sec#") {
return self::createError("ATTENTION! It seems like you've imported a PGP PRIVATE KEY instead of a public key.
It is recommended to immediately revoke your private key and create a new key pair.");
} else if ($keyType !== "pub") {
return self::createError("Unknown key type: $keyType");
} else if (isInPast($expires)) {
return self::createError("It seems like the gpg key is already expired.");
} else {
return $keyData;
}
}
public function _execute(): bool {
$currentUser = $this->context->getUser();
@@ -69,8 +49,7 @@ namespace Core\API\GpgKey {
}
// fix key first, enforce a newline after
$keyString = $this->getParam("pubkey");
$keyString = preg_replace("/(-{2,})\n([^\n])/", "$1\n\n$2", $keyString);
$keyString = $this->formatKey($this->getParam("publicKey"));
$keyData = $this->testKey($keyString);
if ($keyData === false) {
return false;

View File

@@ -215,38 +215,15 @@ abstract class Request {
return false;
}
if ($this->isMethodAllowed("GET") && $this->isMethodAllowed("POST")) {
$values = $_REQUEST;
} else if ($this->isMethodAllowed("POST")) {
$values = $_POST;
} else if ($this->isMethodAllowed("GET")) {
$values = $_GET;
}
if (in_array($_SERVER['REQUEST_METHOD'], ['POST', 'PUT', 'PATCH'])) {
$contentTypeData = explode(";", $_SERVER["CONTENT_TYPE"] ?? "");
$charset = "utf-8";
if ($contentTypeData[0] === "application/json") {
for ($i = 1; $i < count($contentTypeData); $i++) {
if (preg_match("/charset=(.*)/", $contentTypeData[$i], $match)) {
$charset = $match[1];
}
}
$body = file_get_contents('php://input');
if (strcasecmp($charset, "utf-8") !== 0) {
$body = iconv($charset, 'utf-8', $body);
}
$jsonData = json_decode($body, true);
if ($jsonData !== null) {
$values = array_merge($values, $jsonData);
} else {
$this->lastError = "Invalid request body.";
http_response_code(400);
return false;
}
$values = $_REQUEST;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && in_array("application/json", explode(";", $_SERVER["CONTENT_TYPE"] ?? ""))) {
$jsonData = json_decode(file_get_contents('php://input'), true);
if ($jsonData !== null) {
$values = array_merge($values, $jsonData);
} else {
$this->lastError = 'Invalid request body.';
http_response_code(400);
return false;
}
}
@@ -362,7 +339,8 @@ abstract class Request {
$obj = $this->params;
}
return $obj[$name]?->value;
// I don't know why phpstorm
return (isset($obj[$name]) ? $obj[$name]->value : NULL);
}
public function isMethodAllowed(string $method): bool {

View File

@@ -19,6 +19,7 @@ namespace Core\API {
// API parameters should be more configurable, e.g. allow regexes, min/max values for numbers, etc.
$this->predefinedKeys = [
"allowed_extensions" => new ArrayType("allowed_extensions", Parameter::TYPE_STRING),
"mail_contact" => new Parameter("mail_contact", Parameter::TYPE_EMAIL, true, ""),
"trusted_domains" => new ArrayType("trusted_domains", Parameter::TYPE_STRING),
"user_registration_enabled" => new Parameter("user_registration_enabled", Parameter::TYPE_BOOLEAN),
"captcha_provider" => new StringType("captcha_provider", -1, true, "disabled", CaptchaProvider::PROVIDERS),
@@ -38,29 +39,41 @@ namespace Core\API\Settings {
use Core\API\Parameter\RegexType;
use Core\API\Parameter\StringType;
use Core\API\SettingsAPI;
use Core\API\Traits\GpgKeyValidation;
use Core\Configuration\Settings;
use Core\Driver\SQL\Column\Column;
use Core\Driver\SQL\Condition\CondBool;
use Core\Driver\SQL\Condition\CondIn;
use Core\Driver\SQL\Strategy\UpdateStrategy;
use Core\Objects\Context;
use Core\Objects\DatabaseEntity\GpgKey;
use Core\Objects\DatabaseEntity\Group;
class Get extends SettingsAPI {
private ?GpgKey $contactGpgKey;
public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall, array(
'key' => new StringType('key', -1, true, NULL)
));
$this->contactGpgKey = null;
}
public function _execute(): bool {
$key = $this->getParam("key");
$sql = $this->context->getSQL();
$siteSettings = $this->context->getSettings();
$settings = Settings::getAll($sql, $key, $this->isExternalCall());
if ($settings !== null) {
$this->result["settings"] = $settings;
// TODO: improve this custom key
$gpgKeyId = $this->result["settings"]["mail_contact_gpg_key_id"] ?? null;
$this->contactGpgKey = $gpgKeyId === null ? null : GpgKey::find($sql, $gpgKeyId);
unset($this->result["settings"]["mail_contact_gpg_key_id"]);
$this->result["settings"]["mail_contact_gpg_key"] = $this->contactGpgKey?->jsonSerialize();
} else {
return $this->createError("Error fetching settings: " . $sql->getLastError());
}
@@ -68,6 +81,10 @@ namespace Core\API\Settings {
return $this->success;
}
public function getContactGpgKey(): ?GpgKey {
return $this->contactGpgKey;
}
public static function getDescription(): string {
return "Allows users to fetch site settings";
}
@@ -138,7 +155,6 @@ namespace Core\API\Settings {
["value" => new Column("value")])
);
$this->success = ($query->execute() !== FALSE);
$this->lastError = $sql->getLastError();
@@ -188,4 +204,90 @@ namespace Core\API\Settings {
return [Group::ADMIN];
}
}
class ImportGPG extends SettingsAPI {
use GpgKeyValidation;
public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall, [
"publicKey" => new StringType("publicKey")
]);
$this->forbidMethod("GET");
}
protected function _execute(): bool {
$sql = $this->context->getSQL();
// fix key first, enforce a newline after
$keyString = $this->formatKey($this->getParam("publicKey"));
$keyData = $this->testKey($keyString, null);
if ($keyData === false) {
return false;
}
$res = GpgKey::importKey($keyString);
if (!$res["success"]) {
return $this->createError($res["error"]);
}
// we will auto-confirm this key
$sql = $this->context->getSQL();
$gpgKey = new GpgKey($keyData["fingerprint"], $keyData["algorithm"], $keyData["expires"], true);
if (!$gpgKey->save($sql)) {
return $this->createError("Error creating gpg key: " . $sql->getLastError());
}
$this->success = $sql->insert("Settings", ["name", "value", "private", "readonly"])
->addRow("mail_contact_gpg_key_id", $gpgKey->getId(), false, true)
->onDuplicateKeyStrategy(new UpdateStrategy(
["name"],
["value" => new Column("value")])
)->execute() !== false;
$this->lastError = $sql->getLastError();
$this->result["gpgKey"] = $gpgKey->jsonSerialize();
return $this->success;
}
public static function getDescription(): string {
return "Allows administrators to import a GPG-key to use it as a contact key.";
}
public static function getDefaultPermittedGroups(): array {
return [Group::ADMIN];
}
}
class RemoveGPG extends SettingsAPI {
public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall);
}
protected function _execute(): bool {
$sql = $this->context->getSQL();
$settings = $this->context->getSettings();
$gpgKey = $settings->getContactGPGKey();
if ($gpgKey === null) {
return $this->createError("No GPG-Key configured yet");
}
$this->success = $sql->update("Settings")
->set("value", NULL)
->whereEq("name", "mail_contact_gpg_key_id")
->execute() !== false;
$this->lastError = $sql->getLastError();
return $this->success;
}
public static function getDescription(): string {
return "Allows administrators to remove the GPG-key used as a contact key.";
}
public static function getDefaultPermittedGroups(): array {
return [Group::ADMIN];
}
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Core\API\Traits;
use Core\Objects\DatabaseEntity\GpgKey;
trait GpgKeyValidation {
function testKey(string $keyString, ?string $expectedType = "pub") {
$res = GpgKey::getKeyInfo($keyString);
if (!$res["success"]) {
return $this->createError($res["error"] ?? $res["msg"]);
}
$keyData = $res["data"];
$keyType = $keyData["type"];
$expires = $keyData["expires"];
if ($expectedType === "pub" && $keyType === "sec#") {
return $this->createError("ATTENTION! It seems like you've imported a PGP PRIVATE KEY instead of a public key.
It is recommended to immediately revoke your private key and create a new key pair.");
} else if ($expectedType !== null && $keyType !== $expectedType) {
return $this->createError("Key has unexpected type: $keyType, expected: $expectedType");
} else if (isInPast($expires)) {
return $this->createError("It seems like the gpg key is already expired.");
} else {
return $keyData;
}
}
function formatKey(string $keyString): string {
return preg_replace("/(-{2,})\n([^\n])/", "$1\n\n$2", $keyString);
}
}

View File

@@ -1008,7 +1008,15 @@ namespace Core\API\User {
} else {
$this->success = ($user->delete($sql) !== FALSE);
$this->lastError = $sql->getLastError();
$this->logger->info(sprintf(
"User '%s' (id=%d) deleted by %s",
$user->getDisplayName(),
$id,
$this->logUserId())
);
}
} else {
$this->lastError = $sql->getLastError();
}
return $this->success;