293 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			293 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| 
 | |
| namespace Core\API {
 | |
| 
 | |
|   use Core\API\Parameter\IntegerType;
 | |
|   use Core\API\Parameter\StringType;
 | |
|   use Core\Objects\Captcha\CaptchaProvider;
 | |
|   use Core\Objects\Context;
 | |
|   use Core\API\Parameter\ArrayType;
 | |
|   use Core\API\Parameter\Parameter;
 | |
| 
 | |
|   abstract class SettingsAPI extends Request {
 | |
| 
 | |
|     protected array $predefinedKeys;
 | |
| 
 | |
|     public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
 | |
|       parent::__construct($context, $externalCall, $params);
 | |
| 
 | |
|       // 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),
 | |
|         "mail_enabled" => new Parameter("mail_enabled", Parameter::TYPE_BOOLEAN),
 | |
|         "mail_port" => new IntegerType("mail_port", 1, 65535),
 | |
|         "rate_limiting_enabled" => new Parameter("rate_limiting_enabled", Parameter::TYPE_BOOLEAN),
 | |
|         "redis_port" => new IntegerType("redis_port", 1, 65535),
 | |
|       ];
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| namespace Core\API\Settings {
 | |
| 
 | |
|   use Core\API\Parameter\ArrayType;
 | |
|   use Core\API\Parameter\Parameter;
 | |
|   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());
 | |
|        }
 | |
| 
 | |
|        return $this->success;
 | |
|     }
 | |
| 
 | |
|     public function getContactGpgKey(): ?GpgKey {
 | |
|       return $this->contactGpgKey;
 | |
|     }
 | |
| 
 | |
|     public static function getDescription(): string {
 | |
|       return "Allows users to fetch site settings";
 | |
|     }
 | |
| 
 | |
|     public static function getDefaultPermittedGroups(): array {
 | |
|       return [Group::ADMIN];
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   class Set extends SettingsAPI {
 | |
|     public function __construct(Context $context, bool $externalCall = false) {
 | |
|       parent::__construct($context, $externalCall, array(
 | |
|         'settings' => new ArrayType("settings", Parameter::TYPE_MIXED)
 | |
|       ));
 | |
|     }
 | |
| 
 | |
|     public function _execute(): bool {
 | |
|       $values = $this->getParam("settings");
 | |
|       if (empty($values)) {
 | |
|         return $this->createError("No values given.");
 | |
|       }
 | |
| 
 | |
|       $paramKey = new RegexType('key', "[a-zA-Z_][a-zA-Z_0-9-]*");
 | |
|       $paramValueDefault = new StringType('value', 1024, true, NULL);
 | |
| 
 | |
|       $sql = $this->context->getSQL();
 | |
|       $query = $sql->insert("Settings", ["name", "value"]);
 | |
|       $keys = array();
 | |
|       $deleteKeys = array();
 | |
| 
 | |
|       foreach ($values as $key => $value) {
 | |
|         $paramValue = $this->predefinedKeys[$key] ?? $paramValueDefault;
 | |
| 
 | |
|         if (!$paramKey->parseParam($key)) {
 | |
|           $key = print_r($key, true);
 | |
|           return $this->createError("Invalid Type for key in parameter settings: '$key' (Required: " . $paramKey->getTypeName() . ")");
 | |
|         } else if (!is_null($value) && !$paramValue->parseParam($value)) {
 | |
|           $value = print_r($value, true);
 | |
|           return $this->createError("Invalid Type for value in parameter settings for key '$key': '$value' (Required: " . $paramValue->getTypeName() . ")");
 | |
|         } else {
 | |
|           if (!is_null($paramValue->value)) {
 | |
|             $query->addRow($paramKey->value, json_encode($paramValue->value));
 | |
|           } else {
 | |
|             $deleteKeys[] = $paramKey->value;
 | |
|           }
 | |
|           $keys[] = $paramKey->value;
 | |
|           $paramKey->reset();
 | |
|           $paramValue->reset();
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       if ($this->isExternalCall()) {
 | |
|         $column = $this->checkReadonly($keys);
 | |
|         if(!$this->success) {
 | |
|           return false;
 | |
|         } else if($column !== null) {
 | |
|           return $this->createError("Column '$column' is readonly.");
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       if (!empty($deleteKeys) && !$this->deleteKeys($deleteKeys)) {
 | |
|         return false;
 | |
|       }
 | |
| 
 | |
|       if (count($deleteKeys) !== count($keys)) {
 | |
|         $query->onDuplicateKeyStrategy(new UpdateStrategy(
 | |
|           ["name"],
 | |
|           ["value" => new Column("value")])
 | |
|         );
 | |
| 
 | |
|         $this->success = ($query->execute() !== FALSE);
 | |
|         $this->lastError = $sql->getLastError();
 | |
| 
 | |
|         if ($this->success) {
 | |
|           $this->logger->info("The site settings were changed");
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       return $this->success;
 | |
|     }
 | |
| 
 | |
|     private function checkReadonly(array $keys) {
 | |
|       $sql = $this->context->getSQL();
 | |
|       $res = $sql->select("name")
 | |
|         ->from("Settings")
 | |
|         ->where(new CondBool("readonly"))
 | |
|         ->where(new CondIn(new Column("name"), $keys))
 | |
|         ->first()
 | |
|         ->execute();
 | |
| 
 | |
|       $this->success = ($res !== FALSE);
 | |
|       $this->lastError = $sql->getLastError();
 | |
| 
 | |
|       if ($this->success && $res !== null) {
 | |
|         return $res["name"];
 | |
|       }
 | |
| 
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     private function deleteKeys(array $keys): bool {
 | |
|       $sql = $this->context->getSQL();
 | |
|       $res = $sql->delete("Settings")
 | |
|         ->where(new CondIn(new Column("name"), $keys))
 | |
|         ->execute();
 | |
| 
 | |
|       $this->success = ($res !== FALSE);
 | |
|       $this->lastError = $sql->getLastError();
 | |
|       return $this->success;
 | |
|     }
 | |
| 
 | |
|     public static function getDescription(): string {
 | |
|       return "Allows users to modify site settings";
 | |
|     }
 | |
| 
 | |
|     public static function getDefaultPermittedGroups(): array {
 | |
|       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];
 | |
|     }
 | |
|   }
 | |
| } |