Google reCaptcha

This commit is contained in:
2020-06-26 23:32:45 +02:00
parent 9442a120ab
commit cd6c28c9b3
16 changed files with 552 additions and 115 deletions

View File

@@ -0,0 +1,115 @@
<?php
namespace Api {
class ContactAPI extends Request {
}
}
namespace Api\Contact {
use Api\ContactAPI;
use Api\Parameter\Parameter;
use Api\Parameter\StringType;
use Api\VerifyCaptcha;
use Objects\User;
class Request extends ContactAPI {
private int $notificationId;
private int $contactRequestId;
public function __construct(User $user, bool $externalCall = false) {
$parameters = array(
'fromName' => new StringType('fromName', 32),
'fromEmail' => new Parameter('fromEmail', Parameter::TYPE_EMAIL),
'message' => new StringType('message', 512),
);
$settings = $user->getConfiguration()->getSettings();
if ($settings->isRecaptchaEnabled()) {
$parameters["captcha"] = new StringType("captcha");
}
parent::__construct($user, $externalCall, $parameters);
}
public function execute($values = array()) {
if (!parent::execute($values)) {
return false;
}
$settings = $this->user->getConfiguration()->getSettings();
if ($settings->isRecaptchaEnabled()) {
$captcha = $this->getParam("captcha");
$req = new VerifyCaptcha($this->user);
if (!$req->execute(array("captcha" => $captcha, "action" => "contact"))) {
return $this->createError($req->getLastError());
}
}
if (!$this->insertContactRequest()) {
return false;
}
$this->createNotification();
if (!$this->success) {
return $this->createError("The contact request was saved, but the server was unable to create a notification.");
}
return $this->success;
}
private function insertContactRequest() {
$sql = $this->user->getSQL();
$name = $this->getParam("fromName");
$email = $this->getParam("fromEmail");
$message = $this->getParam("message");
$res = $sql->insert("ContactRequest", array("from_name", "from_email", "message"))
->addRow($name, $email, $message)
->returning("uid")
->execute();
$this->success = ($res !== FALSE);
$this->lastError = $sql->getLastError();
if ($this->success) {
$this->contactRequestId = $sql->getLastInsertId();
}
return $this->success;
}
private function createNotification() {
$sql = $this->user->getSQL();
$name = $this->getParam("fromName");
$email = $this->getParam("fromEmail");
$message = $this->getParam("message");
$res = $sql->insert("Notification", array("title", "message", "type"))
->addRow("New Contact Request from: $name", "$name ($email) wrote:\n$message", "message")
->returning("uid")
->execute();
$this->success = ($res !== FALSE);
$this->lastError = $sql->getLastError();
if ($this->success) {
$this->notificationId = $sql->getLastInsertId();
$res = $sql->insert("GroupNotification", array("group_id", "notification_id"))
->addRow(USER_GROUP_ADMIN, $this->notificationId)
->addRow(USER_GROUP_SUPPORT, $this->notificationId)
->execute();
$this->success = ($res !== FALSE);
$this->lastError = $sql->getLastError();
}
return $this->success;
}
}
}

View File

@@ -1,11 +1,9 @@
<?php
namespace Api {
class NotificationsAPI extends Request {
}
}
namespace Api\Notifications {
@@ -14,6 +12,9 @@ namespace Api\Notifications {
use Api\Parameter\Parameter;
use Api\Parameter\StringType;
use Driver\SQL\Condition\Compare;
use Driver\SQL\Condition\CondIn;
use Driver\SQL\Query\Select;
use Objects\User;
class Create extends NotificationsAPI {
@@ -144,67 +145,67 @@ namespace Api\Notifications {
class Fetch extends NotificationsAPI {
private array $notifications;
private array $notificationids;
public function __construct($user, $externalCall = false) {
parent::__construct($user, $externalCall, array());
parent::__construct($user, $externalCall, array(
'new' => new Parameter('new', Parameter::TYPE_BOOLEAN, true, true)
));
$this->loginRequired = true;
}
private function fetchUserNotifications() {
$onlyNew = $this->getParam('new');
$userId = $this->user->getId();
$sql = $this->user->getSQL();
$res = $sql->select($sql->distinct("Notification.uid"), "created_at", "title", "message")
$query = $sql->select($sql->distinct("Notification.uid"), "created_at", "title", "message", "type")
->from("Notification")
->innerJoin("UserNotification", "UserNotification.notification_id", "Notification.uid")
->where(new Compare("UserNotification.user_id", $userId))
->where(new Compare("UserNotification.seen", false))
->orderBy("created_at")->descending()
->execute();
->orderBy("created_at")->descending();
$this->success = ($res !== FALSE);
$this->lastError = $sql->getLastError();
if ($this->success) {
foreach($res as $row) {
$id = $row["uid"];
if (!isset($this->notifications[$id])) {
$this->notifications[$id] = array(
"uid" => $id,
"title" => $row["title"],
"message" => $row["message"],
"created_at" => $row["created_at"],
);
}
}
if ($onlyNew) {
$query->where(new Compare("UserNotification.seen", false));
}
return $this->success;
return $this->fetchNotifications($query);
}
private function fetchGroupNotifications() {
$onlyNew = $this->getParam('new');
$userId = $this->user->getId();
$sql = $this->user->getSQL();
$res = $sql->select($sql->distinct("Notification.uid"), "created_at", "title", "message")
$query = $sql->select($sql->distinct("Notification.uid"), "created_at", "title", "message", "type")
->from("Notification")
->innerJoin("GroupNotification", "GroupNotification.notification_id", "Notification.uid")
->innerJoin("UserGroup", "GroupNotification.group_id", "UserGroup.group_id")
->where(new Compare("UserGroup.user_id", $userId))
->where(new Compare("GroupNotification.seen", false))
->orderBy("created_at")->descending()
->execute();
->orderBy("created_at")->descending();
if ($onlyNew) {
$query->where(new Compare("GroupNotification.seen", false));
}
return $this->fetchNotifications($query);
}
private function fetchNotifications(Select $query) {
$sql = $this->user->getSQL();
$res = $query->execute();
$this->success = ($res !== FALSE);
$this->lastError = $sql->getLastError();
if ($this->success) {
foreach($res as $row) {
$id = $row["uid"];
if (!isset($this->notifications[$id])) {
if (!in_array($id, $this->notificationids)) {
$this->notificationids[] = $id;
$this->notifications[] = array(
"uid" => $id,
"title" => $row["title"],
"message" => $row["message"],
"created_at" => $row["created_at"],
"type" => $row["type"]
);
}
}
@@ -219,6 +220,7 @@ namespace Api\Notifications {
}
$this->notifications = array();
$this->notificationids = array();
if ($this->fetchUserNotifications() && $this->fetchGroupNotifications()) {
$this->result["notifications"] = $this->notifications;
}
@@ -227,4 +229,41 @@ namespace Api\Notifications {
}
}
class Seen extends NotificationsAPI {
public function __construct(User $user, bool $externalCall = false) {
parent::__construct($user, $externalCall, array());
$this->loginRequired = true;
}
public function execute($values = array()) {
if (!parent::execute($values)) {
return false;
}
$sql = $this->user->getSQL();
$res = $sql->update("UserNotification")
->set("seen", true)
->where(new Compare("user_id", $this->user->getId()))
->execute();
$this->success = ($res !== FALSE);
$this->lastError = $sql->getLastError();
if ($this->success) {
$res = $sql->update("GroupNotification")
->set("seen", true)
->where(new CondIn("group_id",
$sql->select("group_id")
->from("UserGroup")
->where(new Compare("user_id", $this->user->getId()))))
->execute();
$this->success = ($res !== FALSE);
$this->lastError = $sql->getLastError();
}
return $this->success;
}
}
}

View File

@@ -48,15 +48,17 @@ class Request {
}
public function parseParams($values) {
foreach($this->params as $name => $param) {
$value = $values[$name] ?? NULL;
if(!$param->optional && (is_null($value) || empty($value))) {
$isEmpty = (is_string($value) || is_array($value)) && empty($value);
if(!$param->optional && (is_null($value) || $isEmpty)) {
$this->lastError = 'Missing parameter: ' . $name;
return false;
}
if(!is_null($value) && !empty($value)) {
if(!is_null($value) && !$isEmpty) {
if(!$param->parseParam($value)) {
$value = print_r($value, true);
$this->lastError = "Invalid Type for parameter: $name '$value' (Required: " . $param->getTypeName() . ")";

View File

@@ -120,6 +120,7 @@ namespace Api\User {
use Api\Parameter\StringType;
use Api\SendMail;
use Api\UserAPI;
use Api\VerifyCaptcha;
use DateTime;
use Driver\SQL\Condition\Compare;
use Driver\SQL\Condition\CondIn;
@@ -531,13 +532,20 @@ namespace Api\User {
private ?int $userId;
private string $token;
public function __construct($user, $externalCall = false) {
parent::__construct($user, $externalCall, array(
public function __construct(User $user, bool $externalCall = false) {
$parameters = array(
"username" => new StringType("username", 32),
'email' => new Parameter('email', Parameter::TYPE_EMAIL),
"password" => new StringType("password"),
"confirmPassword" => new StringType("confirmPassword"),
));
);
$settings = $user->getConfiguration()->getSettings();
if ($settings->isRecaptchaEnabled()) {
$parameters["captcha"] = new StringType("captcha");
}
parent::__construct($user, $externalCall, $parameters);
}
private function insertToken() {
@@ -582,6 +590,15 @@ namespace Api\User {
return $this->createError("User Registration is not enabled.");
}
$settings = $this->user->getConfiguration()->getSettings();
if ($settings->isRecaptchaEnabled()) {
$captcha = $this->getParam("captcha");
$req = new VerifyCaptcha($this->user);
if (!$req->execute(array("captcha" => $captcha, "action" => "register"))) {
return $this->createError($req->getLastError());
}
}
$username = $this->getParam("username");
$email = $this->getParam('email');
if (!$this->userExists($username, $email)) {

View File

@@ -0,0 +1,67 @@
<?php
namespace Api;
use Api\Parameter\StringType;
use Objects\User;
class VerifyCaptcha extends Request {
public function __construct(User $user, bool $externalCall = false) {
parent::__construct($user, $externalCall, array(
"captcha" => new StringType("captcha"),
"action" => new StringType("action"),
));
$this->isPublic = false;
}
public function execute($values = array()) {
if(!parent::execute($values)) {
return false;
}
$settings = $this->user->getConfiguration()->getSettings();
if (!$settings->isRecaptchaEnabled()) {
return $this->createError("Google reCaptcha is not enabled.");
}
$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");
}
}
}
return $this->success;
}
}

View File

@@ -88,6 +88,7 @@ class CreateDatabase {
$queries[] = $sql->createTable("Notification")
->addSerial("uid")
->addEnum("type", array("default","message","warning"), false, "default")
->addDateTime("created_at", false, $sql->currentTimestamp())
->addString("title", 32)
->addString("message", 256)
@@ -96,7 +97,7 @@ class CreateDatabase {
$queries[] = $sql->createTable("UserNotification")
->addInt("user_id")
->addInt("notification_id")
->addBool("seen")
->addBool("seen", false)
->foreignKey("user_id", "User", "uid")
->foreignKey("notification_id", "Notification", "uid")
->unique("user_id", "notification_id");
@@ -104,7 +105,7 @@ class CreateDatabase {
$queries[] = $sql->createTable("GroupNotification")
->addInt("group_id")
->addInt("notification_id")
->addBool("seen")
->addBool("seen", false)
->foreignKey("group_id", "Group", "uid")
->foreignKey("notification_id", "Notification", "uid")
->unique("group_id", "notification_id");
@@ -144,8 +145,8 @@ class CreateDatabase {
$queries[] = $sql->createTable("Settings")
->addString("name", 32)
->addString("value", 1024, true)
->addBool("private", false)
->addBool("readonly", false)
->addBool("private", false) // these values are not returned from '/api/settings/get', but can be changed
->addBool("readonly", false) // these values are neither returned, nor can be changed from outside
->primaryKey("name");
$settingsQuery = $sql->insert("Settings", array("name", "value", "private", "readonly"))
@@ -162,6 +163,14 @@ class CreateDatabase {
(Settings::loadDefaults())->addRows($settingsQuery);
$queries[] = $settingsQuery;
$queries[] = $sql->createTable("ContactRequest")
->addSerial("uid")
->addString("from_name", 32)
->addString("from_email", 64)
->addString("message", 512)
->addDateTime("created_at", false, $sql->currentTimestamp())
->primaryKey("uid");
return $queries;
}

View File

@@ -16,6 +16,9 @@ class Settings {
private string $jwtSecret;
private bool $installationComplete;
private bool $registrationAllowed;
private bool $recaptchaEnabled;
private string $recaptchaPublicKey;
private string $recaptchaPrivateKey;
public function getJwtSecret(): string {
return $this->jwtSecret;
@@ -36,6 +39,9 @@ class Settings {
$settings->jwtSecret = $jwt;
$settings->installationComplete = false;
$settings->registrationAllowed = false;
$settings->recaptchaPublicKey = "";
$settings->recaptchaPrivateKey = "";
$settings->recaptchaEnabled = false;
return $settings;
}
@@ -49,6 +55,9 @@ class Settings {
$this->registrationAllowed = $result["user_registration_enabled"] ?? $this->registrationAllowed;
$this->installationComplete = $result["installation_completed"] ?? $this->installationComplete;
$this->jwtSecret = $result["jwt_secret"] ?? $this->jwtSecret;
$this->recaptchaEnabled = $result["recaptcha_enabled"] ?? $this->recaptchaEnabled;
$this->recaptchaPublicKey = $result["recaptcha_public_key"] ?? $this->recaptchaPublicKey;
$this->recaptchaPrivateKey = $result["recaptcha_private_key"] ?? $this->recaptchaPrivateKey;
if (!isset($result["jwt_secret"])) {
$req = new \Api\Settings\Set($user);
@@ -66,7 +75,10 @@ class Settings {
->addRow("base_url", $this->baseUrl, false, false)
->addRow("user_registration_enabled", $this->registrationAllowed ? "1" : "0", false, false)
->addRow("installation_completed", $this->installationComplete ? "1" : "0", true, true)
->addRow("jwt_secret", $this->jwtSecret, true, true);
->addRow("jwt_secret", $this->jwtSecret, true, true)
->addRow("recaptcha_enabled", $this->recaptchaEnabled ? "1" : "0", false, false)
->addRow("recaptcha_public_key", $this->recaptchaPublicKey, false, false)
->addRow("recaptcha_private_key", $this->recaptchaPrivateKey, true, false);
}
public function getSiteName() {
@@ -76,4 +88,16 @@ class Settings {
public function getBaseUrl() {
return $this->baseUrl;
}
public function isRecaptchaEnabled() {
return $this->recaptchaEnabled;
}
public function getRecaptchaSiteKey() {
return $this->recaptchaPublicKey;
}
public function getRecaptchaSecretKey() {
return $this->recaptchaPrivateKey;
}
}

View File

@@ -5,13 +5,13 @@ namespace Driver\SQL\Condition;
class CondIn extends Condition {
private string $column;
private array $values;
private $expression;
public function __construct(string $column, array $values) {
public function __construct(string $column, $expression) {
$this->column = $column;
$this->values = $values;
$this->expression = $expression;
}
public function getColumn() { return $this->column; }
public function getValues() { return $this->values; }
public function getExpression() { return $this->expression; }
}

View File

@@ -10,7 +10,6 @@ use Driver\SQL\Condition\Condition;
use Driver\SQL\Condition\CondKeyword;
use Driver\SQL\Condition\CondNot;
use Driver\SQL\Condition\CondOr;
use Driver\SQL\Condition\CondRegex;
use Driver\SQL\Constraint\Constraint;
use \Driver\SQL\Constraint\Unique;
use \Driver\SQL\Constraint\PrimaryKey;
@@ -18,6 +17,7 @@ use \Driver\SQL\Constraint\ForeignKey;
use Driver\SQL\Query\CreateTable;
use Driver\SQL\Query\Delete;
use Driver\SQL\Query\Insert;
use Driver\SQL\Query\Query;
use Driver\SQL\Query\Select;
use Driver\SQL\Query\Truncate;
use Driver\SQL\Query\Update;
@@ -50,27 +50,27 @@ abstract class SQL {
}
public function createTable($tableName) {
return new Query\CreateTable($this, $tableName);
return new CreateTable($this, $tableName);
}
public function insert($tableName, $columns=array()) {
return new Query\Insert($this, $tableName, $columns);
return new Insert($this, $tableName, $columns);
}
public function select(...$columNames) {
return new Query\Select($this, $columNames);
return new Select($this, $columNames);
}
public function truncate($table) {
return new Query\Truncate($this, $table);
return new Truncate($this, $table);
}
public function delete($table) {
return new Query\Delete($this, $table);
return new Delete($this, $table);
}
public function update($table) {
return new Query\Update($this, $table);
return new Update($this, $table);
}
// ####################
@@ -86,6 +86,53 @@ abstract class SQL {
public abstract function disconnect();
// Querybuilder
protected function buildQuery(Query $query, array &$params) {
if ($query instanceof Select) {
$select = $query;
$columns = $this->columnName($select->getColumns());
$tables = $select->getTables();
if (!$tables) {
return $this->execute("SELECT $columns", $params, true);
}
$tables = $this->tableName($tables);
$where = $this->getWhereClause($select->getConditions(), $params);
$joinStr = "";
$joins = $select->getJoins();
if (!empty($joins)) {
foreach($joins as $join) {
$type = $join->getType();
$joinTable = $this->tableName($join->getTable());
$columnA = $this->columnName($join->getColumnA());
$columnB = $this->columnName($join->getColumnB());
$joinStr .= " $type JOIN $joinTable ON $columnA=$columnB";
}
}
$groupBy = "";
$groupColumns = $select->getGroupBy();
if (!empty($groupColumns)) {
$groupBy = " GROUP BY " . $this->columnName($groupColumns);
}
$orderBy = "";
$orderColumns = $select->getOrderBy();
if (!empty($orderColumns)) {
$orderBy = " ORDER BY " . $this->columnName($orderColumns);
$orderBy .= ($select->isOrderedAscending() ? " ASC" : " DESC");
}
$limit = ($select->getLimit() > 0 ? (" LIMIT " . $select->getLimit()) : "");
$offset = ($select->getOffset() > 0 ? (" OFFSET " . $select->getOffset()) : "");
return "SELECT $columns FROM $tables$joinStr$where$groupBy$orderBy$limit$offset";
} else {
$this->lastError = "buildQuery() not implemented for type: " . get_class($query);
return FALSE;
}
}
public function executeCreateTable(CreateTable $createTable) {
$tableName = $this->tableName($createTable->getTableName());
$ifNotExists = $createTable->ifNotExists() ? " IF NOT EXISTS": "";
@@ -161,46 +208,8 @@ abstract class SQL {
}
public function executeSelect(Select $select) {
$columns = $this->columnName($select->getColumns());
$tables = $select->getTables();
$params = array();
if (!$tables) {
return $this->execute("SELECT $columns", $params, true);
}
$tables = $this->tableName($tables);
$where = $this->getWhereClause($select->getConditions(), $params);
$joinStr = "";
$joins = $select->getJoins();
if (!empty($joins)) {
foreach($joins as $join) {
$type = $join->getType();
$joinTable = $this->tableName($join->getTable());
$columnA = $this->columnName($join->getColumnA());
$columnB = $this->columnName($join->getColumnB());
$joinStr .= " $type JOIN $joinTable ON $columnA=$columnB";
}
}
$groupBy = "";
$groupColumns = $select->getGroupBy();
if (!empty($groupColumns)) {
$groupBy = " GROUP BY " . $this->columnName($groupColumns);
}
$orderBy = "";
$orderColumns = $select->getOrderBy();
if (!empty($orderColumns)) {
$orderBy = " ORDER BY " . $this->columnName($orderColumns);
$orderBy .= ($select->isOrderedAscending() ? " ASC" : " DESC");
}
$limit = ($select->getLimit() > 0 ? (" LIMIT " . $select->getLimit()) : "");
$offset = ($select->getOffset() > 0 ? (" OFFSET " . $select->getOffset()) : "");
$query = "SELECT $columns FROM $tables$joinStr$where$groupBy$orderBy$limit$offset";
$query = $this->buildQuery($select, $params);
if($select->dump) { var_dump($query); var_dump($params); }
return $this->execute($query, $params, true);
}
@@ -342,14 +351,21 @@ abstract class SQL {
}
} else if($condition instanceof CondIn) {
$value = $condition->getValues();
$expression = $condition->getExpression();
if (is_array($expression)) {
$values = array();
foreach ($expression as $value) {
$values[] = $this->addValue($value, $params);
}
$values = array();
foreach ($condition->getValues() as $value) {
$values[] = $this->addValue($value, $params);
$values = implode(",", $values);
} else if($expression instanceof Select) {
$values = $this->buildQuery($expression, $params);
} else {
$this->lastError = "Unsupported in-expression value: " . get_class($condition);
return false;
}
$values = implode(",", $values);
return $this->columnName($condition->getColumn()) . " IN ($values)";
} else if($condition instanceof CondKeyword) {
$left = $condition->getLeftExp();