Google reCaptcha

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

@ -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;
}
}
}

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

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

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

@ -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;
}
}

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

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

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

@ -10,7 +10,6 @@ use Driver\SQL\Condition\Condition;
use Driver\SQL\Condition\CondKeyword; use Driver\SQL\Condition\CondKeyword;
use Driver\SQL\Condition\CondNot; use Driver\SQL\Condition\CondNot;
use Driver\SQL\Condition\CondOr; use Driver\SQL\Condition\CondOr;
use Driver\SQL\Condition\CondRegex;
use Driver\SQL\Constraint\Constraint; use Driver\SQL\Constraint\Constraint;
use \Driver\SQL\Constraint\Unique; use \Driver\SQL\Constraint\Unique;
use \Driver\SQL\Constraint\PrimaryKey; use \Driver\SQL\Constraint\PrimaryKey;
@ -18,6 +17,7 @@ use \Driver\SQL\Constraint\ForeignKey;
use Driver\SQL\Query\CreateTable; use Driver\SQL\Query\CreateTable;
use Driver\SQL\Query\Delete; use Driver\SQL\Query\Delete;
use Driver\SQL\Query\Insert; use Driver\SQL\Query\Insert;
use Driver\SQL\Query\Query;
use Driver\SQL\Query\Select; use Driver\SQL\Query\Select;
use Driver\SQL\Query\Truncate; use Driver\SQL\Query\Truncate;
use Driver\SQL\Query\Update; use Driver\SQL\Query\Update;
@ -50,27 +50,27 @@ abstract class SQL {
} }
public function createTable($tableName) { public function createTable($tableName) {
return new Query\CreateTable($this, $tableName); return new CreateTable($this, $tableName);
} }
public function insert($tableName, $columns=array()) { public function insert($tableName, $columns=array()) {
return new Query\Insert($this, $tableName, $columns); return new Insert($this, $tableName, $columns);
} }
public function select(...$columNames) { public function select(...$columNames) {
return new Query\Select($this, $columNames); return new Select($this, $columNames);
} }
public function truncate($table) { public function truncate($table) {
return new Query\Truncate($this, $table); return new Truncate($this, $table);
} }
public function delete($table) { public function delete($table) {
return new Query\Delete($this, $table); return new Delete($this, $table);
} }
public function update($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(); public abstract function disconnect();
// Querybuilder // 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) { public function executeCreateTable(CreateTable $createTable) {
$tableName = $this->tableName($createTable->getTableName()); $tableName = $this->tableName($createTable->getTableName());
$ifNotExists = $createTable->ifNotExists() ? " IF NOT EXISTS": ""; $ifNotExists = $createTable->ifNotExists() ? " IF NOT EXISTS": "";
@ -161,46 +208,8 @@ abstract class SQL {
} }
public function executeSelect(Select $select) { public function executeSelect(Select $select) {
$columns = $this->columnName($select->getColumns());
$tables = $select->getTables();
$params = array(); $params = array();
$query = $this->buildQuery($select, $params);
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";
if($select->dump) { var_dump($query); var_dump($params); } if($select->dump) { var_dump($query); var_dump($params); }
return $this->execute($query, $params, true); return $this->execute($query, $params, true);
} }
@ -342,14 +351,21 @@ abstract class SQL {
} }
} else if($condition instanceof CondIn) { } 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(); $values = implode(",", $values);
foreach ($condition->getValues() as $value) { } else if($expression instanceof Select) {
$values[] = $this->addValue($value, $params); $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)"; return $this->columnName($condition->getColumn()) . " IN ($values)";
} else if($condition instanceof CondKeyword) { } else if($condition instanceof CondKeyword) {
$left = $condition->getLeftExp(); $left = $condition->getLeftExp();

@ -54,7 +54,7 @@ if(isset($_GET["api"]) && is_string($_GET["api"])) {
try { try {
$file = getClassPath($parentClass); $file = getClassPath($parentClass);
if(!file_exists($file)) { if(!file_exists($file) || !class_exists($parentClass) || !class_exists($apiClass)) {
header("404 Not Found"); header("404 Not Found");
$response = createError("Not found"); $response = createError("Not found");
} else { } else {

10
js/admin.min.js vendored

File diff suppressed because one or more lines are too long

@ -43,8 +43,12 @@ export default class API {
return this.apiCall("user/logout"); return this.apiCall("user/logout");
} }
async getNotifications() { async getNotifications(onlyNew = true) {
return this.apiCall("notifications/fetch"); return this.apiCall("notifications/fetch", { new: onlyNew });
}
async markNotificationsSeen() {
return this.apiCall("notifications/seen");
} }
async getUser(id) { async getUser(id) {
@ -101,6 +105,5 @@ export default class API {
async sendTestMail(receiver) { async sendTestMail(receiver) {
return this.apiCall("sendTestMail", { receiver: receiver }); return this.apiCall("sendTestMail", { receiver: receiver });
} }
}; };

@ -28,7 +28,7 @@ class AdminDashboard extends React.Component {
this.state = { this.state = {
loaded: false, loaded: false,
dialog: { onClose: () => this.hideDialog() }, dialog: { onClose: () => this.hideDialog() },
notifications: { } notifications: [ ]
}; };
} }
@ -75,6 +75,7 @@ class AdminDashboard extends React.Component {
this.controlObj = { this.controlObj = {
showDialog: this.showDialog.bind(this), showDialog: this.showDialog.bind(this),
fetchNotifications: this.fetchNotifications.bind(this),
api: this.api api: this.api
}; };
@ -92,7 +93,7 @@ class AdminDashboard extends React.Component {
return <EditUser {...newProps} /> return <EditUser {...newProps} />
}}/> }}/>
<Route path={"/admin/group/add"}><CreateGroup {...this.controlObj} /></Route> <Route path={"/admin/group/add"}><CreateGroup {...this.controlObj} /></Route>
<Route path={"/admin/logs"}><Logs {...this.controlObj} /></Route> <Route path={"/admin/logs"}><Logs {...this.controlObj} notifications={this.state.notifications} /></Route>
<Route path={"/admin/settings"}><Settings {...this.controlObj} /></Route> <Route path={"/admin/settings"}><Settings {...this.controlObj} /></Route>
<Route path={"/admin/pages"}><PageOverview {...this.controlObj} /></Route> <Route path={"/admin/pages"}><PageOverview {...this.controlObj} /></Route>
<Route path={"/admin/help"}><HelpPage {...this.controlObj} /></Route> <Route path={"/admin/help"}><HelpPage {...this.controlObj} /></Route>

@ -32,8 +32,8 @@ export default function HelpPage() {
<div className="col-12 col-md-8 col-lg-4"> <div className="col-12 col-md-8 col-lg-4">
<p> <p>
WebBase is a php framework to simplify user management, pages and routing. WebBase is a php framework to simplify user management, pages and routing.
It can easily be modified and extended by writing document classes or It can easily be modified and extended by writing document classes and the database
access the database with the available abstracted scheme. It also includes can be accessed through the available abstracted scheme. It also includes
a REST API with access control, parameter type checking and more. a REST API with access control, parameter type checking and more.
</p> </p>
</div> </div>

@ -1,13 +1,100 @@
import * as React from "react"; import * as React from "react";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import Icon from "../elements/icon";
import moment from 'moment';
export default class Logs extends React.Component { export default class Logs extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = {
alerts: [],
notifications: []
};
this.parent = {
api : props.api,
fetchNotifications: props.fetchNotifications,
}
}
removeError(i) {
if (i >= 0 && i < this.state.alerts.length) {
let alerts = this.state.alerts.slice();
alerts.splice(i, 1);
this.setState({...this.state, alerts: alerts});
}
}
componentDidMount() {
this.parent.api.getNotifications(false).then((res) => {
if (!res.success) {
let alerts = this.state.alerts.slice();
alerts.push({ message: res.msg, title: "Error fetching Notifications" });
this.setState({ ...this.state, alerts: alerts });
} else {
this.setState({ ...this.state, notifications: res.notifications });
}
this.parent.api.markNotificationsSeen().then((res) => {
if (!res.success) {
let alerts = this.state.alerts.slice();
alerts.push({ message: res.msg, title: "Error fetching Notifications" });
this.setState({ ...this.state, alerts: alerts });
}
this.parent.fetchNotifications();
});
});
} }
render() { render() {
const colors = ["red", "green", "blue", "purple", "maroon"];
let dates = { };
for (let notification of this.state.notifications) {
let day = moment(notification["created_at"]).format('ll');
if (!dates.hasOwnProperty(day)) {
dates[day] = [];
}
let icon = "bell";
if (notification.type === "message") {
icon = "envelope";
} else if(notification.type === "warning") {
icon = "exclamation-triangle";
}
dates[day].push({ ...notification, icon: icon, timestamp: notification["created_at"] });
}
let elements = [];
for (let date in dates) {
let color = colors[Math.floor(Math.random() * colors.length)];
elements.push(
<div className={"time-label"} key={"time-label-" + date}>
<span className={"bg-" + color}>{date}</span>
</div>
);
for (let event of dates[date]) {
let timeString = moment(event.timestamp).fromNow();
elements.push(
<div>
<Icon icon={event.icon} className={"bg-" + color}/>
<div className="timeline-item">
<span className="time"><Icon icon={"clock"}/> {timeString}</span>
<h3 className="timeline-header">{event.title}</h3>
<div className="timeline-body">{event.message}</div>
</div>
</div>
);
}
}
return <> return <>
<div className="content-header"> <div className="content-header">
<div className="container-fluid"> <div className="container-fluid">
@ -25,13 +112,18 @@ export default class Logs extends React.Component {
</div> </div>
</div> </div>
<div className={"content"}> <div className={"content"}>
<div className={"content-fluid"}> <div className={"container-fluid"}>
<div className={"row"}> <div className={"row"}>
<div className={"col-lg-6"}> <div className={"col-lg-8 col-12"}>
<div className="timeline">
</div> <div className={"time-label"}>
<div className={"col-lg-6"}> <span className={"bg-blue"}>Today</span>
</div>
{elements}
<div>
<Icon icon={"clock"} className={"bg-gray"}/>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

@ -45,6 +45,13 @@ export default class Settings extends React.Component {
isEditing: null, isEditing: null,
keys: ["message_confirm_email", "message_accept_invite", "message_reset_password"] keys: ["message_confirm_email", "message_accept_invite", "message_reset_password"]
}, },
recaptcha: {
alerts: [],
isOpen: true,
isSaving: false,
isResetting: false,
keys: ["recaptcha_enabled", "recaptcha_public_key", "recaptcha_private_key"]
},
uncategorised: { uncategorised: {
alerts: [], alerts: [],
isOpen: true, isOpen: true,
@ -60,8 +67,9 @@ export default class Settings extends React.Component {
}; };
this.hiddenKeys = [ this.hiddenKeys = [
"mail_password", "recaptcha_private_key",
"jwt_secret" "mail_password",
"jwt_secret"
]; ];
} }
@ -164,7 +172,7 @@ export default class Settings extends React.Component {
<div className={"card-header"} style={{cursor: "pointer"}} <div className={"card-header"} style={{cursor: "pointer"}}
onClick={() => this.toggleCollapse(category)}> onClick={() => this.toggleCollapse(category)}>
<h4 className={"card-title"}> <h4 className={"card-title"}>
<Icon className={"mr-2"} icon={icon}/> <Icon className={"mr-2"} icon={icon} type={icon==="google"?"fab":"fas"} />
{title} {title}
</h4> </h4>
<div className={"card-tools"}> <div className={"card-tools"}>
@ -402,6 +410,49 @@ export default class Settings extends React.Component {
return formGroups; return formGroups;
} }
getRecaptchaForm() {
return <>
<div className={"form-group mt-2"}>
<div className={"form-check"}>
<input type={"checkbox"} className={"form-check-input"}
name={"recaptcha_enabled"} id={"recaptcha_enabled"}
checked={(this.state.settings["recaptcha_enabled"] ?? "0") === "1"}
onChange={this.onChangeValue.bind(this)}/>
<label className={"form-check-label"} htmlFor={"recaptcha_enabled"}>
Enable Google's reCaptcha
</label>
</div>
</div>
<hr className={"m-2"}/>
<label htmlFor={"recaptcha_public_key"} className={"mt-2"}>reCaptcha Site Key</label>
<div className={"input-group"}>
<div className={"input-group-prepend"}>
<span className={"input-group-text"}>
<Icon icon={"unlock"}/>
</span>
</div>
<input type={"text"} className={"form-control"}
value={this.state.settings["recaptcha_public_key"] ?? ""}
placeholder={"Enter site key"} name={"recaptcha_public_key"}
id={"recaptcha_public_key"} onChange={this.onChangeValue.bind(this)}
disabled={(this.state.settings["recaptcha_enabled"] ?? "0") !== "1"}/>
</div>
<label htmlFor={"recaptcha_private_key"} className={"mt-2"}>reCaptcha Secret Key</label>
<div className={"input-group mb-3"}>
<div className={"input-group-prepend"}>
<span className={"input-group-text"}>
<Icon icon={"lock"}/>
</span>
</div>
<input type={"password"} className={"form-control"}
value={this.state.settings["recaptcha_private_key"] ?? ""}
placeholder={"(unchanged)"} name={"recaptcha_private_key"}
id={"mail_password"} onChange={this.onChangeValue.bind(this)}
disabled={(this.state.settings["recaptcha_enabled"] ?? "0") !== "1"}/>
</div>
</>
}
getUncategorizedForm() { getUncategorizedForm() {
let tr = []; let tr = [];
@ -463,6 +514,7 @@ export default class Settings extends React.Component {
"general": {color: "primary", icon: "cogs", title: "General Settings", content: this.createGeneralForm()}, "general": {color: "primary", icon: "cogs", title: "General Settings", content: this.createGeneralForm()},
"mail": {color: "warning", icon: "envelope", title: "Mail Settings", content: this.createMailForm()}, "mail": {color: "warning", icon: "envelope", title: "Mail Settings", content: this.createMailForm()},
"messages": {color: "info", icon: "copy", title: "Message Templates", content: this.getMessagesForm()}, "messages": {color: "info", icon: "copy", title: "Message Templates", content: this.getMessagesForm()},
"recaptcha": {color: "danger", icon: "google", title: "Google reCaptcha", content: this.getRecaptchaForm()},
"uncategorised": {color: "secondary", icon: "stream", title: "Uncategorised", content: this.getUncategorizedForm()}, "uncategorised": {color: "secondary", icon: "stream", title: "Uncategorised", content: this.getUncategorizedForm()},
}; };