hCaptcha Integration
This commit is contained in:
parent
aea20b7a10
commit
51ee723dcb
@ -16,7 +16,7 @@ class Info extends Request {
|
||||
$settings = $this->context->getSettings();
|
||||
$this->result["info"] = [
|
||||
"registrationAllowed" => $settings->isRegistrationAllowed(),
|
||||
"recaptchaEnabled" => $settings->isRecaptchaEnabled(),
|
||||
"captchaEnabled" => $settings->isCaptchaEnabled(),
|
||||
"version" => WEBBASE_VERSION,
|
||||
"siteName" => $settings->getSiteName(),
|
||||
];
|
||||
|
@ -3,6 +3,8 @@
|
||||
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;
|
||||
@ -20,7 +22,7 @@ namespace Core\API {
|
||||
"allowed_extensions" => new ArrayType("allowed_extensions", Parameter::TYPE_STRING),
|
||||
"trusted_domains" => new ArrayType("trusted_domains", Parameter::TYPE_STRING),
|
||||
"user_registration_enabled" => new Parameter("user_registration_enabled", Parameter::TYPE_BOOLEAN),
|
||||
"recaptcha_enabled" => new Parameter("recaptcha_enabled", Parameter::TYPE_BOOLEAN),
|
||||
"captcha_provider" => new StringType("captcha_provider", -1, true, "none", CaptchaProvider::PROVIDERS),
|
||||
"mail_enabled" => new Parameter("mail_enabled", Parameter::TYPE_BOOLEAN),
|
||||
"mail_port" => new IntegerType("mail_port", 1, 65535)
|
||||
];
|
||||
|
@ -10,28 +10,12 @@ use Core\Objects\Context;
|
||||
|
||||
class Stats extends Request {
|
||||
|
||||
private bool $mailConfigured;
|
||||
private bool $recaptchaConfigured;
|
||||
|
||||
public function __construct(Context $context, $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array());
|
||||
}
|
||||
|
||||
private function checkSettings(): bool {
|
||||
$req = new \Core\API\Settings\Get($this->context);
|
||||
$this->success = $req->execute(array("key" => "^(mail_enabled|recaptcha_enabled)$"));
|
||||
$this->lastError = $req->getLastError();
|
||||
|
||||
if ($this->success) {
|
||||
$settings = $req->getResult()["settings"];
|
||||
$this->mailConfigured = $settings["mail_enabled"];
|
||||
$this->recaptchaConfigured = $settings["recaptcha_enabled"];
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
$settings = $this->context->getSettings();
|
||||
$sql = $this->context->getSQL();
|
||||
$userCount = User::count($sql);
|
||||
$pageCount = Route::count($sql, new CondBool("active"));
|
||||
@ -54,10 +38,6 @@ class Stats extends Request {
|
||||
$loadAvg = sys_getloadavg();
|
||||
}
|
||||
|
||||
if (!$this->checkSettings()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->result["data"] = [
|
||||
"userCount" => $userCount,
|
||||
"pageCount" => $pageCount,
|
||||
@ -69,8 +49,8 @@ class Stats extends Request {
|
||||
"memory_usage" => memory_get_usage(),
|
||||
"load_avg" => $loadAvg,
|
||||
"database" => $this->context->getSQL()->getStatus(),
|
||||
"mail" => $this->mailConfigured,
|
||||
"reCaptcha" => $this->recaptchaConfigured
|
||||
"mail" => $settings->isMailEnabled(),
|
||||
"captcha" => $settings->getCaptchaProvider()?->jsonSerialize()
|
||||
],
|
||||
];
|
||||
|
||||
|
@ -727,7 +727,7 @@ namespace Core\API\User {
|
||||
);
|
||||
|
||||
$settings = $context->getSettings();
|
||||
if ($settings->isRecaptchaEnabled()) {
|
||||
if ($settings->isCaptchaEnabled()) {
|
||||
$parameters["captcha"] = new StringType("captcha");
|
||||
}
|
||||
|
||||
@ -747,7 +747,7 @@ namespace Core\API\User {
|
||||
return $this->createError("User Registration is not enabled.");
|
||||
}
|
||||
|
||||
if ($settings->isRecaptchaEnabled()) {
|
||||
if ($settings->isCaptchaEnabled()) {
|
||||
$captcha = $this->getParam("captcha");
|
||||
$req = new VerifyCaptcha($this->context);
|
||||
if (!$req->execute(array("captcha" => $captcha, "action" => "register"))) {
|
||||
@ -1003,7 +1003,7 @@ namespace Core\API\User {
|
||||
);
|
||||
|
||||
$settings = $context->getSettings();
|
||||
if ($settings->isRecaptchaEnabled()) {
|
||||
if ($settings->isCaptchaEnabled()) {
|
||||
$parameters["captcha"] = new StringType("captcha");
|
||||
}
|
||||
|
||||
@ -1021,7 +1021,7 @@ namespace Core\API\User {
|
||||
return $this->createError("The mail service is not enabled, please contact the server administration.");
|
||||
}
|
||||
|
||||
if ($settings->isRecaptchaEnabled()) {
|
||||
if ($settings->isCaptchaEnabled()) {
|
||||
$captcha = $this->getParam("captcha");
|
||||
$req = new VerifyCaptcha($this->context);
|
||||
if (!$req->execute(array("captcha" => $captcha, "action" => "resetPassword"))) {
|
||||
@ -1096,7 +1096,7 @@ namespace Core\API\User {
|
||||
);
|
||||
|
||||
$settings = $context->getSettings();
|
||||
if ($settings->isRecaptchaEnabled()) {
|
||||
if ($settings->isCaptchaEnabled()) {
|
||||
$parameters["captcha"] = new StringType("captcha");
|
||||
}
|
||||
|
||||
@ -1110,7 +1110,7 @@ namespace Core\API\User {
|
||||
}
|
||||
|
||||
$settings = $this->context->getSettings();
|
||||
if ($settings->isRecaptchaEnabled()) {
|
||||
if ($settings->isCaptchaEnabled()) {
|
||||
$captcha = $this->getParam("captcha");
|
||||
$req = new VerifyCaptcha($this->context);
|
||||
if (!$req->execute(array("captcha" => $captcha, "action" => "resendConfirmation"))) {
|
||||
|
@ -18,45 +18,15 @@ class VerifyCaptcha extends Request {
|
||||
|
||||
public function _execute(): bool {
|
||||
$settings = $this->context->getSettings();
|
||||
if (!$settings->isRecaptchaEnabled()) {
|
||||
return $this->createError("Google reCaptcha is not enabled.");
|
||||
$captchaProvider = $settings->getCaptchaProvider();
|
||||
if ($captchaProvider === null) {
|
||||
return $this->createError("No Captcha configured.");
|
||||
}
|
||||
|
||||
$url = "https://www.google.com/recaptcha/api/siteverify";
|
||||
$secret = $settings->getRecaptchaSecretKey();
|
||||
$captcha = $this->getParam("captcha");
|
||||
$action = $this->getParam("action");
|
||||
|
||||
$params = array(
|
||||
"secret" => $secret,
|
||||
"response" => $captcha
|
||||
);
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
$response = @json_decode(curl_exec($ch), true);
|
||||
curl_close($ch);
|
||||
|
||||
$this->success = false;
|
||||
$this->lastError = "Could not verify captcha: No response from google received.";
|
||||
|
||||
if ($response) {
|
||||
$this->success = $response["success"];
|
||||
if (!$this->success) {
|
||||
$this->lastError = "Could not verify captcha: " . implode(";", $response["error-codes"]);
|
||||
} else {
|
||||
$score = $response["score"];
|
||||
if ($action !== $response["action"]) {
|
||||
$this->createError("Could not verify captcha: Action does not match");
|
||||
} else if ($score < 0.7) {
|
||||
$this->createError("Could not verify captcha: Google ReCaptcha Score < 0.7 (Your score: $score), you are likely a bot");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->success = $captchaProvider->verify($captcha, $action);
|
||||
$this->lastError = $captchaProvider->getError();
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,9 @@ use Core\Driver\SQL\Condition\CondNot;
|
||||
use Core\Driver\SQL\Condition\CondRegex;
|
||||
use Core\Driver\SQL\Query\Insert;
|
||||
use Core\Driver\SQL\SQL;
|
||||
use Core\Objects\Captcha\CaptchaProvider;
|
||||
use Core\Objects\Captcha\GoogleRecaptchaProvider;
|
||||
use Core\Objects\Captcha\HCaptchaProvider;
|
||||
use Core\Objects\Context;
|
||||
|
||||
class Settings {
|
||||
@ -27,10 +30,10 @@ class Settings {
|
||||
private array $allowedExtensions;
|
||||
private string $timeZone;
|
||||
|
||||
// recaptcha
|
||||
private bool $recaptchaEnabled;
|
||||
private string $recaptchaPublicKey;
|
||||
private string $recaptchaPrivateKey;
|
||||
// captcha
|
||||
private string $captchaProvider;
|
||||
private string $captchaSiteKey;
|
||||
private string $captchaSecretKey;
|
||||
|
||||
// mail
|
||||
private bool $mailEnabled;
|
||||
@ -98,10 +101,10 @@ class Settings {
|
||||
$settings->registrationAllowed = false;
|
||||
$settings->timeZone = date_default_timezone_get();
|
||||
|
||||
// Recaptcha
|
||||
$settings->recaptchaEnabled = false;
|
||||
$settings->recaptchaPublicKey = "";
|
||||
$settings->recaptchaPrivateKey = "";
|
||||
// captcha
|
||||
$settings->captchaProvider = "none";
|
||||
$settings->captchaSiteKey = "";
|
||||
$settings->captchaSecretKey = "";
|
||||
|
||||
// Mail
|
||||
$settings->mailEnabled = false;
|
||||
@ -124,9 +127,9 @@ class Settings {
|
||||
$this->registrationAllowed = $result["user_registration_enabled"] ?? $this->registrationAllowed;
|
||||
$this->installationComplete = $result["installation_completed"] ?? $this->installationComplete;
|
||||
$this->timeZone = $result["time_zone"] ?? $this->timeZone;
|
||||
$this->recaptchaEnabled = $result["recaptcha_enabled"] ?? $this->recaptchaEnabled;
|
||||
$this->recaptchaPublicKey = $result["recaptcha_public_key"] ?? $this->recaptchaPublicKey;
|
||||
$this->recaptchaPrivateKey = $result["recaptcha_private_key"] ?? $this->recaptchaPrivateKey;
|
||||
$this->captchaProvider = $result["captcha_provider"] ?? $this->captchaProvider;
|
||||
$this->captchaSiteKey = $result["captcha_site_key"] ?? $this->captchaSiteKey;
|
||||
$this->captchaSecretKey = $result["captcha_secret_key"] ?? $this->captchaSecretKey;
|
||||
$this->mailEnabled = $result["mail_enabled"] ?? $this->mailEnabled;
|
||||
$this->mailSender = $result["mail_from"] ?? $this->mailSender;
|
||||
$this->mailFooter = $result["mail_footer"] ?? $this->mailFooter;
|
||||
@ -146,9 +149,9 @@ class Settings {
|
||||
->addRow("user_registration_enabled", json_encode($this->registrationAllowed), false, false)
|
||||
->addRow("installation_completed", json_encode($this->installationComplete), true, true)
|
||||
->addRow("time_zone", json_encode($this->timeZone), false, false)
|
||||
->addRow("recaptcha_enabled", json_encode($this->recaptchaEnabled), false, false)
|
||||
->addRow("recaptcha_public_key", json_encode($this->recaptchaPublicKey), false, false)
|
||||
->addRow("recaptcha_private_key", json_encode($this->recaptchaPrivateKey), true, false)
|
||||
->addRow("captcha_provider", json_encode($this->captchaProvider), false, false)
|
||||
->addRow("captcha_site_key", json_encode($this->captchaSiteKey), false, false)
|
||||
->addRow("captcha_secret_key", json_encode($this->captchaSecretKey), true, false)
|
||||
->addRow("allowed_extensions", json_encode($this->allowedExtensions), false, false)
|
||||
->addRow("mail_host", '""', false, false)
|
||||
->addRow("mail_port", '587', false, false)
|
||||
@ -176,16 +179,16 @@ class Settings {
|
||||
return $this->baseUrl;
|
||||
}
|
||||
|
||||
public function isRecaptchaEnabled(): bool {
|
||||
return $this->recaptchaEnabled;
|
||||
public function isCaptchaEnabled(): bool {
|
||||
return CaptchaProvider::isValid($this->captchaProvider);
|
||||
}
|
||||
|
||||
public function getRecaptchaSiteKey(): string {
|
||||
return $this->recaptchaPublicKey;
|
||||
}
|
||||
|
||||
public function getRecaptchaSecretKey(): string {
|
||||
return $this->recaptchaPrivateKey;
|
||||
public function getCaptchaProvider(): ?CaptchaProvider {
|
||||
return match ($this->captchaProvider) {
|
||||
CaptchaProvider::RECAPTCHA => new GoogleRecaptchaProvider($this->captchaSiteKey, $this->captchaSecretKey),
|
||||
CaptchaProvider::HCAPTCHA => new HCaptchaProvider($this->captchaSiteKey, $this->captchaSecretKey),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
public function isRegistrationAllowed(): bool {
|
||||
|
@ -5,6 +5,8 @@ namespace Core\Elements;
|
||||
use Core\Configuration\Settings;
|
||||
use Core\Driver\Logger\Logger;
|
||||
use Core\Driver\SQL\SQL;
|
||||
use Core\Objects\Captcha\GoogleRecaptchaProvider;
|
||||
use Core\Objects\Captcha\HCaptchaProvider;
|
||||
use Core\Objects\Context;
|
||||
use Core\Objects\Router\DocumentRoute;
|
||||
use Core\Objects\Router\Router;
|
||||
@ -78,7 +80,7 @@ abstract class Document {
|
||||
return $this->router;
|
||||
}
|
||||
|
||||
public function addCSPWhitelist(string $path) {
|
||||
public function addCSPWhitelist(string $path): void {
|
||||
$urlParts = parse_url($path);
|
||||
if (!$urlParts || !isset($urlParts["host"])) {
|
||||
$this->cspWhitelist[] = getProtocol() . "://" . getCurrentHostName() . $path;
|
||||
@ -89,7 +91,23 @@ abstract class Document {
|
||||
|
||||
public function sendHeaders(): void {
|
||||
if ($this->cspEnabled) {
|
||||
$frameSrc = [];
|
||||
|
||||
$captchaProvider = $this->getSettings()->getCaptchaProvider();
|
||||
if ($captchaProvider instanceof GoogleRecaptchaProvider) {
|
||||
$frameSrc[] = "https://www.google.com/recaptcha/";
|
||||
$frameSrc[] = "https://recaptcha.google.com/recaptcha/";
|
||||
$this->cspWhitelist[] = "https://www.google.com/recaptcha/";
|
||||
$this->cspWhitelist[] = "https://www.gstatic.com/recaptcha/";
|
||||
} else if ($captchaProvider instanceof HCaptchaProvider) {
|
||||
$frameSrc[] = "https://hcaptcha.com";
|
||||
$frameSrc[] = "https://*.hcaptcha.com";
|
||||
$this->cspWhitelist[] = "https://hcaptcha.com";
|
||||
$this->cspWhitelist[] = "https://*.hcaptcha.com";
|
||||
}
|
||||
|
||||
$cspWhiteList = implode(" ", $this->cspWhitelist);
|
||||
$frameSrc = implode(" ", $frameSrc);
|
||||
$csp = [
|
||||
"default-src $cspWhiteList 'self'",
|
||||
"object-src 'none'",
|
||||
@ -98,10 +116,8 @@ abstract class Document {
|
||||
"img-src 'self' 'unsafe-inline' data: https:;",
|
||||
"script-src $cspWhiteList 'nonce-$this->cspNonce'",
|
||||
"frame-ancestors 'self'",
|
||||
"frame-src $frameSrc 'self'",
|
||||
];
|
||||
if ($this->getSettings()->isRecaptchaEnabled()) {
|
||||
$csp[] = "frame-src https://www.google.com/ 'self'";
|
||||
}
|
||||
|
||||
$compiledCSP = implode("; ", $csp);
|
||||
header("Content-Security-Policy: $compiledCSP;");
|
||||
|
@ -28,7 +28,7 @@ abstract class Head extends View {
|
||||
protected abstract function initRawFields(): array;
|
||||
protected abstract function initTitle(): string;
|
||||
|
||||
protected function init() {
|
||||
protected function init(): void {
|
||||
$this->keywords = array();
|
||||
$this->description = "";
|
||||
$this->baseUrl = "";
|
||||
@ -51,19 +51,15 @@ abstract class Head extends View {
|
||||
public function addJS($url) { $this->sources[] = new Script(Script::MIME_TEXT_JAVASCRIPT, $url, ""); }
|
||||
public function addJSCode($code) { $this->sources[] = new Script(Script::MIME_TEXT_JAVASCRIPT, "", $code); }
|
||||
|
||||
public function loadFontawesome() {
|
||||
public function loadFontawesome(): void {
|
||||
$this->addCSS(Link::FONTAWESOME);
|
||||
}
|
||||
|
||||
public function loadGoogleRecaptcha($siteKey) {
|
||||
$this->addJS("https://www.google.com/recaptcha/api.js?render=$siteKey");
|
||||
}
|
||||
|
||||
public function loadJQuery() {
|
||||
public function loadJQuery(): void {
|
||||
$this->addJS(Script::JQUERY);
|
||||
}
|
||||
|
||||
public function loadBootstrap() {
|
||||
public function loadBootstrap(): void {
|
||||
$this->addCSS(Link::BOOTSTRAP);
|
||||
$this->addJS(Script::BOOTSTRAP);
|
||||
}
|
||||
|
@ -60,7 +60,6 @@ class HtmlDocument extends Document {
|
||||
return $code;
|
||||
}
|
||||
|
||||
|
||||
public function getTitle(): string {
|
||||
if ($this->head !== null) {
|
||||
return $this->head->getTitle();
|
||||
|
@ -80,6 +80,7 @@ class TemplateDocument extends Document {
|
||||
$session = $context->getSession();
|
||||
$settings = $this->getSettings();
|
||||
$language = $context->getLanguage();
|
||||
$captchaProvider = $settings->getCaptchaProvider();
|
||||
|
||||
$urlParts = parse_url($this->getRouter()->getRequestedUri());
|
||||
|
||||
@ -102,9 +103,10 @@ class TemplateDocument extends Document {
|
||||
"lastModified" => date(L('Y-m-d H:i:s'), @filemtime(self::getTemplatePath($name))),
|
||||
"registrationEnabled" => $settings->isRegistrationAllowed(),
|
||||
"title" => $this->title,
|
||||
"recaptcha" => [
|
||||
"key" => $settings->isRecaptchaEnabled() ? $settings->getRecaptchaSiteKey() : null,
|
||||
"enabled" => $settings->isRecaptchaEnabled(),
|
||||
"captcha" => [
|
||||
"provider" => $captchaProvider?->getName(),
|
||||
"site_key" => $captchaProvider?->getSiteKey(),
|
||||
"enabled" => $captchaProvider !== null,
|
||||
],
|
||||
"csp" => [
|
||||
"nonce" => $this->getCSPNonce(),
|
||||
|
@ -17,7 +17,7 @@ return [
|
||||
"value" => "Wert",
|
||||
"general" => "Allgemein",
|
||||
"mail" => "Mail",
|
||||
"recaptcha" => "reCaptcha",
|
||||
"captcha" => "Captcha",
|
||||
"uncategorized" => "Unkategorisiert",
|
||||
|
||||
# general settings
|
||||
@ -40,10 +40,10 @@ return [
|
||||
"mail_address" => "E-Mail Adresse",
|
||||
"send_test_email" => "Test E-Mail senden",
|
||||
|
||||
# recaptcha
|
||||
"recaptcha_enabled" => "Aktiviere Google reCaptcha",
|
||||
"recaptcha_public_key" => "reCaptcha öffentlicher Schlüssel",
|
||||
"recaptcha_private_key" => "reCaptcha privater Schlüssel",
|
||||
# captcha
|
||||
"captcha_provider" => "Captcha Anbieter",
|
||||
"captcha_site_key" => "Öffentlicher Captcha Schlüssel",
|
||||
"captcha_secret_key" => "Geheimer Captcha Schlüssel",
|
||||
|
||||
# dialog
|
||||
"fetch_settings_error" => "Fehler beim Holen der Einstellungen",
|
||||
|
@ -17,7 +17,7 @@ return [
|
||||
"value" => "Value",
|
||||
"general" => "General",
|
||||
"mail" => "Mail",
|
||||
"recaptcha" => "reCaptcha",
|
||||
"captcha" => "Captcha",
|
||||
"uncategorized" => "Uncategorized",
|
||||
|
||||
# general settings
|
||||
@ -40,10 +40,10 @@ return [
|
||||
"mail_address" => "Mail address",
|
||||
"send_test_email" => "Send test e-mail",
|
||||
|
||||
# recaptcha
|
||||
"recaptcha_enabled" => "Enable Google reCaptcha",
|
||||
"recaptcha_public_key" => "reCaptcha Public Key",
|
||||
"recaptcha_private_key" => "reCaptcha Private Key",
|
||||
# captcha
|
||||
"captcha_provider" => "Captcha Provider",
|
||||
"captcha_site_key" => "Captcha Site Key",
|
||||
"captcha_secret_key" => "Secret Captcha Key",
|
||||
|
||||
# dialog
|
||||
"fetch_settings_error" => "Error fetching settings",
|
||||
|
64
Core/Objects/Captcha/CaptchaProvider.class.php
Normal file
64
Core/Objects/Captcha/CaptchaProvider.class.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\Captcha;
|
||||
|
||||
use Core\Objects\ApiObject;
|
||||
|
||||
abstract class CaptchaProvider extends ApiObject {
|
||||
|
||||
const NONE = "none";
|
||||
const RECAPTCHA = "recaptcha";
|
||||
const HCAPTCHA = "hcaptcha";
|
||||
|
||||
const PROVIDERS = [self::NONE, self::RECAPTCHA, self::HCAPTCHA];
|
||||
|
||||
private string $siteKey;
|
||||
private string $secretKey;
|
||||
protected string $error;
|
||||
|
||||
public function __construct(string $siteKey, string $secretKey) {
|
||||
$this->siteKey = $siteKey;
|
||||
$this->secretKey = $secretKey;
|
||||
$this->error = "";
|
||||
}
|
||||
|
||||
public function getSiteKey(): string {
|
||||
return $this->siteKey;
|
||||
}
|
||||
|
||||
public function getError(): string {
|
||||
return $this->error;
|
||||
}
|
||||
|
||||
public static function isValid(string $type): bool {
|
||||
return in_array($type, [self::RECAPTCHA, self::HCAPTCHA]);
|
||||
}
|
||||
|
||||
public abstract function verify(string $captcha, string $action): bool;
|
||||
|
||||
public abstract function getName(): string;
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
return [
|
||||
"name" => $this->getName(),
|
||||
"siteKey" => $this->getSiteKey(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function performVerifyRequest(string $url, string $captcha) {
|
||||
$params = [
|
||||
"secret" => $this->secretKey,
|
||||
"response" => $captcha
|
||||
];
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
$response = @json_decode(curl_exec($ch), true);
|
||||
curl_close($ch);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
43
Core/Objects/Captcha/GoogleRecaptchaProvider.php
Normal file
43
Core/Objects/Captcha/GoogleRecaptchaProvider.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\Captcha;
|
||||
|
||||
class GoogleRecaptchaProvider extends CaptchaProvider {
|
||||
|
||||
public function __construct(string $siteKey, string $secretKey) {
|
||||
parent::__construct($siteKey, $secretKey);
|
||||
}
|
||||
|
||||
public function verify(string $captcha, string $action): bool {
|
||||
$url = "https://www.google.com/recaptcha/api/siteverify";
|
||||
|
||||
$success = false;
|
||||
$response = $this->performVerifyRequest($url, $captcha);
|
||||
$this->error = "Could not verify captcha: Invalid response from Google received.";
|
||||
|
||||
if ($response) {
|
||||
$success = $response["success"];
|
||||
if (!$success) {
|
||||
$this->error = "Could not verify captcha: " . implode(";", $response["error-codes"]);
|
||||
} else {
|
||||
$score = $response["score"];
|
||||
if ($action !== $response["action"]) {
|
||||
$success = false;
|
||||
$this->error = "Could not verify captcha: Action does not match";
|
||||
} else if ($score < 0.7) {
|
||||
$success = false;
|
||||
$this->error = "Could not verify captcha: Google ReCaptcha Score < 0.7 (Your score: $score), you are likely a bot";
|
||||
} else {
|
||||
$success = true;
|
||||
$this->error = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return CaptchaProvider::RECAPTCHA;
|
||||
}
|
||||
}
|
30
Core/Objects/Captcha/HCaptchaProvider.class.php
Normal file
30
Core/Objects/Captcha/HCaptchaProvider.class.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Objects\Captcha;
|
||||
|
||||
class HCaptchaProvider extends CaptchaProvider {
|
||||
|
||||
public function __construct(string $siteKey, string $secretKey) {
|
||||
parent::__construct($siteKey, $secretKey);
|
||||
}
|
||||
|
||||
public function verify(string $captcha, string $action): bool {
|
||||
$success = true;
|
||||
$url = "https://api.hcaptcha.com/siteverify";
|
||||
$response = $this->performVerifyRequest($url, $captcha);
|
||||
$this->error = "Could not verify captcha: Invalid response from hCaptcha received.";
|
||||
|
||||
if ($response) {
|
||||
$success = $response["success"];
|
||||
if (!$success) {
|
||||
$this->error = "Captcha verification failed.";
|
||||
}
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return CaptchaProvider::HCAPTCHA;
|
||||
}
|
||||
}
|
@ -9,8 +9,15 @@
|
||||
<link rel="stylesheet" href="/css/fontawesome.min.css" nonce="{{ site.csp.nonce }}">
|
||||
<link rel="stylesheet" href="/css/account.css" nonce="{{ site.csp.nonce }}">
|
||||
<title>{{ L("account.title")}} - {{ L(view_title) }}</title>
|
||||
{% if site.recaptcha.enabled %}
|
||||
<script src="https://www.google.com/recaptcha/api.js?render={{ site.recaptcha.key }}" nonce="{{ site.csp.nonce }}"></script>
|
||||
{% if site.captcha.enabled %}
|
||||
<script nonce="{{ site.csp.nonce }}">
|
||||
window.captchaProvider = {{ site.captcha|json_encode()|raw }};
|
||||
</script>
|
||||
{% if site.captcha.provider == 'recaptcha' %}
|
||||
<script src="https://www.google.com/recaptcha/api.js?render={{ site.captcha.site_key }}" nonce="{{ site.csp.nonce }}"></script>
|
||||
{% elseif site.captcha.provider == 'hcaptcha' %}
|
||||
<script src="https://js.hcaptcha.com/1/api.js" nonce="{{ site.csp.nonce }}"></script>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@ -30,7 +37,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if site.recaptcha.enabled %}
|
||||
<input type='hidden' value='{{ site.recaptcha.key }}' id='siteKey' />
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
@ -40,6 +40,9 @@
|
||||
<input type="password" autocomplete='new-password' name='confirmPassword'
|
||||
id='confirmPassword' class="form-control" placeholder="{{ L('account.password_confirm') }}">
|
||||
</div>
|
||||
{% if site.captcha.enabled and site.captcha.provider == 'hcaptcha' %}
|
||||
<div class="h-captcha mt-2" data-sitekey="{{ site.captcha.site_key }}"></div>
|
||||
{% endif %}
|
||||
<div class="input-group mt-3">
|
||||
<button type="button" class="btn btn-primary" id='btnRegister'>
|
||||
{{ L('general.submit') }}
|
||||
|
@ -53,6 +53,9 @@
|
||||
class="form-control" type="email" maxlength="64" />
|
||||
</div>
|
||||
</div>
|
||||
{% if site.captcha.enabled and site.captcha.provider == 'hcaptcha' %}
|
||||
<div class="h-captcha mt-2" data-sitekey="{{ site.captcha.site_key }}"></div>
|
||||
{% endif %}
|
||||
<div class="input-group mt-2">
|
||||
<button id='btnRequestPasswordReset' class='btn btn-primary'>
|
||||
{{ L('general.submit') }}
|
||||
|
@ -15,7 +15,8 @@ Web-Base is a php framework which provides basic web functionalities and a moder
|
||||
- [Localization](#localization)
|
||||
- [Command Line Interface (CLI)](#cli)
|
||||
- [Account & User functions](#access-control)
|
||||
- Admin Dashboard
|
||||
- [Google reCaptcha](https://developers.google.com/recaptcha/) and [hCaptcha](https://docs.hcaptcha.com/) Integration
|
||||
- modern ReactJS Admin Dashboard
|
||||
- Docker Support
|
||||
|
||||
### Upcoming:
|
||||
|
@ -14,7 +14,6 @@ $(document).ready(function () {
|
||||
function hideAlert() {
|
||||
$("#alertMessage").hide();
|
||||
}
|
||||
|
||||
function submitForm(btn, method, params, onSuccess) {
|
||||
let textBefore = btn.text();
|
||||
btn.prop("disabled", true);
|
||||
@ -27,6 +26,12 @@ $(document).ready(function () {
|
||||
} else {
|
||||
onSuccess();
|
||||
}
|
||||
|
||||
// reset captcha
|
||||
let captchaProvider = jsCore.getCaptchaProvider();
|
||||
if (captchaProvider?.provider === "hcaptcha") {
|
||||
hcaptcha.reset();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -75,11 +80,11 @@ $(document).ready(function () {
|
||||
} else if(password !== confirmPassword) {
|
||||
showAlert("danger", L("register.passwords_do_not_match"));
|
||||
} else {
|
||||
let captchaProvider = jsCore.getCaptchaProvider();
|
||||
let params = { username: username, email: email, password: password, confirmPassword: confirmPassword };
|
||||
if (jsCore.isRecaptchaEnabled()) {
|
||||
let siteKey = $("#siteKey").val().trim();
|
||||
if (captchaProvider?.provider === "recaptcha") {
|
||||
grecaptcha.ready(function() {
|
||||
grecaptcha.execute(siteKey, {action: 'register'}).then(function(captcha) {
|
||||
grecaptcha.execute(captchaProvider.site_key, {action: 'register'}).then(function(captcha) {
|
||||
params["captcha"] = captcha;
|
||||
submitForm(btn, "user/register", params, () => {
|
||||
showAlert("success", "Account successfully created, check your emails.");
|
||||
@ -88,6 +93,10 @@ $(document).ready(function () {
|
||||
});
|
||||
});
|
||||
} else {
|
||||
if (captchaProvider?.provider === "hcaptcha") {
|
||||
params.captcha = hcaptcha.getResponse();
|
||||
}
|
||||
|
||||
submitForm(btn, "user/register", params, () => {
|
||||
showAlert("success", "Account successfully created, check your emails.");
|
||||
$("input:not([id='siteKey'])").val("");
|
||||
@ -132,12 +141,11 @@ $(document).ready(function () {
|
||||
|
||||
let btn = $(this);
|
||||
let email = $("#email").val();
|
||||
|
||||
let captchaProvider = jsCore.getCaptchaProvider();
|
||||
let params = { email: email };
|
||||
if (jsCore.isRecaptchaEnabled()) {
|
||||
let siteKey = $("#siteKey").val().trim();
|
||||
if (captchaProvider?.provider === "recaptcha") {
|
||||
grecaptcha.ready(function() {
|
||||
grecaptcha.execute(siteKey, {action: 'resetPassword'}).then(function(captcha) {
|
||||
grecaptcha.execute(captchaProvider.site_key, {action: 'resetPassword'}).then(function(captcha) {
|
||||
params["captcha"] = captcha;
|
||||
submitForm(btn, "user/requestPasswordReset", params, () => {
|
||||
showAlert("success", "If the e-mail address exists and is linked to a account, you will receive a password reset token.");
|
||||
@ -146,6 +154,9 @@ $(document).ready(function () {
|
||||
});
|
||||
});
|
||||
} else {
|
||||
if (captchaProvider?.provider === "hcaptcha") {
|
||||
params.captcha = hcaptcha.getResponse();
|
||||
}
|
||||
submitForm(btn, "user/requestPasswordReset", params, () => {
|
||||
showAlert("success", "If the e-mail address exists and is linked to a account, you will receive a password reset token.");
|
||||
$("input:not([id='siteKey'])").val("");
|
||||
@ -191,11 +202,11 @@ $(document).ready(function () {
|
||||
|
||||
let btn = $(this);
|
||||
let email = $("#email").val();
|
||||
let captchaProvider = jsCore.getCaptchaProvider();
|
||||
let params = { email: email };
|
||||
if (jsCore.isRecaptchaEnabled()) {
|
||||
let siteKey = $("#siteKey").val().trim();
|
||||
if (captchaProvider?.provider === "recaptcha") {
|
||||
grecaptcha.ready(function() {
|
||||
grecaptcha.execute(siteKey, {action: 'resendConfirmation'}).then(function(captcha) {
|
||||
grecaptcha.execute(captchaProvider.site_key, {action: 'resendConfirmation'}).then(function(captcha) {
|
||||
params["captcha"] = captcha;
|
||||
submitForm(btn, "user/resendConfirmEmail", params, () => {
|
||||
showAlert("success", "If the e-mail address exists and is linked to a account, you will receive a new confirmation email.");
|
||||
@ -204,6 +215,10 @@ $(document).ready(function () {
|
||||
});
|
||||
});
|
||||
} else {
|
||||
if (captchaProvider?.provider === "hcaptcha") {
|
||||
params.captcha = hcaptcha.getResponse();
|
||||
}
|
||||
|
||||
submitForm(btn, "user/resendConfirmEmail", params, () => {
|
||||
showAlert("success", "\"If the e-mail address exists and is linked to a account, you will receive a new confirmation email.");
|
||||
$("input:not([id='siteKey'])").val("");
|
||||
|
@ -162,8 +162,8 @@ let Core = function () {
|
||||
return this.getJsonDateTime(date).split(' ')[1];
|
||||
};
|
||||
|
||||
this.isRecaptchaEnabled = function () {
|
||||
return (typeof grecaptcha !== 'undefined');
|
||||
this.getCaptchaProvider = function () {
|
||||
return window.captchaProvider || null;
|
||||
}
|
||||
|
||||
this.__construct();
|
||||
|
@ -18,13 +18,12 @@ import {Link} from "react-router-dom";
|
||||
import {
|
||||
Add,
|
||||
Delete,
|
||||
Google,
|
||||
LibraryBooks,
|
||||
Mail,
|
||||
RestartAlt,
|
||||
Save,
|
||||
Send,
|
||||
SettingsApplications
|
||||
SettingsApplications, SmartToy
|
||||
} from "@mui/icons-material";
|
||||
import TIME_ZONES from "shared/time-zones";
|
||||
import ButtonBar from "../../elements/button-bar";
|
||||
@ -63,10 +62,10 @@ export default function SettingsView(props) {
|
||||
"mail_password",
|
||||
"mail_async",
|
||||
],
|
||||
"recaptcha": [
|
||||
"recaptcha_enabled",
|
||||
"recaptcha_private_key",
|
||||
"recaptcha_public_key",
|
||||
"captcha": [
|
||||
"captcha_provider",
|
||||
"captcha_secret_key",
|
||||
"captcha_site_key",
|
||||
],
|
||||
"hidden": ["installation_completed", "mail_last_sync"]
|
||||
};
|
||||
@ -280,11 +279,11 @@ export default function SettingsView(props) {
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
];
|
||||
} else if (selectedTab === "recaptcha") {
|
||||
} else if (selectedTab === "captcha") {
|
||||
return [
|
||||
renderCheckBox("recaptcha_enabled"),
|
||||
renderTextInput("recaptcha_public_key", !parseBool(settings.recaptcha_enabled)),
|
||||
renderPasswordInput("recaptcha_private_key", !parseBool(settings.recaptcha_enabled)),
|
||||
renderSelection("captcha_provider", ["none", "recaptcha", "hcaptcha"]),
|
||||
renderTextInput("captcha_site_key", settings.captcha_provider === "none"),
|
||||
renderPasswordInput("captcha_secret_key", settings.captcha_provider === "none"),
|
||||
];
|
||||
} else if (selectedTab === "uncategorized") {
|
||||
return <TableContainer component={Paper}>
|
||||
@ -364,8 +363,8 @@ export default function SettingsView(props) {
|
||||
icon={<SettingsApplications />} iconPosition={"start"} />
|
||||
<Tab value={"mail"} label={L("settings.mail")}
|
||||
icon={<Mail />} iconPosition={"start"} />
|
||||
<Tab value={"recaptcha"} label={L("settings.recaptcha")}
|
||||
icon={<Google />} iconPosition={"start"} />
|
||||
<Tab value={"captcha"} label={L("settings.captcha")}
|
||||
icon={<SmartToy />} iconPosition={"start"} />
|
||||
<Tab value={"uncategorized"} label={L("settings.uncategorized")}
|
||||
icon={<LibraryBooks />} iconPosition={"start"} />
|
||||
</Tabs>
|
||||
|
Loading…
Reference in New Issue
Block a user