Namespace and ClassPath rewrites
This commit is contained in:
166
Core/API/ApiKeyAPI.class.php
Normal file
166
Core/API/ApiKeyAPI.class.php
Normal file
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
namespace Core\API {
|
||||
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Objects\Context;
|
||||
|
||||
abstract class ApiKeyAPI extends Request {
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
|
||||
parent::__construct($context, $externalCall, $params);
|
||||
}
|
||||
|
||||
protected function apiKeyExists(int $id): bool {
|
||||
$sql = $this->context->getSQL();
|
||||
$res = $sql->select($sql->count())
|
||||
->from("ApiKey")
|
||||
->where(new Compare("id", $id))
|
||||
->where(new Compare("user_id", $this->context->getUser()->getId()))
|
||||
->where(new Compare("valid_until", $sql->currentTimestamp(), ">"))
|
||||
->where(new Compare("active", 1))
|
||||
->execute();
|
||||
|
||||
$this->success = ($res !== FALSE);
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
if($this->success && $res[0]["count"] === 0) {
|
||||
return $this->createError("This API-Key does not exist.");
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace Core\API\ApiKey {
|
||||
|
||||
use Core\API\ApiKeyAPI;
|
||||
use Core\API\Parameter\Parameter;
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Driver\SQL\Condition\CondAnd;
|
||||
use Core\Objects\Context;
|
||||
use Core\Objects\DatabaseEntity\ApiKey;
|
||||
|
||||
class Create extends ApiKeyAPI {
|
||||
|
||||
public function __construct(Context $context, $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array());
|
||||
$this->apiKeyAllowed = false;
|
||||
$this->loginRequired = true;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
$sql = $this->context->getSQL();
|
||||
|
||||
$apiKey = new ApiKey();
|
||||
$apiKey->apiKey = generateRandomString(64);
|
||||
$apiKey->validUntil = (new \DateTime())->modify("+30 DAY");
|
||||
$apiKey->user = $this->context->getUser();
|
||||
|
||||
$this->success = $apiKey->save($sql);
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
if ($this->success) {
|
||||
$this->result["api_key"] = $apiKey->jsonSerialize();
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
class Fetch extends ApiKeyAPI {
|
||||
|
||||
public function __construct(Context $context, $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array(
|
||||
"showActiveOnly" => new Parameter("showActiveOnly", Parameter::TYPE_BOOLEAN, true, true)
|
||||
));
|
||||
$this->loginRequired = true;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
$sql = $this->context->getSQL();
|
||||
|
||||
$condition = new Compare("user_id", $this->context->getUser()->getId());
|
||||
if ($this->getParam("showActiveOnly")) {
|
||||
$condition = new CondAnd(
|
||||
$condition,
|
||||
new Compare("valid_until", $sql->currentTimestamp(), ">"),
|
||||
new Compare("active", true)
|
||||
);
|
||||
}
|
||||
|
||||
$apiKeys = ApiKey::findAll($sql, $condition);
|
||||
$this->success = ($apiKeys !== FALSE);
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
if ($this->success) {
|
||||
$this->result["api_keys"] = array();
|
||||
foreach($apiKeys as $apiKey) {
|
||||
$this->result["api_keys"][$apiKey->getId()] = $apiKey->jsonSerialize();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
class Refresh extends ApiKeyAPI {
|
||||
|
||||
public function __construct(Context $context, $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array(
|
||||
"id" => new Parameter("id", Parameter::TYPE_INT),
|
||||
));
|
||||
$this->loginRequired = true;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
$id = $this->getParam("id");
|
||||
if (!$this->apiKeyExists($id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$validUntil = (new \DateTime)->modify("+30 DAY");
|
||||
$sql = $this->context->getSQL();
|
||||
$this->success = $sql->update("ApiKey")
|
||||
->set("valid_until", $validUntil)
|
||||
->where(new Compare("id", $id))
|
||||
->where(new Compare("user_id", $this->context->getUser()->getId()))
|
||||
->execute();
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
if ($this->success) {
|
||||
$this->result["valid_until"] = $validUntil;
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
class Revoke extends ApiKeyAPI {
|
||||
|
||||
public function __construct($user, $externalCall = false) {
|
||||
parent::__construct($user, $externalCall, array(
|
||||
"id" => new Parameter("id", Parameter::TYPE_INT),
|
||||
));
|
||||
$this->loginRequired = true;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
$id = $this->getParam("id");
|
||||
if (!$this->apiKeyExists($id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$sql = $this->context->getSQL();
|
||||
$this->success = $sql->update("ApiKey")
|
||||
->set("active", false)
|
||||
->where(new Compare("id", $id))
|
||||
->where(new Compare("user_id", $this->context->getUser()->getId()))
|
||||
->execute();
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
}
|
||||
291
Core/API/ContactAPI.class.php
Normal file
291
Core/API/ContactAPI.class.php
Normal file
@@ -0,0 +1,291 @@
|
||||
<?php
|
||||
|
||||
namespace Core\API {
|
||||
|
||||
use Core\Objects\Context;
|
||||
|
||||
abstract class ContactAPI extends Request {
|
||||
|
||||
protected ?string $messageId;
|
||||
|
||||
public function __construct(Context $context, bool $externalCall, array $params) {
|
||||
parent::__construct($context, $externalCall, $params);
|
||||
$this->messageId = null;
|
||||
$this->csrfTokenRequired = false;
|
||||
}
|
||||
|
||||
protected function sendMail(string $name, ?string $fromEmail, string $subject, string $message, ?string $to = null): bool {
|
||||
$request = new \Core\API\Mail\Send($this->context);
|
||||
$this->success = $request->execute(array(
|
||||
"subject" => $subject,
|
||||
"body" => $message,
|
||||
"replyTo" => $fromEmail,
|
||||
"replyName" => $name,
|
||||
"to" => $to
|
||||
));
|
||||
|
||||
$this->lastError = $request->getLastError();
|
||||
if ($this->success) {
|
||||
$this->messageId = $request->getResult()["messageId"];
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace Core\API\Contact {
|
||||
|
||||
use Core\API\ContactAPI;
|
||||
use Core\API\Parameter\Parameter;
|
||||
use Core\API\Parameter\StringType;
|
||||
use Core\API\VerifyCaptcha;
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Driver\SQL\Condition\CondNot;
|
||||
use Core\Driver\SQL\Expression\CaseWhen;
|
||||
use Core\Driver\SQL\Expression\Sum;
|
||||
use Core\Objects\Context;
|
||||
|
||||
class Request extends ContactAPI {
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
$parameters = array(
|
||||
'fromName' => new StringType('fromName', 32),
|
||||
'fromEmail' => new Parameter('fromEmail', Parameter::TYPE_EMAIL),
|
||||
'message' => new StringType('message', 512),
|
||||
);
|
||||
|
||||
$settings = $context->getSettings();
|
||||
if ($settings->isRecaptchaEnabled()) {
|
||||
$parameters["captcha"] = new StringType("captcha");
|
||||
}
|
||||
|
||||
parent::__construct($context, $externalCall, $parameters);
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
$settings = $this->context->getSettings();
|
||||
if ($settings->isRecaptchaEnabled()) {
|
||||
$captcha = $this->getParam("captcha");
|
||||
$req = new VerifyCaptcha($this->context);
|
||||
if (!$req->execute(array("captcha" => $captcha, "action" => "contact"))) {
|
||||
return $this->createError($req->getLastError());
|
||||
}
|
||||
}
|
||||
|
||||
// parameter
|
||||
$message = $this->getParam("message");
|
||||
$name = $this->getParam("fromName");
|
||||
$email = $this->getParam("fromEmail");
|
||||
|
||||
$sendMail = $this->sendMail($name, $email, "Contact Request", $message);
|
||||
$insertDB = $this->insertContactRequest();
|
||||
if (!$sendMail && !$insertDB) {
|
||||
return $this->createError("The contact request could not be sent. The Administrator is already informed. Please try again later.");
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
private function insertContactRequest(): bool {
|
||||
$sql = $this->context->getSQL();
|
||||
$name = $this->getParam("fromName");
|
||||
$email = $this->getParam("fromEmail");
|
||||
$message = $this->getParam("message");
|
||||
$messageId = $this->messageId ?? null;
|
||||
|
||||
$res = $sql->insert("ContactRequest", array("from_name", "from_email", "message", "messageId"))
|
||||
->addRow($name, $email, $message, $messageId)
|
||||
->returning("id")
|
||||
->execute();
|
||||
|
||||
$this->success = ($res !== FALSE);
|
||||
$this->lastError = $sql->getLastError();
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
class Respond extends ContactAPI {
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array(
|
||||
"requestId" => new Parameter("requestId", Parameter::TYPE_INT),
|
||||
'message' => new StringType('message', 512),
|
||||
));
|
||||
$this->loginRequired = true;
|
||||
}
|
||||
|
||||
private function getSenderMail(): ?string {
|
||||
$requestId = $this->getParam("requestId");
|
||||
$sql = $this->context->getSQL();
|
||||
$res = $sql->select("from_email")
|
||||
->from("ContactRequest")
|
||||
->where(new Compare("id", $requestId))
|
||||
->execute();
|
||||
|
||||
$this->success = ($res !== false);
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
if ($this->success) {
|
||||
if (empty($res)) {
|
||||
return $this->createError("Request does not exist");
|
||||
} else {
|
||||
return $res[0]["from_email"];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function insertResponseMessage(): bool {
|
||||
$sql = $this->context->getSQL();
|
||||
$message = $this->getParam("message");
|
||||
$requestId = $this->getParam("requestId");
|
||||
|
||||
$this->success = $sql->insert("ContactMessage", ["request_id", "user_id", "message", "messageId", "read"])
|
||||
->addRow($requestId, $this->context->getUser()->getId(), $message, $this->messageId, true)
|
||||
->execute();
|
||||
|
||||
$this->lastError = $sql->getLastError();
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
private function updateEntity() {
|
||||
$sql = $this->context->getSQL();
|
||||
$requestId = $this->getParam("requestId");
|
||||
|
||||
$sql->update("EntityLog")
|
||||
->set("modified", $sql->now())
|
||||
->where(new Compare("entityId", $requestId))
|
||||
->execute();
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
$message = $this->getParam("message");
|
||||
$senderMail = $this->getSenderMail();
|
||||
if (!$this->success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$user = $this->context->getUser();
|
||||
$fromName = $user->getUsername();
|
||||
$fromEmail = $user->getEmail();
|
||||
|
||||
if (!$this->sendMail($fromName, $fromEmail, "Re: Contact Request", $message, $senderMail)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->insertResponseMessage()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->updateEntity();
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
class Fetch extends ContactAPI {
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array());
|
||||
$this->loginRequired = true;
|
||||
$this->csrfTokenRequired = false;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
$sql = $this->context->getSQL();
|
||||
$res = $sql->select("ContactRequest.id", "from_name", "from_email", "from_name",
|
||||
new Sum(new CaseWhen(new CondNot("ContactMessage.read"), 1, 0), "unread"))
|
||||
->from("ContactRequest")
|
||||
->groupBy("ContactRequest.id")
|
||||
->leftJoin("ContactMessage", "ContactRequest.id", "ContactMessage.request_id")
|
||||
->execute();
|
||||
|
||||
$this->success = ($res !== false);
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
if ($this->success) {
|
||||
$this->result["contactRequests"] = [];
|
||||
foreach ($res as $row) {
|
||||
$this->result["contactRequests"][] = array(
|
||||
"id" => intval($row["id"]),
|
||||
"from_name" => $row["from_name"],
|
||||
"from_email" => $row["from_email"],
|
||||
"unread" => intval($row["unread"]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
class Get extends ContactAPI {
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array(
|
||||
"requestId" => new Parameter("requestId", Parameter::TYPE_INT),
|
||||
));
|
||||
$this->loginRequired = true;
|
||||
$this->csrfTokenRequired = false;
|
||||
}
|
||||
|
||||
private function updateRead() {
|
||||
$requestId = $this->getParam("requestId");
|
||||
$sql = $this->context->getSQL();
|
||||
$sql->update("ContactMessage")
|
||||
->set("read", 1)
|
||||
->where(new Compare("request_id", $requestId))
|
||||
->execute();
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
$requestId = $this->getParam("requestId");
|
||||
$sql = $this->context->getSQL();
|
||||
|
||||
$res = $sql->select("from_name", "from_email", "message", "created_at")
|
||||
->from("ContactRequest")
|
||||
->where(new Compare("id", $requestId))
|
||||
->execute();
|
||||
|
||||
$this->success = ($res !== false);
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
if ($this->success) {
|
||||
if (empty($res)) {
|
||||
return $this->createError("Request does not exist");
|
||||
} else {
|
||||
$row = $res[0];
|
||||
$this->result["request"] = array(
|
||||
"from_name" => $row["from_name"],
|
||||
"from_email" => $row["from_email"],
|
||||
"messages" => array(
|
||||
["sender_id" => null, "message" => $row["message"], "timestamp" => $row["created_at"]]
|
||||
)
|
||||
);
|
||||
|
||||
$res = $sql->select("user_id", "message", "created_at")
|
||||
->from("ContactMessage")
|
||||
->where(new Compare("request_id", $requestId))
|
||||
->orderBy("created_at")
|
||||
->execute();
|
||||
|
||||
$this->success = ($res !== false);
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
if ($this->success) {
|
||||
foreach ($res as $row) {
|
||||
$this->result["request"]["messages"][] = array(
|
||||
"sender_id" => $row["user_id"], "message" => $row["message"], "timestamp" => $row["created_at"]
|
||||
);
|
||||
}
|
||||
|
||||
$this->updateRead();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
}
|
||||
184
Core/API/GroupsAPI.class.php
Normal file
184
Core/API/GroupsAPI.class.php
Normal file
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
namespace Core\API {
|
||||
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Objects\Context;
|
||||
|
||||
abstract class GroupsAPI extends Request {
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
|
||||
parent::__construct($context, $externalCall, $params);
|
||||
}
|
||||
|
||||
protected function groupExists($name): bool {
|
||||
$sql = $this->context->getSQL();
|
||||
$res = $sql->select($sql->count())
|
||||
->from("Group")
|
||||
->where(new Compare("name", $name))
|
||||
->execute();
|
||||
|
||||
$this->success = ($res !== FALSE);
|
||||
$this->lastError = $sql->getLastError();
|
||||
return $this->success && $res[0]["count"] > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace Core\API\Groups {
|
||||
|
||||
use Core\API\GroupsAPI;
|
||||
use Core\API\Parameter\Parameter;
|
||||
use Core\API\Parameter\StringType;
|
||||
use Core\Objects\Context;
|
||||
use Core\Objects\DatabaseEntity\Group;
|
||||
|
||||
class Fetch extends GroupsAPI {
|
||||
|
||||
private int $groupCount;
|
||||
|
||||
public function __construct(Context $context, $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array(
|
||||
'page' => new Parameter('page', Parameter::TYPE_INT, true, 1),
|
||||
'count' => new Parameter('count', Parameter::TYPE_INT, true, 20)
|
||||
));
|
||||
|
||||
$this->groupCount = 0;
|
||||
}
|
||||
|
||||
private function fetchGroupCount(): bool {
|
||||
|
||||
$sql = $this->context->getSQL();
|
||||
$res = $sql->select($sql->count())->from("Group")->execute();
|
||||
$this->success = ($res !== FALSE);
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
if ($this->success) {
|
||||
$this->groupCount = $res[0]["count"];
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
$page = $this->getParam("page");
|
||||
if($page < 1) {
|
||||
return $this->createError("Invalid page count");
|
||||
}
|
||||
|
||||
$count = $this->getParam("count");
|
||||
if($count < 1 || $count > 50) {
|
||||
return $this->createError("Invalid fetch count");
|
||||
}
|
||||
|
||||
if (!$this->fetchGroupCount()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$sql = $this->context->getSQL();
|
||||
$res = $sql->select("Group.id as groupId", "Group.name as groupName", "Group.color as groupColor", $sql->count("UserGroup.user_id"))
|
||||
->from("Group")
|
||||
->leftJoin("UserGroup", "UserGroup.group_id", "Group.id")
|
||||
->groupBy("Group.id")
|
||||
->orderBy("Group.id")
|
||||
->ascending()
|
||||
->limit($count)
|
||||
->offset(($page - 1) * $count)
|
||||
->execute();
|
||||
|
||||
$this->success = ($res !== FALSE);
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
if($this->success) {
|
||||
$this->result["groups"] = array();
|
||||
foreach($res as $row) {
|
||||
$groupId = intval($row["groupId"]);
|
||||
$groupName = $row["groupName"];
|
||||
$groupColor = $row["groupColor"];
|
||||
$memberCount = $row["usergroup_user_id_count"];
|
||||
$this->result["groups"][$groupId] = array(
|
||||
"name" => $groupName,
|
||||
"memberCount" => $memberCount,
|
||||
"color" => $groupColor,
|
||||
);
|
||||
}
|
||||
$this->result["pageCount"] = intval(ceil($this->groupCount / $count));
|
||||
$this->result["totalCount"] = $this->groupCount;
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
class Create extends GroupsAPI {
|
||||
public function __construct(Context $context, $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array(
|
||||
'name' => new StringType('name', 32),
|
||||
'color' => new StringType('color', 10),
|
||||
));
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
$name = $this->getParam("name");
|
||||
if (preg_match("/^[a-zA-Z][a-zA-Z0-9_-]*$/", $name) !== 1) {
|
||||
return $this->createError("Invalid name");
|
||||
}
|
||||
|
||||
$color = $this->getParam("color");
|
||||
if (preg_match("/^#[a-fA-F0-9]{3,6}$/", $color) !== 1) {
|
||||
return $this->createError("Invalid color");
|
||||
}
|
||||
|
||||
$exists = $this->groupExists($name);
|
||||
if (!$this->success) {
|
||||
return false;
|
||||
} else if ($exists) {
|
||||
return $this->createError("A group with this name already exists");
|
||||
}
|
||||
|
||||
$sql = $this->context->getSQL();
|
||||
|
||||
$group = new Group();
|
||||
$group->name = $name;
|
||||
$group->color = $color;
|
||||
|
||||
$this->success = ($group->save($sql) !== FALSE);
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
if ($this->success) {
|
||||
$this->result["id"] = $group->getId();
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
class Delete extends GroupsAPI {
|
||||
public function __construct(Context $context, $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array(
|
||||
'id' => new Parameter('id', Parameter::TYPE_INT)
|
||||
));
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
$id = $this->getParam("id");
|
||||
if (in_array($id, DEFAULT_GROUPS)) {
|
||||
return $this->createError("You cannot delete a default group.");
|
||||
}
|
||||
|
||||
$sql = $this->context->getSQL();
|
||||
$group = Group::find($sql, $id);
|
||||
|
||||
$this->success = ($group !== FALSE);
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
if ($this->success && $group === null) {
|
||||
return $this->createError("This group does not exist.");
|
||||
}
|
||||
|
||||
$this->success = ($group->delete($sql) !== FALSE);
|
||||
$this->lastError = $sql->getLastError();
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
}
|
||||
116
Core/API/LanguageAPI.class.php
Normal file
116
Core/API/LanguageAPI.class.php
Normal file
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace Core\API {
|
||||
|
||||
use Core\Objects\Context;
|
||||
|
||||
abstract class LanguageAPI extends Request {
|
||||
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
|
||||
parent::__construct($context, $externalCall, $params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace Core\API\Language {
|
||||
|
||||
use Core\API\LanguageAPI;
|
||||
use Core\API\Parameter\Parameter;
|
||||
use Core\API\Parameter\StringType;
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Driver\SQL\Condition\CondOr;
|
||||
use Core\Objects\Context;
|
||||
use Core\Objects\DatabaseEntity\Language;
|
||||
|
||||
class Get extends LanguageAPI {
|
||||
|
||||
public function __construct(Context $context, $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array());
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
$sql = $this->context->getSQL();
|
||||
$languages = Language::findAll($sql);
|
||||
$this->success = ($languages !== null);
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
if ($this->success) {
|
||||
$this->result['languages'] = [];
|
||||
if (count($languages) === 0) {
|
||||
$this->lastError = L("No languages found");
|
||||
} else {
|
||||
foreach ($languages as $language) {
|
||||
$this->result['languages'][$language->getId()] = $language->jsonSerialize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
class Set extends LanguageAPI {
|
||||
|
||||
private Language $language;
|
||||
|
||||
public function __construct(Context $context, $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array(
|
||||
'langId' => new Parameter('langId', Parameter::TYPE_INT, true, NULL),
|
||||
'langCode' => new StringType('langCode', 5, true, NULL),
|
||||
));
|
||||
|
||||
}
|
||||
|
||||
private function checkLanguage(): bool {
|
||||
$langId = $this->getParam("langId");
|
||||
$langCode = $this->getParam("langCode");
|
||||
|
||||
if (is_null($langId) && is_null($langCode)) {
|
||||
return $this->createError(L("Either langId or langCode must be given"));
|
||||
}
|
||||
|
||||
$sql = $this->context->getSQL();
|
||||
$languages = Language::findAll($sql,
|
||||
new CondOr(new Compare("id", $langId), new Compare("code", $langCode))
|
||||
);
|
||||
|
||||
$this->success = ($languages !== null);
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
if ($this->success) {
|
||||
if (count($languages) === 0) {
|
||||
return $this->createError(L("This Language does not exist"));
|
||||
} else {
|
||||
$this->language = array_shift($languages);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
private function updateLanguage(): bool {
|
||||
$languageId = $this->language->getId();
|
||||
$userId = $this->context->getUser()->getId();
|
||||
$sql = $this->context->getSQL();
|
||||
|
||||
$this->success = $sql->update("User")
|
||||
->set("language_id", $languageId)
|
||||
->where(new Compare("id", $userId))
|
||||
->execute();
|
||||
|
||||
$this->lastError = $sql->getLastError();
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
if (!$this->checkLanguage())
|
||||
return false;
|
||||
|
||||
if ($this->context->getSession()) {
|
||||
$this->updateLanguage();
|
||||
}
|
||||
|
||||
$this->context->setLanguage($this->language);
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
}
|
||||
122
Core/API/LogsAPI.class.php
Normal file
122
Core/API/LogsAPI.class.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace Core\API {
|
||||
|
||||
use Core\Objects\Context;
|
||||
|
||||
abstract class LogsAPI extends Request {
|
||||
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
|
||||
parent::__construct($context, $externalCall, $params);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace Core\API\Logs {
|
||||
|
||||
use Core\API\LogsAPI;
|
||||
use Core\API\Parameter\Parameter;
|
||||
use Core\API\Parameter\StringType;
|
||||
use Core\Driver\Logger\Logger;
|
||||
use Core\Driver\SQL\Column\Column;
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Driver\SQL\Condition\CondIn;
|
||||
use Core\Objects\Context;
|
||||
use Core\Objects\DatabaseEntity\SystemLog;
|
||||
|
||||
class Get extends LogsAPI {
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, [
|
||||
"since" => new Parameter("since", Parameter::TYPE_DATE_TIME, true),
|
||||
"severity" => new StringType("severity", 32, true, "debug")
|
||||
]);
|
||||
$this->csrfTokenRequired = false;
|
||||
}
|
||||
|
||||
protected function _execute(): bool {
|
||||
$since = $this->getParam("since");
|
||||
$sql = $this->context->getSQL();
|
||||
$severity = strtolower(trim($this->getParam("severity")));
|
||||
$shownLogLevels = Logger::LOG_LEVELS;
|
||||
|
||||
$logLevel = array_search($severity, Logger::LOG_LEVELS, true);
|
||||
if ($logLevel === false) {
|
||||
return $this->createError("Invalid severity. Allowed values: " . implode(",", Logger::LOG_LEVELS));
|
||||
} else if ($logLevel > 0) {
|
||||
$shownLogLevels = array_slice(Logger::LOG_LEVELS, $logLevel);
|
||||
}
|
||||
|
||||
$query = SystemLog::findAllBuilder($sql)
|
||||
->orderBy("timestamp")
|
||||
->descending();
|
||||
|
||||
if ($since !== null) {
|
||||
$query->where(new Compare("timestamp", $since, ">="));
|
||||
}
|
||||
|
||||
if ($logLevel > 0) {
|
||||
$query->where(new CondIn(new Column("severity"), $shownLogLevels));
|
||||
}
|
||||
|
||||
$logEntries = $query->execute();
|
||||
$this->success = $logEntries !== false;
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
if ($this->success) {
|
||||
$this->result["logs"] = [];
|
||||
foreach ($logEntries as $logEntry) {
|
||||
$this->result["logs"][] = $logEntry->jsonSerialize();
|
||||
}
|
||||
} else {
|
||||
// we couldn't fetch logs from database, return a message and proceed to log files
|
||||
$this->result["logs"] = [
|
||||
[
|
||||
"id" => "fetch-fail",
|
||||
"module" => "LogsAPI",
|
||||
"message" => "Failed retrieving logs from database: " . $this->lastError,
|
||||
"severity" => "error",
|
||||
"timestamp" => (new \DateTime())->format(Parameter::DATE_TIME_FORMAT)
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// get all log entries from filesystem (if database failed)
|
||||
$logPath = realpath(implode(DIRECTORY_SEPARATOR, [WEBROOT, "Core", "Logs"]));
|
||||
if ($logPath) {
|
||||
$index = 1;
|
||||
foreach (scandir($logPath) as $fileName) {
|
||||
$logFile = $logPath . DIRECTORY_SEPARATOR . $fileName;
|
||||
// {module}_{severity}_{date}_{time}_{ms}.log
|
||||
if (preg_match("/^(\w+)_(\w+)_((\d+-\d+-\d+_){2}\d+)\.log$/", $fileName, $matches) && is_file($logFile)) {
|
||||
$content = @file_get_contents($logFile);
|
||||
$date = \DateTime::createFromFormat(Logger::LOG_FILE_DATE_FORMAT, $matches[3]);
|
||||
if ($content && $date) {
|
||||
|
||||
// filter log date
|
||||
if ($since !== null && datetimeDiff($date, $since) < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// filter log level
|
||||
if (!in_array(trim(strtolower($matches[2])), $shownLogLevels)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->result["logs"][] = [
|
||||
"id" => "file-" . ($index++),
|
||||
"module" => $matches[1],
|
||||
"severity" => $matches[2],
|
||||
"message" => $content,
|
||||
"timestamp" => $date->format(Parameter::DATE_TIME_FORMAT)
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
295
Core/API/MailAPI.class.php
Normal file
295
Core/API/MailAPI.class.php
Normal file
@@ -0,0 +1,295 @@
|
||||
<?php
|
||||
|
||||
namespace Core\API {
|
||||
|
||||
use Core\Objects\ConnectionData;
|
||||
use Core\Objects\Context;
|
||||
|
||||
abstract class MailAPI extends Request {
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
|
||||
parent::__construct($context, $externalCall, $params);
|
||||
}
|
||||
|
||||
protected function getMailConfig(): ?ConnectionData {
|
||||
$req = new \Core\API\Settings\Get($this->context);
|
||||
$this->success = $req->execute(array("key" => "^mail_"));
|
||||
$this->lastError = $req->getLastError();
|
||||
|
||||
if ($this->success) {
|
||||
$settings = $req->getResult()["settings"];
|
||||
|
||||
if (!isset($settings["mail_enabled"]) || $settings["mail_enabled"] !== "1") {
|
||||
$this->createError("Mail is not configured yet.");
|
||||
return null;
|
||||
}
|
||||
|
||||
$host = $settings["mail_host"] ?? "localhost";
|
||||
$port = intval($settings["mail_port"] ?? "25");
|
||||
$login = $settings["mail_username"] ?? "";
|
||||
$password = $settings["mail_password"] ?? "";
|
||||
$connectionData = new ConnectionData($host, $port, $login, $password);
|
||||
$connectionData->setProperty("from", $settings["mail_from"] ?? "");
|
||||
$connectionData->setProperty("last_sync", $settings["mail_last_sync"] ?? "");
|
||||
$connectionData->setProperty("mail_footer", $settings["mail_footer"] ?? "");
|
||||
return $connectionData;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace Core\API\Mail {
|
||||
|
||||
use Core\API\MailAPI;
|
||||
use Core\API\Parameter\Parameter;
|
||||
use Core\API\Parameter\StringType;
|
||||
use DateTimeInterface;
|
||||
use Core\Driver\SQL\Column\Column;
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Driver\SQL\Condition\CondIn;
|
||||
use Core\External\PHPMailer\Exception;
|
||||
use Core\External\PHPMailer\PHPMailer;
|
||||
use Core\Objects\Context;
|
||||
use Core\Objects\DatabaseEntity\GpgKey;
|
||||
|
||||
class Test extends MailAPI {
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array(
|
||||
"receiver" => new Parameter("receiver", Parameter::TYPE_EMAIL),
|
||||
"gpgFingerprint" => new StringType("gpgFingerprint", 64, true, null)
|
||||
));
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
|
||||
$receiver = $this->getParam("receiver");
|
||||
$req = new \Core\API\Mail\Send($this->context);
|
||||
$this->success = $req->execute(array(
|
||||
"to" => $receiver,
|
||||
"subject" => "Test E-Mail",
|
||||
"body" => "Hey! If you receive this e-mail, your mail configuration seems to be working.",
|
||||
"gpgFingerprint" => $this->getParam("gpgFingerprint"),
|
||||
"async" => false
|
||||
));
|
||||
|
||||
$this->lastError = $req->getLastError();
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
class Send extends MailAPI {
|
||||
public function __construct(Context $context, $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array(
|
||||
'to' => new Parameter('to', Parameter::TYPE_EMAIL, true, null),
|
||||
'subject' => new StringType('subject', -1),
|
||||
'body' => new StringType('body', -1),
|
||||
'replyTo' => new Parameter('replyTo', Parameter::TYPE_EMAIL, true, null),
|
||||
'replyName' => new StringType('replyName', 32, true, ""),
|
||||
'gpgFingerprint' => new StringType("gpgFingerprint", 64, true, null),
|
||||
'async' => new Parameter("async", Parameter::TYPE_BOOLEAN, true, true)
|
||||
));
|
||||
$this->isPublic = false;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
|
||||
$mailConfig = $this->getMailConfig();
|
||||
if (!$this->success || $mailConfig === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$fromMail = $mailConfig->getProperty('from');
|
||||
$mailFooter = $mailConfig->getProperty('mail_footer');
|
||||
$toMail = $this->getParam('to') ?? $fromMail;
|
||||
$subject = $this->getParam('subject');
|
||||
$replyTo = $this->getParam('replyTo');
|
||||
$replyName = $this->getParam('replyName');
|
||||
$body = $this->getParam('body');
|
||||
$gpgFingerprint = $this->getParam("gpgFingerprint");
|
||||
|
||||
if ($this->getParam("async")) {
|
||||
$sql = $this->context->getSQL();
|
||||
$this->success = $sql->insert("MailQueue", ["from", "to", "subject", "body",
|
||||
"replyTo", "replyName", "gpgFingerprint"])
|
||||
->addRow($fromMail, $toMail, $subject, $body, $replyTo, $replyName, $gpgFingerprint)
|
||||
->execute() !== false;
|
||||
$this->lastError = $sql->getLastError();
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
|
||||
if (stripos($body, "<body") === false) {
|
||||
$body = "<body>$body</body>";
|
||||
}
|
||||
if (stripos($body, "<html") === false) {
|
||||
$body = "<html>$body</html>";
|
||||
}
|
||||
|
||||
if (!empty($mailFooter)) {
|
||||
$email_signature = realpath(WEBROOT . DIRECTORY_SEPARATOR . $mailFooter);
|
||||
if (is_file($email_signature)) {
|
||||
$email_signature = file_get_contents($email_signature);
|
||||
$body .= $email_signature;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$mail = new PHPMailer;
|
||||
$mail->IsSMTP();
|
||||
$mail->setFrom($fromMail);
|
||||
$mail->addAddress($toMail);
|
||||
|
||||
if ($replyTo) {
|
||||
$mail->addReplyTo($replyTo, $replyName);
|
||||
}
|
||||
|
||||
$mail->Subject = $subject;
|
||||
$mail->SMTPDebug = 0;
|
||||
$mail->Host = $mailConfig->getHost();
|
||||
$mail->Port = $mailConfig->getPort();
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Timeout = 15;
|
||||
$mail->Username = $mailConfig->getLogin();
|
||||
$mail->Password = $mailConfig->getPassword();
|
||||
$mail->SMTPSecure = 'tls';
|
||||
$mail->CharSet = 'UTF-8';
|
||||
|
||||
if ($gpgFingerprint) {
|
||||
$encryptedHeaders = implode("\r\n", [
|
||||
"Date: " . (new \DateTime())->format(DateTimeInterface::RFC2822),
|
||||
"Content-Type: text/html",
|
||||
"Content-Transfer-Encoding: quoted-printable"
|
||||
]);
|
||||
|
||||
$mimeBody = $encryptedHeaders . "\r\n\r\n" . quoted_printable_encode($body);
|
||||
$res = GpgKey::encrypt($mimeBody, $gpgFingerprint);
|
||||
if ($res["success"]) {
|
||||
$encryptedBody = $res["data"];
|
||||
$mail->AltBody = '';
|
||||
$mail->Body = '';
|
||||
$mail->AllowEmpty = true;
|
||||
$mail->ContentType = PHPMailer::CONTENT_TYPE_MULTIPART_ENCRYPTED;
|
||||
$mail->addStringAttachment("Version: 1", null, PHPMailer::ENCODING_BASE64, "application/pgp-encrypted", "");
|
||||
$mail->addStringAttachment($encryptedBody, "encrypted.asc", PHPMailer::ENCODING_7BIT, "application/octet-stream", "");
|
||||
} else {
|
||||
return $this->createError($res["error"]);
|
||||
}
|
||||
} else {
|
||||
$mail->msgHTML($body);
|
||||
$mail->AltBody = strip_tags($body);
|
||||
}
|
||||
|
||||
$this->success = @$mail->Send();
|
||||
if (!$this->success) {
|
||||
$this->lastError = "Error sending Mail: $mail->ErrorInfo";
|
||||
$this->logger->error("sendMail() failed: $mail->ErrorInfo");
|
||||
} else {
|
||||
$this->result["messageId"] = $mail->getLastMessageID();
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->success = false;
|
||||
$this->lastError = "Error sending Mail: $e";
|
||||
$this->logger->error($this->lastError);
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
class SendQueue extends MailAPI {
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, [
|
||||
"debug" => new Parameter("debug", Parameter::TYPE_BOOLEAN, true, false)
|
||||
]);
|
||||
$this->isPublic = false;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
|
||||
$debug = $this->getParam("debug");
|
||||
$startTime = time();
|
||||
if ($debug) {
|
||||
echo "Start of processing mail queue at $startTime" . PHP_EOL;
|
||||
}
|
||||
|
||||
$sql = $this->context->getSQL();
|
||||
$res = $sql->select("id", "from", "to", "subject", "body",
|
||||
"replyTo", "replyName", "gpgFingerprint", "retryCount")
|
||||
->from("MailQueue")
|
||||
->where(new Compare("retryCount", 0, ">"))
|
||||
->where(new Compare("status", "waiting"))
|
||||
->where(new Compare("nextTry", $sql->now(), "<="))
|
||||
->execute();
|
||||
|
||||
$this->success = ($res !== false);
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
if ($this->success && is_array($res)) {
|
||||
if ($debug) {
|
||||
echo "Found " . count($res) . " mails to send" . PHP_EOL;
|
||||
}
|
||||
|
||||
$successfulMails = [];
|
||||
foreach ($res as $row) {
|
||||
|
||||
if (time() - $startTime >= 45) {
|
||||
$this->lastError = "Not able to process whole mail queue within 45 seconds, will continue on next time";
|
||||
break;
|
||||
}
|
||||
|
||||
$to = $row["to"];
|
||||
$subject = $row["subject"];
|
||||
|
||||
if ($debug) {
|
||||
echo "Sending subject=$subject to=$to" . PHP_EOL;
|
||||
}
|
||||
|
||||
$mailId = intval($row["id"]);
|
||||
$retryCount = intval($row["retryCount"]);
|
||||
$req = new Send($this->context);
|
||||
$args = [
|
||||
"to" => $to,
|
||||
"subject" => $subject,
|
||||
"body" => $row["body"],
|
||||
"replyTo" => $row["replyTo"],
|
||||
"replyName" => $row["replyName"],
|
||||
"gpgFingerprint" => $row["gpgFingerprint"],
|
||||
"async" => false
|
||||
];
|
||||
$success = $req->execute($args);
|
||||
$error = $req->getLastError();
|
||||
|
||||
if (!$success) {
|
||||
$delay = [0, 720, 360, 60, 30, 1];
|
||||
$minutes = $delay[max(0, min(count($delay) - 1, $retryCount))];
|
||||
$nextTry = (new \DateTime())->modify("+$minutes minute");
|
||||
$sql->update("MailQueue")
|
||||
->set("retryCount", $retryCount - 1)
|
||||
->set("status", "error")
|
||||
->set("errorMessage", $error)
|
||||
->set("nextTry", $nextTry)
|
||||
->where(new Compare("id", $mailId))
|
||||
->execute();
|
||||
} else {
|
||||
$successfulMails[] = $mailId;
|
||||
}
|
||||
}
|
||||
|
||||
$this->success = count($successfulMails) === count($res);
|
||||
if (!empty($successfulMails)) {
|
||||
$res = $sql->update("MailQueue")
|
||||
->set("status", "success")
|
||||
->where(new CondIn(new Column("id"), $successfulMails))
|
||||
->execute();
|
||||
$this->success = $res !== false;
|
||||
$this->lastError = $sql->getLastError();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
}
|
||||
158
Core/API/NewsAPI.class.php
Normal file
158
Core/API/NewsAPI.class.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace Core\API {
|
||||
|
||||
use Core\Objects\Context;
|
||||
|
||||
abstract class NewsAPI extends Request {
|
||||
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
|
||||
parent::__construct($context, $externalCall, $params);
|
||||
$this->loginRequired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace Core\API\News {
|
||||
|
||||
use Core\API\NewsAPI;
|
||||
use Core\API\Parameter\Parameter;
|
||||
use Core\API\Parameter\StringType;
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Objects\Context;
|
||||
use Core\Objects\DatabaseEntity\News;
|
||||
|
||||
class Get extends NewsAPI {
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, [
|
||||
"since" => new Parameter("since", Parameter::TYPE_DATE_TIME, true, null),
|
||||
"limit" => new Parameter("limit", Parameter::TYPE_INT, true, 10)
|
||||
]);
|
||||
|
||||
$this->loginRequired = false;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
$since = $this->getParam("since");
|
||||
$limit = $this->getParam("limit");
|
||||
if ($limit < 1 || $limit > 30) {
|
||||
return $this->createError("Limit must be in range 1-30");
|
||||
}
|
||||
|
||||
$sql = $this->context->getSQL();
|
||||
$newsQuery = News::findAllBuilder($sql)
|
||||
->limit($limit)
|
||||
->orderBy("published_at")
|
||||
->descending()
|
||||
->fetchEntities();
|
||||
|
||||
if ($since) {
|
||||
$newsQuery->where(new Compare("published_at", $since, ">="));
|
||||
}
|
||||
|
||||
$newsArray = $newsQuery->execute();
|
||||
$this->success = $newsArray !== null;
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
if ($this->success) {
|
||||
$this->result["news"] = [];
|
||||
foreach ($newsArray as $news) {
|
||||
$newsId = $news->getId();
|
||||
$this->result["news"][$newsId] = $news->jsonSerialize();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
class Publish extends NewsAPI {
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, [
|
||||
"title" => new StringType("title", 128),
|
||||
"text" => new StringType("text", 1024)
|
||||
]);
|
||||
$this->loginRequired = true;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
|
||||
$news = new News();
|
||||
$news->text = $this->getParam("text");
|
||||
$news->title = $this->getParam("title");
|
||||
$news->publishedBy = $this->context->getUser();
|
||||
|
||||
$sql = $this->context->getSQL();
|
||||
$this->success = $news->save($sql);
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
if ($this->success) {
|
||||
$this->result["newsId"] = $news->getId();
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
class Delete extends NewsAPI {
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, [
|
||||
"id" => new Parameter("id", Parameter::TYPE_INT)
|
||||
]);
|
||||
$this->loginRequired = true;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
$sql = $this->context->getSQL();
|
||||
$currentUser = $this->context->getUser();
|
||||
|
||||
$news = News::find($sql, $this->getParam("id"));
|
||||
$this->success = ($news !== false);
|
||||
$this->lastError = $sql->getLastError();
|
||||
if (!$this->success) {
|
||||
return false;
|
||||
} else if ($news === null) {
|
||||
return $this->createError("News Post not found");
|
||||
} else if ($news->publishedBy->getId() !== $currentUser->getId() && !$currentUser->hasGroup(USER_GROUP_ADMIN)) {
|
||||
return $this->createError("You do not have permissions to delete news post of other users.");
|
||||
}
|
||||
|
||||
$this->success = $news->delete($sql);
|
||||
$this->lastError = $sql->getLastError();
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
class Edit extends NewsAPI {
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, [
|
||||
"id" => new Parameter("id", Parameter::TYPE_INT),
|
||||
"title" => new StringType("title", 128),
|
||||
"text" => new StringType("text", 1024)
|
||||
]);
|
||||
$this->loginRequired = true;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
$sql = $this->context->getSQL();
|
||||
$currentUser = $this->context->getUser();
|
||||
|
||||
$news = News::find($sql, $this->getParam("id"));
|
||||
$this->success = ($news !== false);
|
||||
$this->lastError = $sql->getLastError();
|
||||
if (!$this->success) {
|
||||
return false;
|
||||
} else if ($news === null) {
|
||||
return $this->createError("News Post not found");
|
||||
} else if ($news->publishedBy->getId() !== $currentUser->getId() && !$currentUser->hasGroup(USER_GROUP_ADMIN)) {
|
||||
return $this->createError("You do not have permissions to edit news post of other users.");
|
||||
}
|
||||
|
||||
$news->text = $this->getParam("text");
|
||||
$news->title = $this->getParam("title");
|
||||
$this->success = $news->save($sql);
|
||||
$this->lastError = $sql->getLastError();
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
}
|
||||
231
Core/API/NotificationsAPI.class.php
Normal file
231
Core/API/NotificationsAPI.class.php
Normal file
@@ -0,0 +1,231 @@
|
||||
<?php
|
||||
|
||||
namespace Core\API {
|
||||
|
||||
use Core\Objects\Context;
|
||||
|
||||
abstract class NotificationsAPI extends Request {
|
||||
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
|
||||
parent::__construct($context, $externalCall, $params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace Core\API\Notifications {
|
||||
|
||||
use Core\API\NotificationsAPI;
|
||||
use Core\API\Parameter\Parameter;
|
||||
use Core\API\Parameter\StringType;
|
||||
use Core\Driver\SQL\Column\Column;
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Driver\SQL\Condition\CondIn;
|
||||
use Core\Driver\SQL\Query\Select;
|
||||
use Core\Objects\Context;
|
||||
use Core\Objects\DatabaseEntity\Group;
|
||||
use Core\Objects\DatabaseEntity\Notification;
|
||||
use Core\Objects\DatabaseEntity\User;
|
||||
|
||||
class Create extends NotificationsAPI {
|
||||
|
||||
public function __construct(Context $context, $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array(
|
||||
'groupId' => new Parameter('groupId', Parameter::TYPE_INT, true),
|
||||
'userId' => new Parameter('userId', Parameter::TYPE_INT, true),
|
||||
'title' => new StringType('title', 32),
|
||||
'message' => new StringType('message', 256),
|
||||
));
|
||||
$this->isPublic = false;
|
||||
}
|
||||
|
||||
private function insertUserNotification($userId, $notificationId): bool {
|
||||
$sql = $this->context->getSQL();
|
||||
$res = $sql->insert("UserNotification", array("user_id", "notification_id"))
|
||||
->addRow($userId, $notificationId)
|
||||
->execute();
|
||||
|
||||
$this->success = ($res !== FALSE);
|
||||
$this->lastError = $sql->getLastError();
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
private function insertGroupNotification($groupId, $notificationId): bool {
|
||||
$sql = $this->context->getSQL();
|
||||
$res = $sql->insert("GroupNotification", array("group_id", "notification_id"))
|
||||
->addRow($groupId, $notificationId)
|
||||
->execute();
|
||||
|
||||
$this->success = ($res !== FALSE);
|
||||
$this->lastError = $sql->getLastError();
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
private function createNotification($title, $message): bool|int {
|
||||
$sql = $this->context->getSQL();
|
||||
$notification = new Notification();
|
||||
$notification->title = $title;
|
||||
$notification->message = $message;
|
||||
|
||||
$this->success = ($notification->save($sql) !== FALSE);
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
if ($this->success) {
|
||||
return $notification->getId();
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
$sql = $this->context->getSQL();
|
||||
$userId = $this->getParam("userId");
|
||||
$groupId = $this->getParam("groupId");
|
||||
$title = $this->getParam("title");
|
||||
$message = $this->getParam("message");
|
||||
|
||||
if (is_null($userId) && is_null($groupId)) {
|
||||
return $this->createError("Either userId or groupId must be specified.");
|
||||
} else if(!is_null($userId) && !is_null($groupId)) {
|
||||
return $this->createError("Only one of userId and groupId must be specified.");
|
||||
} else if(!is_null($userId)) {
|
||||
if (User::exists($sql, $userId)) {
|
||||
$id = $this->createNotification($title, $message);
|
||||
if ($this->success) {
|
||||
return $this->insertUserNotification($userId, $id);
|
||||
}
|
||||
} else {
|
||||
return $this->createError("User not found: $userId");
|
||||
}
|
||||
} else if(!is_null($groupId)) {
|
||||
if (Group::exists($sql, $groupId)) {
|
||||
$id = $this->createNotification($title, $message);
|
||||
if ($this->success) {
|
||||
return $this->insertGroupNotification($groupId, $id);
|
||||
}
|
||||
} else {
|
||||
return $this->createError("Group not found: $groupId");
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
class Fetch extends NotificationsAPI {
|
||||
|
||||
private array $notifications;
|
||||
private array $notificationIds;
|
||||
|
||||
public function __construct(Context $context, $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array(
|
||||
'new' => new Parameter('new', Parameter::TYPE_BOOLEAN, true, true)
|
||||
));
|
||||
$this->loginRequired = true;
|
||||
}
|
||||
|
||||
private function fetchUserNotifications(): bool {
|
||||
$onlyNew = $this->getParam('new');
|
||||
$userId = $this->context->getUser()->getId();
|
||||
$sql = $this->context->getSQL();
|
||||
$query = $sql->select($sql->distinct("Notification.id"), "created_at", "title", "message", "type")
|
||||
->from("Notification")
|
||||
->innerJoin("UserNotification", "UserNotification.notification_id", "Notification.id")
|
||||
->where(new Compare("UserNotification.user_id", $userId))
|
||||
->orderBy("created_at")->descending();
|
||||
|
||||
if ($onlyNew) {
|
||||
$query->where(new Compare("UserNotification.seen", false));
|
||||
}
|
||||
|
||||
return $this->fetchNotifications($query);
|
||||
}
|
||||
|
||||
private function fetchGroupNotifications(): bool {
|
||||
$onlyNew = $this->getParam('new');
|
||||
$userId = $this->context->getUser()->getId();
|
||||
$sql = $this->context->getSQL();
|
||||
$query = $sql->select($sql->distinct("Notification.id"), "created_at", "title", "message", "type")
|
||||
->from("Notification")
|
||||
->innerJoin("GroupNotification", "GroupNotification.notification_id", "Notification.id")
|
||||
->innerJoin("UserGroup", "GroupNotification.group_id", "UserGroup.group_id")
|
||||
->where(new Compare("UserGroup.user_id", $userId))
|
||||
->orderBy("created_at")->descending();
|
||||
|
||||
if ($onlyNew) {
|
||||
$query->where(new Compare("GroupNotification.seen", false));
|
||||
}
|
||||
|
||||
return $this->fetchNotifications($query);
|
||||
}
|
||||
|
||||
private function fetchNotifications(Select $query): bool {
|
||||
$sql = $this->context->getSQL();
|
||||
$res = $query->execute();
|
||||
$this->success = ($res !== FALSE);
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
if ($this->success) {
|
||||
foreach($res as $row) {
|
||||
$id = $row["id"];
|
||||
if (!in_array($id, $this->notificationIds)) {
|
||||
$this->notificationIds[] = $id;
|
||||
$this->notifications[] = array(
|
||||
"id" => $id,
|
||||
"title" => $row["title"],
|
||||
"message" => $row["message"],
|
||||
"created_at" => $row["created_at"],
|
||||
"type" => $row["type"]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
$this->notifications = array();
|
||||
$this->notificationIds = array();
|
||||
if ($this->fetchUserNotifications() && $this->fetchGroupNotifications()) {
|
||||
$this->result["notifications"] = $this->notifications;
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
class Seen extends NotificationsAPI {
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array());
|
||||
$this->loginRequired = true;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
|
||||
$currentUser = $this->context->getUser();
|
||||
$sql = $this->context->getSQL();
|
||||
$res = $sql->update("UserNotification")
|
||||
->set("seen", true)
|
||||
->where(new Compare("user_id", $currentUser->getId()))
|
||||
->execute();
|
||||
|
||||
$this->success = ($res !== FALSE);
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
if ($this->success) {
|
||||
$res = $sql->update("GroupNotification")
|
||||
->set("seen", true)
|
||||
->where(new CondIn(new Column("group_id"),
|
||||
$sql->select("group_id")
|
||||
->from("UserGroup")
|
||||
->where(new Compare("user_id", $currentUser->getId()))))
|
||||
->execute();
|
||||
|
||||
$this->success = ($res !== FALSE);
|
||||
$this->lastError = $sql->getLastError();
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
}
|
||||
64
Core/API/Parameter/ArrayType.class.php
Normal file
64
Core/API/Parameter/ArrayType.class.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Core\API\Parameter;
|
||||
|
||||
class ArrayType extends Parameter {
|
||||
|
||||
private Parameter $elementParameter;
|
||||
public int $elementType;
|
||||
public int $canBeOne;
|
||||
|
||||
/**
|
||||
* ArrayType constructor.
|
||||
* @param string $name the name of the parameter
|
||||
* @param int $elementType element type inside the array, for example, allow only integer values (Parameter::TYPE_INT)
|
||||
* @param bool $canBeOne true, if a single element can be passed inside the request (e.g. array=1 instead of array[]=1). Will be automatically casted to an array
|
||||
* @param bool $optional true if the parameter is optional
|
||||
* @param array|null $defaultValue the default value to use, if the parameter is not given
|
||||
*/
|
||||
public function __construct(string $name, int $elementType = Parameter::TYPE_MIXED, bool $canBeOne = false, bool $optional = FALSE, ?array $defaultValue = NULL) {
|
||||
$this->elementType = $elementType;
|
||||
$this->elementParameter = new Parameter('', $elementType);
|
||||
$this->canBeOne = $canBeOne;
|
||||
parent::__construct($name, Parameter::TYPE_ARRAY, $optional, $defaultValue);
|
||||
}
|
||||
|
||||
public function parseParam($value): bool {
|
||||
if(!is_array($value)) {
|
||||
if (!$this->canBeOne) {
|
||||
return false;
|
||||
} else {
|
||||
$value = array($value);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->elementType != Parameter::TYPE_MIXED) {
|
||||
foreach ($value as &$element) {
|
||||
if ($this->elementParameter->parseParam($element)) {
|
||||
$element = $this->elementParameter->value;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->value = $value;
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getTypeName(): string {
|
||||
$elementType = $this->elementParameter->getTypeName();
|
||||
return parent::getTypeName() . "($elementType)";
|
||||
}
|
||||
|
||||
public function toString(): string {
|
||||
$typeName = $this->getTypeName();
|
||||
$str = "$typeName $this->name";
|
||||
$defaultValue = (is_null($this->value) ? 'NULL' : (is_array($this->value) ? '[' . implode(",", $this->value) . ']' : $this->value));
|
||||
if($this->optional) {
|
||||
$str = "[$str = $defaultValue]";
|
||||
}
|
||||
|
||||
return $str;
|
||||
}
|
||||
}
|
||||
204
Core/API/Parameter/Parameter.class.php
Normal file
204
Core/API/Parameter/Parameter.class.php
Normal file
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
|
||||
namespace Core\API\Parameter;
|
||||
|
||||
use DateTime;
|
||||
|
||||
class Parameter {
|
||||
const TYPE_INT = 0;
|
||||
const TYPE_FLOAT = 1;
|
||||
const TYPE_BOOLEAN = 2;
|
||||
const TYPE_STRING = 3;
|
||||
const TYPE_DATE = 4;
|
||||
const TYPE_TIME = 5;
|
||||
const TYPE_DATE_TIME = 6;
|
||||
const TYPE_EMAIL = 7;
|
||||
|
||||
// only internal access
|
||||
const TYPE_RAW = 8;
|
||||
|
||||
// only json will work here I guess
|
||||
// nope. also name[]=value
|
||||
const TYPE_ARRAY = 9;
|
||||
const TYPE_MIXED = 10;
|
||||
|
||||
const names = array('Integer', 'Float', 'Boolean', 'String', 'Date', 'Time', 'DateTime', 'E-Mail', 'Raw', 'Array', 'Mixed');
|
||||
|
||||
const DATE_FORMAT = "Y-m-d";
|
||||
const TIME_FORMAT = "H:i:s";
|
||||
const DATE_TIME_FORMAT = self::DATE_FORMAT . " " . self::TIME_FORMAT;
|
||||
|
||||
private $defaultValue;
|
||||
|
||||
public string $name;
|
||||
public $value;
|
||||
public bool $optional;
|
||||
public int $type;
|
||||
public string $typeName;
|
||||
|
||||
public function __construct(string $name, int $type, bool $optional = FALSE, $defaultValue = NULL) {
|
||||
$this->name = $name;
|
||||
$this->optional = $optional;
|
||||
$this->defaultValue = $defaultValue;
|
||||
$this->value = $defaultValue;
|
||||
$this->type = $type;
|
||||
$this->typeName = $this->getTypeName();
|
||||
}
|
||||
|
||||
public function reset() {
|
||||
$this->value = $this->defaultValue;
|
||||
}
|
||||
|
||||
public function getSwaggerTypeName(): string {
|
||||
$typeName = strtolower(($this->type >= 0 && $this->type < count(Parameter::names)) ? Parameter::names[$this->type] : "invalid");
|
||||
if ($typeName === "mixed" || $typeName === "raw") {
|
||||
return "object";
|
||||
}
|
||||
|
||||
if (!in_array($typeName, ["array", "boolean", "integer", "number", "object", "string"])) {
|
||||
return "string";
|
||||
}
|
||||
|
||||
return $typeName;
|
||||
}
|
||||
|
||||
public function getSwaggerFormat(): ?string {
|
||||
switch ($this->type) {
|
||||
case self::TYPE_DATE:
|
||||
return self::DATE_FORMAT;
|
||||
case self::TYPE_TIME:
|
||||
return self::TIME_FORMAT;
|
||||
case self::TYPE_DATE_TIME:
|
||||
return self::DATE_TIME_FORMAT;
|
||||
case self::TYPE_EMAIL:
|
||||
return "email";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function getTypeName(): string {
|
||||
return ($this->type >= 0 && $this->type < count(Parameter::names)) ? Parameter::names[$this->type] : "INVALID";
|
||||
}
|
||||
|
||||
public function toString(): string {
|
||||
$typeName = Parameter::names[$this->type];
|
||||
|
||||
$str = "$typeName $this->name";
|
||||
$defaultValue = (is_null($this->value) ? 'NULL' : $this->value);
|
||||
if($this->optional) {
|
||||
$str = "[$str = $defaultValue]";
|
||||
}
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
public static function parseType($value): int {
|
||||
if(is_array($value))
|
||||
return Parameter::TYPE_ARRAY;
|
||||
else if(is_numeric($value) && intval($value) == $value)
|
||||
return Parameter::TYPE_INT;
|
||||
else if(is_float($value) || (is_numeric($value) && floatval($value) == $value))
|
||||
return Parameter::TYPE_FLOAT;
|
||||
else if(is_bool($value) || $value == "true" || $value == "false")
|
||||
return Parameter::TYPE_BOOLEAN;
|
||||
else if(is_a($value, 'DateTime'))
|
||||
return Parameter::TYPE_DATE_TIME;
|
||||
else if($value !== null && ($d = DateTime::createFromFormat(self::DATE_FORMAT, $value)) && $d->format(self::DATE_FORMAT) === $value)
|
||||
return Parameter::TYPE_DATE;
|
||||
else if($value !== null && ($d = DateTime::createFromFormat(self::TIME_FORMAT, $value)) && $d->format(self::TIME_FORMAT) === $value)
|
||||
return Parameter::TYPE_TIME;
|
||||
else if($value !== null && ($d = DateTime::createFromFormat(self::DATE_TIME_FORMAT, $value)) && $d->format(self::DATE_TIME_FORMAT) === $value)
|
||||
return Parameter::TYPE_DATE_TIME;
|
||||
else if (filter_var($value, FILTER_VALIDATE_EMAIL))
|
||||
return Parameter::TYPE_EMAIL;
|
||||
else
|
||||
return Parameter::TYPE_STRING;
|
||||
}
|
||||
|
||||
public function parseParam($value): bool {
|
||||
switch($this->type) {
|
||||
case Parameter::TYPE_INT:
|
||||
if(is_numeric($value) && intval($value) == $value) {
|
||||
$this->value = intval($value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
case Parameter::TYPE_FLOAT:
|
||||
if(is_numeric($value) && (floatval($value) == $value || intval($value) == $value)) {
|
||||
$this->value = floatval($value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
case Parameter::TYPE_BOOLEAN:
|
||||
if(strcasecmp($value, 'true') === 0)
|
||||
$this->value = true;
|
||||
else if(strcasecmp($value, 'false') === 0)
|
||||
$this->value = false;
|
||||
else if(is_bool($value))
|
||||
$this->value = (bool)$value;
|
||||
else
|
||||
return false;
|
||||
return true;
|
||||
|
||||
case Parameter::TYPE_DATE:
|
||||
if(is_a($value, "DateTime")) {
|
||||
$this->value = $value;
|
||||
return true;
|
||||
}
|
||||
|
||||
$d = DateTime::createFromFormat(self::DATE_FORMAT, $value);
|
||||
if($d && $d->format(self::DATE_FORMAT) === $value) {
|
||||
$this->value = $d;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
case Parameter::TYPE_TIME:
|
||||
if(is_a($value, "DateTime")) {
|
||||
$this->value = $value;
|
||||
return true;
|
||||
}
|
||||
|
||||
$d = DateTime::createFromFormat(self::TIME_FORMAT, $value);
|
||||
if($d && $d->format(self::TIME_FORMAT) === $value) {
|
||||
$this->value = $d;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
case Parameter::TYPE_DATE_TIME:
|
||||
if(is_a($value, 'DateTime')) {
|
||||
$this->value = $value;
|
||||
return true;
|
||||
} else {
|
||||
$d = DateTime::createFromFormat(self::DATE_TIME_FORMAT, $value);
|
||||
if($d && $d->format(self::DATE_TIME_FORMAT) === $value) {
|
||||
$this->value = $d;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
case Parameter::TYPE_EMAIL:
|
||||
if (filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||
$this->value = $value;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
case Parameter::TYPE_ARRAY:
|
||||
if(is_array($value)) {
|
||||
$this->value = $value;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
default:
|
||||
$this->value = $value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
Core/API/Parameter/StringType.class.php
Normal file
41
Core/API/Parameter/StringType.class.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Core\API\Parameter;
|
||||
|
||||
class StringType extends Parameter {
|
||||
|
||||
public int $maxLength;
|
||||
public function __construct(string $name, int $maxLength = -1, bool $optional = FALSE, ?string $defaultValue = NULL) {
|
||||
$this->maxLength = $maxLength;
|
||||
parent::__construct($name, Parameter::TYPE_STRING, $optional, $defaultValue);
|
||||
}
|
||||
|
||||
public function parseParam($value): bool {
|
||||
if(!is_string($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if($this->maxLength > 0 && strlen($value) > $this->maxLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->value = $value;
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getTypeName(): string {
|
||||
$maxLength = ($this->maxLength > 0 ? "($this->maxLength)" : "");
|
||||
return parent::getTypeName() . $maxLength;
|
||||
}
|
||||
|
||||
public function toString(): string {
|
||||
$typeName = $this->getTypeName();
|
||||
$str = "$typeName $this->name";
|
||||
$defaultValue = (is_null($this->value) ? 'NULL' : $this->value);
|
||||
if($this->optional) {
|
||||
$str = "[$str = $defaultValue]";
|
||||
}
|
||||
|
||||
return $str;
|
||||
}
|
||||
}
|
||||
197
Core/API/PermissionAPI.class.php
Normal file
197
Core/API/PermissionAPI.class.php
Normal file
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
|
||||
namespace Core\API {
|
||||
|
||||
use Core\Objects\Context;
|
||||
|
||||
abstract class PermissionAPI extends Request {
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
|
||||
parent::__construct($context, $externalCall, $params);
|
||||
}
|
||||
|
||||
protected function checkStaticPermission(): bool {
|
||||
$user = $this->context->getUser();
|
||||
if (!$user || !$user->hasGroup(USER_GROUP_ADMIN)) {
|
||||
return $this->createError("Permission denied.");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace Core\API\Permission {
|
||||
|
||||
use Core\API\Parameter\Parameter;
|
||||
use Core\API\Parameter\StringType;
|
||||
use Core\API\PermissionAPI;
|
||||
use Core\Driver\SQL\Column\Column;
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Driver\SQL\Condition\CondIn;
|
||||
use Core\Driver\SQL\Condition\CondLike;
|
||||
use Core\Driver\SQL\Condition\CondNot;
|
||||
use Core\Driver\SQL\Strategy\UpdateStrategy;
|
||||
use Core\Objects\Context;
|
||||
use Core\Objects\DatabaseEntity\Group;
|
||||
use Core\Objects\DatabaseEntity\User;
|
||||
|
||||
class Check extends PermissionAPI {
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array(
|
||||
'method' => new StringType('method', 323)
|
||||
));
|
||||
|
||||
$this->isPublic = false;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
|
||||
$method = $this->getParam("method");
|
||||
$sql = $this->context->getSQL();
|
||||
$res = $sql->select("groups")
|
||||
->from("ApiPermission")
|
||||
->where(new CondLike($method, new Column("method")))
|
||||
->execute();
|
||||
|
||||
$this->success = ($res !== FALSE);
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
if ($this->success) {
|
||||
if (empty($res) || !is_array($res)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$groups = json_decode($res[0]["groups"]);
|
||||
if (empty($groups)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$currentUser = $this->context->getUser();
|
||||
$userGroups = $currentUser ? $currentUser->getGroups() : [];
|
||||
if (empty($userGroups) || empty(array_intersect($groups, array_keys($userGroups)))) {
|
||||
http_response_code(401);
|
||||
return $this->createError("Permission denied.");
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
class Fetch extends PermissionAPI {
|
||||
|
||||
private ?array $groups;
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array());
|
||||
}
|
||||
|
||||
private function fetchGroups(): bool {
|
||||
$sql = $this->context->getSQL();
|
||||
$this->groups = Group::findAll($sql);
|
||||
$this->success = ($this->groups !== FALSE);
|
||||
$this->lastError = $sql->getLastError();
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
|
||||
if (!$this->fetchGroups()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$sql = $this->context->getSQL();
|
||||
$res = $sql->select("method", "groups", "description")
|
||||
->from("ApiPermission")
|
||||
->execute();
|
||||
|
||||
$this->success = ($res !== FALSE);
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
if ($this->success) {
|
||||
$permissions = array();
|
||||
foreach ($res as $row) {
|
||||
$method = $row["method"];
|
||||
$description = $row["description"];
|
||||
$groups = json_decode($row["groups"]);
|
||||
$permissions[] = array(
|
||||
"method" => $method,
|
||||
"groups" => $groups,
|
||||
"description" => $description
|
||||
);
|
||||
}
|
||||
$this->result["permissions"] = $permissions;
|
||||
$this->result["groups"] = $this->groups;
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
class Save extends PermissionAPI {
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array(
|
||||
'permissions' => new Parameter('permissions', Parameter::TYPE_ARRAY)
|
||||
));
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
|
||||
if (!$this->checkStaticPermission()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$permissions = $this->getParam("permissions");
|
||||
$sql = $this->context->getSQL();
|
||||
$methodParam = new StringType('method', 32);
|
||||
$groupsParam = new Parameter('groups', Parameter::TYPE_ARRAY);
|
||||
|
||||
$updateQuery = $sql->insert("ApiPermission", array("method", "groups"))
|
||||
->onDuplicateKeyStrategy(new UpdateStrategy(array("method"), array("groups" => new Column("groups"))));
|
||||
|
||||
$insertedMethods = array();
|
||||
|
||||
foreach ($permissions as $permission) {
|
||||
if (!is_array($permission)) {
|
||||
return $this->createError("Invalid data type found in parameter: permissions, expected: object");
|
||||
} else if (!isset($permission["method"]) || !array_key_exists("groups", $permission)) {
|
||||
return $this->createError("Invalid object found in parameter: permissions, expected keys 'method' and 'groups'");
|
||||
} else if (!$methodParam->parseParam($permission["method"])) {
|
||||
$expectedType = $methodParam->getTypeName();
|
||||
return $this->createError("Invalid data type found for attribute 'method', expected: $expectedType");
|
||||
} else if (!$groupsParam->parseParam($permission["groups"])) {
|
||||
$expectedType = $groupsParam->getTypeName();
|
||||
return $this->createError("Invalid data type found for attribute 'groups', expected: $expectedType");
|
||||
} else if (empty(trim($methodParam->value))) {
|
||||
return $this->createError("Method cannot be empty.");
|
||||
} else {
|
||||
$method = $methodParam->value;
|
||||
$groups = $groupsParam->value;
|
||||
$updateQuery->addRow($method, $groups);
|
||||
$insertedMethods[] = $method;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($permissions)) {
|
||||
$res = $updateQuery->execute();
|
||||
$this->success = ($res !== FALSE);
|
||||
$this->lastError = $sql->getLastError();
|
||||
}
|
||||
|
||||
if ($this->success) {
|
||||
$res = $sql->delete("ApiPermission")
|
||||
->where(new Compare("description", "")) // only delete non default permissions
|
||||
->where(new CondNot(new CondIn(new Column("method"), $insertedMethods)))
|
||||
->execute();
|
||||
|
||||
$this->success = ($res !== FALSE);
|
||||
$this->lastError = $sql->getLastError();
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
}
|
||||
435
Core/API/Request.class.php
Normal file
435
Core/API/Request.class.php
Normal file
@@ -0,0 +1,435 @@
|
||||
<?php
|
||||
|
||||
namespace Core\API;
|
||||
|
||||
use Core\Driver\Logger\Logger;
|
||||
use Core\Objects\Context;
|
||||
use PhpMqtt\Client\MqttClient;
|
||||
|
||||
/**
|
||||
* TODO: we need following features, probably as abstract/generic class/method:
|
||||
* - easy way for pagination (select with limit/offset)
|
||||
* - CRUD Endpoints/Objects (Create, Update, Delete)
|
||||
*/
|
||||
|
||||
abstract class Request {
|
||||
|
||||
protected Context $context;
|
||||
protected Logger $logger;
|
||||
protected array $params;
|
||||
protected string $lastError;
|
||||
protected array $result;
|
||||
protected bool $success;
|
||||
protected bool $isPublic;
|
||||
protected bool $loginRequired;
|
||||
protected bool $variableParamCount;
|
||||
protected bool $isDisabled;
|
||||
protected bool $apiKeyAllowed;
|
||||
protected bool $csrfTokenRequired;
|
||||
|
||||
private array $defaultParams;
|
||||
private array $allowedMethods;
|
||||
private bool $externalCall;
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
|
||||
$this->context = $context;
|
||||
$this->logger = new Logger($this->getAPIName(), $this->context->getSQL());
|
||||
$this->defaultParams = $params;
|
||||
|
||||
$this->success = false;
|
||||
$this->result = array();
|
||||
$this->externalCall = $externalCall;
|
||||
$this->isPublic = true;
|
||||
$this->isDisabled = false;
|
||||
$this->loginRequired = false;
|
||||
$this->variableParamCount = false;
|
||||
$this->apiKeyAllowed = true;
|
||||
$this->allowedMethods = array("GET", "POST");
|
||||
$this->lastError = "";
|
||||
$this->csrfTokenRequired = true;
|
||||
}
|
||||
|
||||
public function getAPIName(): string {
|
||||
if (get_class($this) === Request::class) {
|
||||
return "API";
|
||||
}
|
||||
|
||||
$reflection = new \ReflectionClass($this);
|
||||
if ($reflection->getParentClass()->isAbstract() && $reflection->getParentClass()->isSubclassOf(Request::class)) {
|
||||
return $reflection->getParentClass()->getShortName() . "/" . $reflection->getShortName();
|
||||
} else {
|
||||
return $reflection->getShortName();
|
||||
}
|
||||
}
|
||||
|
||||
protected function forbidMethod($method) {
|
||||
if (($key = array_search($method, $this->allowedMethods)) !== false) {
|
||||
unset($this->allowedMethods[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
public function getDefaultParams(): array {
|
||||
return $this->defaultParams;
|
||||
}
|
||||
|
||||
public function isDisabled(): bool {
|
||||
return $this->isDisabled;
|
||||
}
|
||||
|
||||
protected function allowMethod($method) {
|
||||
$availableMethods = ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "TRACE", "CONNECT"];
|
||||
if (in_array($method, $availableMethods) && !in_array($method, $this->allowedMethods)) {
|
||||
$this->allowedMethods[] = $method;
|
||||
}
|
||||
}
|
||||
|
||||
protected function getRequestMethod() {
|
||||
return $_SERVER["REQUEST_METHOD"];
|
||||
}
|
||||
|
||||
public function parseParams($values, $structure = NULL): bool {
|
||||
|
||||
if ($structure === NULL) {
|
||||
$structure = $this->params;
|
||||
}
|
||||
|
||||
foreach ($structure as $name => $param) {
|
||||
$value = $values[$name] ?? NULL;
|
||||
|
||||
$isEmpty = (is_string($value) && strlen($value) === 0) || (is_array($value) && empty($value));
|
||||
if (!$param->optional && (is_null($value) || $isEmpty)) {
|
||||
return $this->createError("Missing parameter: $name");
|
||||
}
|
||||
|
||||
$param->reset();
|
||||
if (!is_null($value) && !$isEmpty) {
|
||||
if (!$param->parseParam($value)) {
|
||||
$value = print_r($value, true);
|
||||
return $this->createError("Invalid Type for parameter: $name '$value' (Required: " . $param->getTypeName() . ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function parseVariableParams($values) {
|
||||
foreach ($values as $name => $value) {
|
||||
if (isset($this->params[$name])) continue;
|
||||
$type = Parameter\Parameter::parseType($value);
|
||||
$param = new Parameter\Parameter($name, $type, true);
|
||||
$param->parseParam($value);
|
||||
$this->params[$name] = $param;
|
||||
}
|
||||
}
|
||||
|
||||
// wrapper for unit tests
|
||||
protected function _die(string $data = ""): bool {
|
||||
die($data);
|
||||
}
|
||||
|
||||
protected abstract function _execute(): bool;
|
||||
|
||||
public final function execute($values = array()): bool {
|
||||
|
||||
$this->params = array_merge([], $this->defaultParams);
|
||||
$this->success = false;
|
||||
$this->result = array();
|
||||
$this->lastError = '';
|
||||
|
||||
$session = $this->context->getSession();
|
||||
if ($session) {
|
||||
$this->result['logoutIn'] = $session->getExpiresSeconds();
|
||||
}
|
||||
|
||||
if ($this->externalCall) {
|
||||
$values = $_REQUEST;
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_SERVER["CONTENT_TYPE"]) && in_array("application/json", explode(";", $_SERVER["CONTENT_TYPE"]))) {
|
||||
$jsonData = json_decode(file_get_contents('php://input'), true);
|
||||
if ($jsonData !== null) {
|
||||
$values = array_merge($values, $jsonData);
|
||||
} else {
|
||||
$this->lastError = 'Invalid request body.';
|
||||
http_response_code(400);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->isDisabled) {
|
||||
$this->lastError = "This function is currently disabled.";
|
||||
http_response_code(503);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->externalCall && !$this->isPublic) {
|
||||
$this->lastError = 'This function is private.';
|
||||
http_response_code(403);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->externalCall) {
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(204); # No content
|
||||
header("Allow: OPTIONS, " . implode(", ", $this->allowedMethods));
|
||||
return $this->_die();
|
||||
}
|
||||
|
||||
// check the request method
|
||||
if (!in_array($_SERVER['REQUEST_METHOD'], $this->allowedMethods)) {
|
||||
$this->lastError = 'This method is not allowed';
|
||||
http_response_code(405);
|
||||
return false;
|
||||
}
|
||||
|
||||
$apiKeyAuthorized = false;
|
||||
if (!$session && $this->apiKeyAllowed) {
|
||||
if (isset($_SERVER["HTTP_AUTHORIZATION"])) {
|
||||
$authHeader = $_SERVER["HTTP_AUTHORIZATION"];
|
||||
if (startsWith($authHeader, "Bearer ")) {
|
||||
$apiKey = substr($authHeader, strlen("Bearer "));
|
||||
$apiKeyAuthorized = $this->context->loadApiKey($apiKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Logged in or api key authorized?
|
||||
if ($this->loginRequired) {
|
||||
if (!$session && !$apiKeyAuthorized) {
|
||||
$this->lastError = 'You are not logged in.';
|
||||
http_response_code(401);
|
||||
return false;
|
||||
} else if ($session) {
|
||||
$tfaToken = $session->getUser()->getTwoFactorToken();
|
||||
if ($tfaToken && $tfaToken->isConfirmed() && !$tfaToken->isAuthenticated()) {
|
||||
$this->lastError = '2FA-Authorization is required';
|
||||
http_response_code(401);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CSRF Token
|
||||
if ($this->csrfTokenRequired && $session) {
|
||||
// csrf token required + external call
|
||||
// if it's not a call with API_KEY, check for csrf_token
|
||||
$csrfToken = $values["csrf_token"] ?? $_SERVER["HTTP_XSRF_TOKEN"] ?? null;
|
||||
if (!$csrfToken || strcmp($csrfToken, $session->getCsrfToken()) !== 0) {
|
||||
$this->lastError = "CSRF-Token mismatch";
|
||||
http_response_code(403);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for permission
|
||||
if (!($this instanceof \API\Permission\Save)) {
|
||||
$req = new \Core\API\Permission\Check($this->context);
|
||||
$this->success = $req->execute(array("method" => $this->getMethod()));
|
||||
$this->lastError = $req->getLastError();
|
||||
if (!$this->success) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->parseParams($values)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->variableParamCount) {
|
||||
$this->parseVariableParams($values);
|
||||
}
|
||||
|
||||
$sql = $this->context->getSQL();
|
||||
if (!$sql->isConnected()) {
|
||||
$this->lastError = $sql->getLastError();
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->success = true;
|
||||
$success = $this->_execute();
|
||||
if ($this->success !== $success) {
|
||||
// _execute returns a different value then it set for $this->success
|
||||
// this should actually not occur, how to handle this case?
|
||||
$this->success = $success;
|
||||
}
|
||||
|
||||
$sql->setLastError('');
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
protected function createError($err): bool {
|
||||
$this->success = false;
|
||||
$this->lastError = $err;
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function getParam($name, $obj = NULL) {
|
||||
// i don't know why phpstorm
|
||||
if ($obj === NULL) {
|
||||
$obj = $this->params;
|
||||
}
|
||||
|
||||
return (isset($obj[$name]) ? $obj[$name]->value : NULL);
|
||||
}
|
||||
|
||||
public function isMethodAllowed(string $method): bool {
|
||||
return in_array($method, $this->allowedMethods);
|
||||
}
|
||||
|
||||
public function isPublic(): bool {
|
||||
return $this->isPublic;
|
||||
}
|
||||
|
||||
public function getLastError(): string {
|
||||
return $this->lastError;
|
||||
}
|
||||
|
||||
public function getResult(): array {
|
||||
return $this->result;
|
||||
}
|
||||
|
||||
public function success(): bool {
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public function loginRequired(): bool {
|
||||
return $this->loginRequired;
|
||||
}
|
||||
|
||||
public function isExternalCall(): bool {
|
||||
return $this->externalCall;
|
||||
}
|
||||
|
||||
private function getMethod() {
|
||||
$class = str_replace("\\", "/", get_class($this));
|
||||
$class = substr($class, strlen("api/"));
|
||||
return $class;
|
||||
}
|
||||
|
||||
public function getJsonResult(): string {
|
||||
$this->result['success'] = $this->success;
|
||||
$this->result['msg'] = $this->lastError;
|
||||
return json_encode($this->result);
|
||||
}
|
||||
|
||||
protected function disableOutputBuffer() {
|
||||
ob_implicit_flush(true);
|
||||
$levels = ob_get_level();
|
||||
for ( $i = 0; $i < $levels; $i ++ ) {
|
||||
ob_end_flush();
|
||||
}
|
||||
flush();
|
||||
}
|
||||
|
||||
protected function disableCache() {
|
||||
header("Last-Modified: " . (new \DateTime())->format("D, d M Y H:i:s T"));
|
||||
header("Expires: Sat, 26 Jul 1997 05:00:00 GMT");
|
||||
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
|
||||
header("Cache-Control: post-check=0, pre-check=0", false);
|
||||
header("Pragma: no-cache");
|
||||
}
|
||||
|
||||
protected function setupSSE() {
|
||||
$this->context->sendCookies();
|
||||
$this->context->getSQL()?->close();
|
||||
set_time_limit(0);
|
||||
ignore_user_abort(true);
|
||||
header('Content-Type: text/event-stream');
|
||||
header('Connection: keep-alive');
|
||||
header('X-Accel-Buffering: no');
|
||||
$this->disableCache();
|
||||
$this->disableOutputBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \PhpMqtt\Client\Exceptions\ProtocolViolationException
|
||||
* @throws \PhpMqtt\Client\Exceptions\DataTransferException
|
||||
* @throws \PhpMqtt\Client\Exceptions\MqttClientException
|
||||
*/
|
||||
protected function startMqttSSE(MqttClient $mqtt, callable $onPing) {
|
||||
$lastPing = 0;
|
||||
$mqtt->registerLoopEventHandler(function(MqttClient $mqtt, $elapsed) use (&$lastPing, $onPing) {
|
||||
if ($elapsed - $lastPing >= 5) {
|
||||
$onPing();
|
||||
$lastPing = $elapsed;
|
||||
}
|
||||
|
||||
if (connection_status() !== 0) {
|
||||
$mqtt->interrupt();
|
||||
}
|
||||
});
|
||||
|
||||
$mqtt->loop();
|
||||
$this->lastError = "MQTT Loop disconnected";
|
||||
$mqtt->disconnect();
|
||||
}
|
||||
|
||||
protected function processImageUpload(string $uploadDir, array $allowedExtensions = ["jpg","jpeg","png","gif"], $transformCallback = null) {
|
||||
if (empty($_FILES)) {
|
||||
return $this->createError("You need to upload an image.");
|
||||
} else if (count($_FILES) > 1) {
|
||||
return $this->createError("You can only upload one image at once.");
|
||||
}
|
||||
|
||||
$upload = array_values($_FILES)[0];
|
||||
if (is_array($upload["name"])) {
|
||||
return $this->createError("You can only upload one image at once.");
|
||||
} else if ($upload["error"] !== UPLOAD_ERR_OK) {
|
||||
return $this->createError("There was an error uploading the image, code: " . $upload["error"]);
|
||||
}
|
||||
|
||||
$imageName = $upload["name"];
|
||||
$ext = strtolower(pathinfo($imageName, PATHINFO_EXTENSION));
|
||||
if (!in_array($ext, $allowedExtensions)) {
|
||||
return $this->createError("Only the following file extensions are allowed: " . implode(",", $allowedExtensions));
|
||||
}
|
||||
|
||||
if (!is_dir($uploadDir) && !mkdir($uploadDir, 0777, true)) {
|
||||
return $this->createError("Upload directory does not exist and could not be created.");
|
||||
}
|
||||
|
||||
$srcPath = $upload["tmp_name"];
|
||||
$mimeType = mime_content_type($srcPath);
|
||||
if (!startsWith($mimeType, "image/")) {
|
||||
return $this->createError("Uploaded file is not an image.");
|
||||
}
|
||||
|
||||
try {
|
||||
$image = new \Imagick($srcPath);
|
||||
|
||||
// strip exif
|
||||
$profiles = $image->getImageProfiles("icc", true);
|
||||
$image->stripImage();
|
||||
if (!empty($profiles)) {
|
||||
$image->profileImage("icc", $profiles['icc']);
|
||||
}
|
||||
} catch (\ImagickException $ex) {
|
||||
return $this->createError("Error loading image: " . $ex->getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
if ($transformCallback) {
|
||||
$fileName = call_user_func([$this, $transformCallback], $image, $uploadDir);
|
||||
} else {
|
||||
|
||||
$image->writeImage($srcPath);
|
||||
$image->destroy();
|
||||
|
||||
$uuid = uuidv4();
|
||||
$fileName = "$uuid.$ext";
|
||||
$destPath = "$uploadDir/$fileName";
|
||||
if (!file_exists($destPath)) {
|
||||
if (!@move_uploaded_file($srcPath, $destPath)) {
|
||||
return $this->createError("Could not store uploaded file.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [$fileName, $imageName];
|
||||
} catch (\ImagickException $ex) {
|
||||
return $this->createError("Error processing image: " . $ex->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
403
Core/API/RoutesAPI.class.php
Normal file
403
Core/API/RoutesAPI.class.php
Normal file
@@ -0,0 +1,403 @@
|
||||
<?php
|
||||
|
||||
namespace Core\API {
|
||||
|
||||
use Core\API\Routes\GenerateCache;
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Objects\Context;
|
||||
|
||||
abstract class RoutesAPI extends Request {
|
||||
|
||||
const ACTIONS = array("redirect_temporary", "redirect_permanently", "static", "dynamic");
|
||||
const ROUTER_CACHE_CLASS = "\\Core\\Cache\\RouterCache";
|
||||
|
||||
protected string $routerCachePath;
|
||||
|
||||
public function __construct(Context $context, bool $externalCall, array $params) {
|
||||
parent::__construct($context, $externalCall, $params);
|
||||
$this->routerCachePath = getClassPath(self::ROUTER_CACHE_CLASS);
|
||||
}
|
||||
|
||||
protected function routeExists($uid): bool {
|
||||
$sql = $this->context->getSQL();
|
||||
$res = $sql->select($sql->count())
|
||||
->from("Route")
|
||||
->where(new Compare("id", $uid))
|
||||
->execute();
|
||||
|
||||
$this->success = ($res !== false);
|
||||
$this->lastError = $sql->getLastError();
|
||||
if ($this->success) {
|
||||
if ($res[0]["count"] === 0) {
|
||||
return $this->createError("Route not found");
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
protected function toggleRoute($uid, $active): bool {
|
||||
if (!$this->routeExists($uid)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$sql = $this->context->getSQL();
|
||||
$this->success = $sql->update("Route")
|
||||
->set("active", $active)
|
||||
->where(new Compare("id", $uid))
|
||||
->execute();
|
||||
|
||||
$this->lastError = $sql->getLastError();
|
||||
$this->success = $this->success && $this->regenerateCache();
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
protected function regenerateCache(): bool {
|
||||
$req = new GenerateCache($this->context);
|
||||
$this->success = $req->execute();
|
||||
$this->lastError = $req->getLastError();
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace Core\API\Routes {
|
||||
|
||||
use Core\API\Parameter\Parameter;
|
||||
use Core\API\Parameter\StringType;
|
||||
use Core\API\RoutesAPI;
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Driver\SQL\Condition\CondBool;
|
||||
use Core\Objects\Context;
|
||||
use Core\Objects\Router\DocumentRoute;
|
||||
use Core\Objects\Router\RedirectRoute;
|
||||
use Core\Objects\Router\Router;
|
||||
use Core\Objects\Router\StaticFileRoute;
|
||||
|
||||
class Fetch extends RoutesAPI {
|
||||
|
||||
public function __construct(Context $context, $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array());
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
$sql = $this->context->getSQL();
|
||||
|
||||
$res = $sql
|
||||
->select("id", "request", "action", "target", "extra", "active", "exact")
|
||||
->from("Route")
|
||||
->orderBy("id")
|
||||
->ascending()
|
||||
->execute();
|
||||
|
||||
$this->lastError = $sql->getLastError();
|
||||
$this->success = ($res !== FALSE);
|
||||
|
||||
if ($this->success) {
|
||||
$routes = array();
|
||||
foreach ($res as $row) {
|
||||
$routes[] = array(
|
||||
"id" => intval($row["id"]),
|
||||
"request" => $row["request"],
|
||||
"action" => $row["action"],
|
||||
"target" => $row["target"],
|
||||
"extra" => $row["extra"] ?? "",
|
||||
"active" => intval($sql->parseBool($row["active"])),
|
||||
"exact" => intval($sql->parseBool($row["exact"])),
|
||||
);
|
||||
}
|
||||
|
||||
$this->result["routes"] = $routes;
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
class Save extends RoutesAPI {
|
||||
|
||||
private array $routes;
|
||||
|
||||
public function __construct(Context $context, $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array(
|
||||
'routes' => new Parameter('routes', Parameter::TYPE_ARRAY, false)
|
||||
));
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
if (!$this->validateRoutes()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$sql = $this->context->getSQL();
|
||||
|
||||
// DELETE old rules
|
||||
$this->success = ($sql->truncate("Route")->execute() !== FALSE);
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
// INSERT new routes
|
||||
if ($this->success) {
|
||||
$stmt = $sql->insert("Route", array("request", "action", "target", "extra", "active", "exact"));
|
||||
|
||||
foreach ($this->routes as $route) {
|
||||
$stmt->addRow($route["request"], $route["action"], $route["target"], $route["extra"], $route["active"], $route["exact"]);
|
||||
}
|
||||
$this->success = ($stmt->execute() !== FALSE);
|
||||
$this->lastError = $sql->getLastError();
|
||||
}
|
||||
|
||||
$this->success = $this->success && $this->regenerateCache();
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
private function validateRoutes(): bool {
|
||||
|
||||
$this->routes = array();
|
||||
$keys = array(
|
||||
"request" => [Parameter::TYPE_STRING, Parameter::TYPE_INT],
|
||||
"action" => Parameter::TYPE_STRING,
|
||||
"target" => Parameter::TYPE_STRING,
|
||||
"extra" => Parameter::TYPE_STRING,
|
||||
"active" => Parameter::TYPE_BOOLEAN,
|
||||
"exact" => Parameter::TYPE_BOOLEAN,
|
||||
);
|
||||
|
||||
foreach ($this->getParam("routes") as $index => $route) {
|
||||
foreach ($keys as $key => $expectedType) {
|
||||
if (!array_key_exists($key, $route)) {
|
||||
return $this->createError("Route $index missing key: $key");
|
||||
}
|
||||
|
||||
$value = $route[$key];
|
||||
$type = Parameter::parseType($value);
|
||||
if (!is_array($expectedType)) {
|
||||
$expectedType = [$expectedType];
|
||||
}
|
||||
|
||||
if (!in_array($type, $expectedType)) {
|
||||
if (count($expectedType) > 0) {
|
||||
$expectedTypeName = "expected: " . Parameter::names[$expectedType];
|
||||
} else {
|
||||
$expectedTypeName = "expected one of: " . implode(",", array_map(
|
||||
function ($type) {
|
||||
return Parameter::names[$type];
|
||||
}, $expectedType));
|
||||
}
|
||||
$gotTypeName = Parameter::names[$type];
|
||||
return $this->createError("Route $index has invalid value for key: $key, $expectedTypeName, got: $gotTypeName");
|
||||
}
|
||||
}
|
||||
|
||||
$action = $route["action"];
|
||||
if (!in_array($action, self::ACTIONS)) {
|
||||
return $this->createError("Invalid action: $action");
|
||||
}
|
||||
|
||||
if (empty($route["request"])) {
|
||||
return $this->createError("Request cannot be empty.");
|
||||
}
|
||||
|
||||
if (empty($route["target"])) {
|
||||
return $this->createError("Target cannot be empty.");
|
||||
}
|
||||
|
||||
$this->routes[] = $route;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class Add extends RoutesAPI {
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array(
|
||||
"request" => new StringType("request", 128),
|
||||
"action" => new StringType("action"),
|
||||
"target" => new StringType("target", 128),
|
||||
"extra" => new StringType("extra", 64, true, ""),
|
||||
));
|
||||
$this->isPublic = false;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
|
||||
$request = $this->getParam("request");
|
||||
$action = $this->getParam("action");
|
||||
$target = $this->getParam("target");
|
||||
$extra = $this->getParam("extra");
|
||||
|
||||
if (!in_array($action, self::ACTIONS)) {
|
||||
return $this->createError("Invalid action: $action");
|
||||
}
|
||||
|
||||
$sql = $this->context->getSQL();
|
||||
$this->success = $sql->insert("Route", ["request", "action", "target", "extra"])
|
||||
->addRow($request, $action, $target, $extra)
|
||||
->execute();
|
||||
|
||||
$this->lastError = $sql->getLastError();
|
||||
$this->success = $this->success && $this->regenerateCache();
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
class Update extends RoutesAPI {
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array(
|
||||
"id" => new Parameter("id", Parameter::TYPE_INT),
|
||||
"request" => new StringType("request", 128),
|
||||
"action" => new StringType("action"),
|
||||
"target" => new StringType("target", 128),
|
||||
"extra" => new StringType("extra", 64, true, ""),
|
||||
));
|
||||
$this->isPublic = false;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
|
||||
$id = $this->getParam("id");
|
||||
if (!$this->routeExists($id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$request = $this->getParam("request");
|
||||
$action = $this->getParam("action");
|
||||
$target = $this->getParam("target");
|
||||
$extra = $this->getParam("extra");
|
||||
if (!in_array($action, self::ACTIONS)) {
|
||||
return $this->createError("Invalid action: $action");
|
||||
}
|
||||
|
||||
$sql = $this->context->getSQL();
|
||||
$this->success = $sql->update("Route")
|
||||
->set("request", $request)
|
||||
->set("action", $action)
|
||||
->set("target", $target)
|
||||
->set("extra", $extra)
|
||||
->where(new Compare("id", $id))
|
||||
->execute();
|
||||
|
||||
$this->lastError = $sql->getLastError();
|
||||
$this->success = $this->success && $this->regenerateCache();
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
class Remove extends RoutesAPI {
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array(
|
||||
"id" => new Parameter("id", Parameter::TYPE_INT)
|
||||
));
|
||||
$this->isPublic = false;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
|
||||
$uid = $this->getParam("id");
|
||||
if (!$this->routeExists($uid)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$sql = $this->context->getSQL();
|
||||
$this->success = $sql->delete("Route")
|
||||
->where(new Compare("id", $uid))
|
||||
->execute();
|
||||
|
||||
$this->lastError = $sql->getLastError();
|
||||
$this->success = $this->success && $this->regenerateCache();
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
class Enable extends RoutesAPI {
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array(
|
||||
"id" => new Parameter("id", Parameter::TYPE_INT)
|
||||
));
|
||||
$this->isPublic = false;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
$uid = $this->getParam("id");
|
||||
return $this->toggleRoute($uid, true);
|
||||
}
|
||||
}
|
||||
|
||||
class Disable extends RoutesAPI {
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array(
|
||||
"id" => new Parameter("id", Parameter::TYPE_INT)
|
||||
));
|
||||
$this->isPublic = false;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
$uid = $this->getParam("id");
|
||||
return $this->toggleRoute($uid, false);
|
||||
}
|
||||
}
|
||||
|
||||
class GenerateCache extends RoutesAPI {
|
||||
|
||||
private ?Router $router;
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, []);
|
||||
$this->isPublic = false;
|
||||
$this->router = null;
|
||||
}
|
||||
|
||||
protected function _execute(): bool {
|
||||
$sql = $this->context->getSQL();
|
||||
$res = $sql
|
||||
->select("id", "request", "action", "target", "extra", "exact")
|
||||
->from("Route")
|
||||
->where(new CondBool("active"))
|
||||
->orderBy("id")->ascending()
|
||||
->execute();
|
||||
|
||||
$this->success = $res !== false;
|
||||
$this->lastError = $sql->getLastError();
|
||||
if (!$this->success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->router = new Router($this->context);
|
||||
foreach ($res as $row) {
|
||||
$request = $row["request"];
|
||||
$target = $row["target"];
|
||||
$exact = $sql->parseBool($row["exact"]);
|
||||
switch ($row["action"]) {
|
||||
case "redirect_temporary":
|
||||
$this->router->addRoute(new RedirectRoute($request, $exact, $target, 307));
|
||||
break;
|
||||
case "redirect_permanently":
|
||||
$this->router->addRoute(new RedirectRoute($request, $exact, $target, 308));
|
||||
break;
|
||||
case "static":
|
||||
$this->router->addRoute(new StaticFileRoute($request, $exact, $target));
|
||||
break;
|
||||
case "dynamic":
|
||||
$extra = json_decode($row["extra"]) ?? [];
|
||||
$this->router->addRoute(new DocumentRoute($request, $exact, $target, ...$extra));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->success = $this->router->writeCache($this->routerCachePath);
|
||||
if (!$this->success) {
|
||||
return $this->createError("Error saving router cache file: " . $this->routerCachePath);
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public function getRouter(): ?Router {
|
||||
return $this->router;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
41
Core/API/Search.class.php
Normal file
41
Core/API/Search.class.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Core\API;
|
||||
|
||||
use Core\API\Parameter\StringType;
|
||||
use Core\Objects\Context;
|
||||
use Core\Objects\Search\Searchable;
|
||||
use Core\Objects\Search\SearchQuery;
|
||||
|
||||
class Search extends Request {
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
|
||||
parent::__construct($context, $externalCall, [
|
||||
"text" => new StringType("text", 32)
|
||||
]);
|
||||
}
|
||||
|
||||
protected function _execute(): bool {
|
||||
|
||||
$query = new SearchQuery(trim($this->getParam("text")));
|
||||
if (strlen($query->getQuery()) < 3) {
|
||||
return $this->createError("You have to type at least 3 characters to search for");
|
||||
}
|
||||
|
||||
$router = $this->context->router;
|
||||
if ($router === null) {
|
||||
return $this->createError("There is currently no router configured. Error during installation?");
|
||||
}
|
||||
|
||||
$this->result["results"] = [];
|
||||
foreach ($router->getRoutes(false) as $route) {
|
||||
if(in_array(Searchable::class, array_keys((new \ReflectionClass($route))->getTraits()))) {
|
||||
foreach ($route->doSearch($this->context, $query) as $searchResult) {
|
||||
$this->result["results"][] = $searchResult->jsonSerialize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
194
Core/API/SettingsAPI.class.php
Normal file
194
Core/API/SettingsAPI.class.php
Normal file
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
|
||||
namespace Core\API {
|
||||
|
||||
use Core\Objects\Context;
|
||||
|
||||
abstract class SettingsAPI extends Request {
|
||||
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
|
||||
parent::__construct($context, $externalCall, $params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace Core\API\Settings {
|
||||
|
||||
use Core\API\Parameter\Parameter;
|
||||
use Core\API\Parameter\StringType;
|
||||
use Core\API\SettingsAPI;
|
||||
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\Condition\CondNot;
|
||||
use Core\Driver\SQL\Condition\CondRegex;
|
||||
use Core\Driver\SQL\Strategy\UpdateStrategy;
|
||||
use Core\Objects\Context;
|
||||
|
||||
class Get extends SettingsAPI {
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array(
|
||||
'key' => new StringType('key', -1, true, NULL)
|
||||
));
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
$key = $this->getParam("key");
|
||||
$sql = $this->context->getSQL();
|
||||
|
||||
$query = $sql->select("name", "value") ->from("Settings");
|
||||
|
||||
if (!is_null($key)) {
|
||||
$query->where(new CondRegex(new Column("name"), $key));
|
||||
}
|
||||
|
||||
// filter sensitive values, if called from outside
|
||||
if ($this->isExternalCall()) {
|
||||
$query->where(new CondNot("private"));
|
||||
}
|
||||
|
||||
$res = $query->execute();
|
||||
|
||||
$this->success = ($res !== FALSE);
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
if ($this->success) {
|
||||
$settings = array();
|
||||
foreach($res as $row) {
|
||||
$settings[$row["name"]] = $row["value"];
|
||||
}
|
||||
$this->result["settings"] = $settings;
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
class Set extends SettingsAPI {
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array(
|
||||
'settings' => new Parameter('settings', Parameter::TYPE_ARRAY)
|
||||
));
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
$values = $this->getParam("settings");
|
||||
if (empty($values)) {
|
||||
return $this->createError("No values given.");
|
||||
}
|
||||
|
||||
$paramKey = new StringType('key', 32);
|
||||
$paramValue = new StringType('value', 1024, true, NULL);
|
||||
|
||||
$sql = $this->context->getSQL();
|
||||
$query = $sql->insert("Settings", array("name", "value"));
|
||||
$keys = array();
|
||||
$deleteKeys = array();
|
||||
|
||||
foreach($values as $key => $value) {
|
||||
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: '$value' (Required: " . $paramValue->getTypeName() . ")");
|
||||
} else if(preg_match("/^[a-zA-Z_][a-zA-Z_0-9-]*$/", $paramKey->value) !== 1) {
|
||||
return $this->createError("The property key should only contain alphanumeric characters, underscores and dashes");
|
||||
} else {
|
||||
if (!is_null($paramValue->value)) {
|
||||
$query->addRow($paramKey->value, $paramValue->value);
|
||||
} else {
|
||||
$deleteKeys[] = $paramKey->value;
|
||||
}
|
||||
$keys[] = $paramKey->value;
|
||||
}
|
||||
}
|
||||
|
||||
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($keys)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (count($deleteKeys) !== count($keys)) {
|
||||
$query->onDuplicateKeyStrategy(new UpdateStrategy(
|
||||
array("name"),
|
||||
array("value" => new Column("value")))
|
||||
);
|
||||
|
||||
|
||||
$this->success = ($query->execute() !== FALSE);
|
||||
$this->lastError = $sql->getLastError();
|
||||
}
|
||||
|
||||
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) {
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
class GenerateJWT extends SettingsAPI {
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, [
|
||||
"type" => new StringType("type", 32, true, "HS512")
|
||||
]);
|
||||
}
|
||||
|
||||
protected function _execute(): bool {
|
||||
$algorithm = $this->getParam("type");
|
||||
if (!Settings::isJwtAlgorithmSupported($algorithm)) {
|
||||
return $this->createError("Algorithm is not supported");
|
||||
}
|
||||
|
||||
$settings = $this->context->getSettings();
|
||||
if (!$settings->generateJwtKey($algorithm)) {
|
||||
return $this->createError("Error generating JWT-Key: " . $settings->getLogger()->getLastMessage());
|
||||
}
|
||||
|
||||
$saveRequest = $settings->saveJwtKey($this->context);
|
||||
if (!$saveRequest->success()) {
|
||||
return $this->createError("Error saving JWT-Key: " . $saveRequest->getLastError());
|
||||
}
|
||||
|
||||
$this->result["jwt_public_key"] = $settings->getJwtPublicKey(false)?->getKeyMaterial();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
112
Core/API/Stats.class.php
Normal file
112
Core/API/Stats.class.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace Core\API;
|
||||
|
||||
use DateTime;
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Driver\SQL\Condition\CondBool;
|
||||
use Core\Objects\Context;
|
||||
use Core\Objects\DatabaseEntity\User;
|
||||
|
||||
class Stats extends Request {
|
||||
|
||||
private bool $mailConfigured;
|
||||
private bool $recaptchaConfigured;
|
||||
|
||||
public function __construct(Context $context, $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array());
|
||||
}
|
||||
|
||||
private function getUserCount() {
|
||||
$sql = $this->context->getSQL();
|
||||
$res = $sql->select($sql->count())->from("User")->execute();
|
||||
$this->success = $this->success && ($res !== FALSE);
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
return ($this->success ? $res[0]["count"] : 0);
|
||||
}
|
||||
|
||||
private function getPageCount() {
|
||||
$sql = $this->context->getSQL();
|
||||
$res = $sql->select($sql->count())->from("Route")
|
||||
->where(new CondBool("active"))
|
||||
->execute();
|
||||
$this->success = $this->success && ($res !== FALSE);
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
return ($this->success ? $res[0]["count"] : 0);
|
||||
}
|
||||
|
||||
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"] ?? "0") === "1";
|
||||
$this->recaptchaConfigured = ($settings["recaptcha_enabled"] ?? "0") === "1";
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
private function getVisitorCount() {
|
||||
$sql = $this->context->getSQL();
|
||||
$date = new DateTime();
|
||||
$monthStart = $date->format("Ym00");
|
||||
$monthEnd = $date->modify("+1 month")->format("Ym00");
|
||||
$res = $sql->select($sql->count($sql->distinct("cookie")))
|
||||
->from("Visitor")
|
||||
->where(new Compare("day", $monthStart, ">="))
|
||||
->where(new Compare("day", $monthEnd, "<"))
|
||||
->where(new Compare("count", 2, ">="))
|
||||
->execute();
|
||||
|
||||
$this->success = ($res !== false);
|
||||
$this->lastError = $sql->getLastError();
|
||||
return ($this->success ? $res[0]["count"] : $this->success);
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
$userCount = $this->getUserCount();
|
||||
$pageCount = $this->getPageCount();
|
||||
$req = new \Core\API\Visitors\Stats($this->context);
|
||||
$this->success = $req->execute(array("type"=>"monthly"));
|
||||
$this->lastError = $req->getLastError();
|
||||
if (!$this->success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$visitorStatistics = $req->getResult()["visitors"];
|
||||
$visitorCount = $this->getVisitorCount();
|
||||
if (!$this->success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$loadAvg = "Unknown";
|
||||
if (function_exists("sys_getloadavg")) {
|
||||
$loadAvg = sys_getloadavg();
|
||||
}
|
||||
|
||||
if (!$this->checkSettings()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->result["userCount"] = $userCount;
|
||||
$this->result["pageCount"] = $pageCount;
|
||||
$this->result["visitors"] = $visitorStatistics;
|
||||
$this->result["visitorsTotal"] = $visitorCount;
|
||||
$this->result["server"] = array(
|
||||
"version" => WEBBASE_VERSION,
|
||||
"server" => $_SERVER["SERVER_SOFTWARE"] ?? "Unknown",
|
||||
"memory_usage" => memory_get_usage(),
|
||||
"load_avg" => $loadAvg,
|
||||
"database" => $this->context->getSQL()->getStatus(),
|
||||
"mail" => $this->mailConfigured,
|
||||
"reCaptcha" => $this->recaptchaConfigured
|
||||
);
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
}
|
||||
204
Core/API/Swagger.class.php
Normal file
204
Core/API/Swagger.class.php
Normal file
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
|
||||
namespace Core\API;
|
||||
|
||||
use Core\API\Parameter\StringType;
|
||||
use Core\Objects\Context;
|
||||
use Core\Objects\DatabaseEntity\User;
|
||||
|
||||
class Swagger extends Request {
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, []);
|
||||
$this->csrfTokenRequired = false;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
header("Content-Type: application/x-yaml");
|
||||
header("Access-Control-Allow-Origin: *");
|
||||
die($this->getDocumentation());
|
||||
}
|
||||
|
||||
private function getApiEndpoints(): array {
|
||||
|
||||
// first load all direct classes
|
||||
$classes = [];
|
||||
$apiDirs = ["Core", "Site"];
|
||||
foreach ($apiDirs as $apiDir) {
|
||||
$basePath = realpath(WEBROOT . "/$apiDir/Api/");
|
||||
if (!$basePath) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (scandir($basePath) as $fileName) {
|
||||
$fullPath = $basePath . "/" . $fileName;
|
||||
if (is_file($fullPath) && endsWith($fileName, ".class.php")) {
|
||||
require_once $fullPath;
|
||||
$apiName = explode(".", $fileName)[0];
|
||||
$className = "\\API\\$apiName";
|
||||
if (!class_exists($className)) {
|
||||
var_dump("Class not exist: $className");
|
||||
continue;
|
||||
}
|
||||
|
||||
$reflection = new \ReflectionClass($className);
|
||||
if (!$reflection->isSubclassOf(Request::class) || $reflection->isAbstract()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$endpoint = "/" . strtolower($apiName);
|
||||
$classes[$endpoint] = $reflection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// then load all inheriting classes
|
||||
foreach (get_declared_classes() as $declaredClass) {
|
||||
$reflectionClass = new \ReflectionClass($declaredClass);
|
||||
if (!$reflectionClass->isAbstract() && $reflectionClass->isSubclassOf(Request::class)) {
|
||||
$inheritingClass = $reflectionClass->getParentClass();
|
||||
if ($inheritingClass->isAbstract() && endsWith($inheritingClass->getShortName(), "API")) {
|
||||
$endpoint = strtolower(substr($inheritingClass->getShortName(), 0, -3));
|
||||
$endpoint = "/$endpoint/" . lcfirst($reflectionClass->getShortName());
|
||||
$classes[$endpoint] = $reflectionClass;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $classes;
|
||||
}
|
||||
|
||||
private function fetchPermissions(): array {
|
||||
$req = new Permission\Fetch($this->context);
|
||||
$this->success = $req->execute();
|
||||
$permissions = [];
|
||||
foreach( $req->getResult()["permissions"] as $permission) {
|
||||
$permissions["/" . strtolower($permission["method"])] = $permission["groups"];
|
||||
}
|
||||
|
||||
return $permissions;
|
||||
}
|
||||
|
||||
private function canView(array $requiredGroups, Request $request): bool {
|
||||
if (!$request->isPublic()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$currentUser = $this->context->getUser();
|
||||
if (($request->loginRequired() || !empty($requiredGroups)) && !$currentUser) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// special case: hardcoded permission
|
||||
if ($request instanceof Permission\Save && (!$currentUser || !$currentUser->hasGroup(USER_GROUP_ADMIN))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!empty($requiredGroups)) {
|
||||
$userGroups = array_keys($currentUser?->getGroups() ?? []);
|
||||
return !empty(array_intersect($requiredGroups, $userGroups));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function getDocumentation(): string {
|
||||
|
||||
$settings = $this->context->getSettings();
|
||||
$siteName = $settings->getSiteName();
|
||||
$domain = parse_url($settings->getBaseUrl(), PHP_URL_HOST);
|
||||
|
||||
$permissions = $this->fetchPermissions();
|
||||
|
||||
$definitions = [];
|
||||
$paths = [];
|
||||
foreach (self::getApiEndpoints() as $endpoint => $apiClass) {
|
||||
$body = null;
|
||||
$requiredProperties = [];
|
||||
$apiObject = $apiClass->newInstance($this->context, false);
|
||||
if (!$this->canView($permissions[strtolower($endpoint)] ?? [], $apiObject)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parameters = $apiObject->getDefaultParams();
|
||||
if (!empty($parameters)) {
|
||||
$body = [];
|
||||
foreach ($apiObject->getDefaultParams() as $param) {
|
||||
$body[$param->name] = [
|
||||
"type" => $param->getSwaggerTypeName(),
|
||||
"default" => $param->value
|
||||
];
|
||||
|
||||
if ($param instanceof StringType && $param->maxLength > 0) {
|
||||
$body[$param->name]["maxLength"] = $param->maxLength;
|
||||
}
|
||||
|
||||
if ($body[$param->name]["type"] === "string" && ($format = $param->getSwaggerFormat())) {
|
||||
$body[$param->name]["format"] = $format;
|
||||
}
|
||||
|
||||
if (!$param->optional) {
|
||||
$requiredProperties[] = $param->name;
|
||||
}
|
||||
}
|
||||
|
||||
$bodyName = $apiClass->getShortName() . "Body";
|
||||
$definitions[$bodyName] = [
|
||||
"description" => "Body for $endpoint",
|
||||
"properties" => $body
|
||||
];
|
||||
|
||||
if (!empty($requiredProperties)) {
|
||||
$definitions[$bodyName]["required"] = $requiredProperties;
|
||||
}
|
||||
}
|
||||
|
||||
$endPointDefinition = [
|
||||
"post" => [
|
||||
"produces" => ["application/json"],
|
||||
"responses" => [
|
||||
"200" => ["description" => ""],
|
||||
"401" => ["description" => "Login or 2FA Authorization is required"],
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
if ($apiObject->isDisabled()) {
|
||||
$endPointDefinition["post"]["deprecated"] = true;
|
||||
}
|
||||
|
||||
if ($body) {
|
||||
$endPointDefinition["post"]["consumes"] = ["application/json"];
|
||||
$endPointDefinition["post"]["parameters"] = [[
|
||||
"in" => "body",
|
||||
"name" => "body",
|
||||
"required" => !empty($requiredProperties),
|
||||
"schema" => ["\$ref" => "#/definitions/" . $apiClass->getShortName() . "Body"]
|
||||
]];
|
||||
} else if ($apiObject->isMethodAllowed("GET")) {
|
||||
$endPointDefinition["get"] = $endPointDefinition["post"];
|
||||
unset($endPointDefinition["post"]);
|
||||
}
|
||||
|
||||
$paths[$endpoint] = $endPointDefinition;
|
||||
}
|
||||
|
||||
$yamlData = [
|
||||
"swagger" => "2.0",
|
||||
"info" => [
|
||||
"description" => "This is the Backend API-Description of $siteName",
|
||||
"version" => WEBBASE_VERSION,
|
||||
"title" => $siteName,
|
||||
"contact" => [ "email" => "webmaster@$domain" ],
|
||||
],
|
||||
"host" => $domain,
|
||||
"basePath" => "/api",
|
||||
"schemes" => ["https"],
|
||||
"paths" => $paths,
|
||||
"definitions" => $definitions
|
||||
];
|
||||
|
||||
return \yaml_emit($yamlData);
|
||||
|
||||
}
|
||||
}
|
||||
74
Core/API/TemplateAPI.class.php
Normal file
74
Core/API/TemplateAPI.class.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Core\API {
|
||||
|
||||
use Core\Objects\Context;
|
||||
|
||||
abstract class TemplateAPI extends Request {
|
||||
function __construct(Context $context, bool $externalCall = false, array $params = array()) {
|
||||
parent::__construct($context, $externalCall, $params);
|
||||
$this->isPublic = false; // internal API
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace Core\API\Template {
|
||||
|
||||
use Core\API\Parameter\ArrayType;
|
||||
use Core\API\Parameter\Parameter;
|
||||
use Core\API\Parameter\StringType;
|
||||
use Core\API\TemplateAPI;
|
||||
use Core\Objects\Context;
|
||||
use Twig\Environment;
|
||||
use Twig\Error\LoaderError;
|
||||
use Twig\Error\RuntimeError;
|
||||
use Twig\Error\SyntaxError;
|
||||
use Twig\Loader\FilesystemLoader;
|
||||
|
||||
class Render extends TemplateAPI {
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, [
|
||||
"file" => new StringType("file"),
|
||||
"parameters" => new ArrayType("parameters", Parameter::TYPE_MIXED, false, true, [])
|
||||
]);
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
$templateFile = $this->getParam("file");
|
||||
$parameters = $this->getParam("parameters");
|
||||
$extension = pathinfo($templateFile, PATHINFO_EXTENSION);
|
||||
$allowedExtensions = ["html", "twig"];
|
||||
|
||||
if (!in_array($extension, $allowedExtensions)) {
|
||||
return $this->createError("Invalid template file extension. Allowed: " . implode(",", $allowedExtensions));
|
||||
}
|
||||
|
||||
$templateDir = WEBROOT . "/Core/Templates/";
|
||||
$templateCache = WEBROOT . "/Core/Cache/Templates/";
|
||||
$path = realpath($templateDir . $templateFile);
|
||||
if (!startsWith($path, realpath($templateDir))) {
|
||||
return $this->createError("Template file not in template directory");
|
||||
} else if (!is_file($path)) {
|
||||
return $this->createError("Template file not found");
|
||||
}
|
||||
|
||||
$twigLoader = new FilesystemLoader($templateDir);
|
||||
$twigEnvironment = new Environment($twigLoader, [
|
||||
'cache' => $templateCache,
|
||||
'auto_reload' => true
|
||||
]);
|
||||
|
||||
try {
|
||||
$this->result["html"] = $twigEnvironment->render($templateFile, $parameters);
|
||||
} catch (LoaderError | RuntimeError | SyntaxError $e) {
|
||||
return $this->createError("Error rendering twig template: " . $e->getMessage());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
396
Core/API/TfaAPI.class.php
Normal file
396
Core/API/TfaAPI.class.php
Normal file
@@ -0,0 +1,396 @@
|
||||
<?php
|
||||
|
||||
namespace Core\API {
|
||||
|
||||
use Core\Objects\Context;
|
||||
use Core\Objects\TwoFactor\AuthenticationData;
|
||||
use Core\Objects\TwoFactor\KeyBasedTwoFactorToken;
|
||||
|
||||
abstract class TfaAPI extends Request {
|
||||
|
||||
private bool $userVerificationRequired;
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
|
||||
parent::__construct($context, $externalCall, $params);
|
||||
$this->loginRequired = true;
|
||||
$this->apiKeyAllowed = false;
|
||||
$this->userVerificationRequired = false;
|
||||
}
|
||||
|
||||
protected function verifyAuthData(AuthenticationData $authData): bool {
|
||||
$settings = $this->context->getSettings();
|
||||
// $relyingParty = $settings->getSiteName();
|
||||
$domain = parse_url($settings->getBaseUrl(), PHP_URL_HOST);
|
||||
// $domain = "localhost";
|
||||
|
||||
if (!$authData->verifyIntegrity($domain)) {
|
||||
return $this->createError("mismatched rpIDHash. expected: " . hash("sha256", $domain) . " got: " . bin2hex($authData->getHash()));
|
||||
} else if (!$authData->isUserPresent()) {
|
||||
return $this->createError("No user present");
|
||||
} else if ($this->userVerificationRequired && !$authData->isUserVerified()) {
|
||||
return $this->createError("user was not verified on device (PIN/Biometric/...)");
|
||||
} else if ($authData->hasExtensionData()) {
|
||||
return $this->createError("No extensions supported");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function verifyClientDataJSON($jsonData, KeyBasedTwoFactorToken $token): bool {
|
||||
$settings = $this->context->getSettings();
|
||||
$expectedType = $token->isConfirmed() ? "webauthn.get" : "webauthn.create";
|
||||
$type = $jsonData["type"] ?? "null";
|
||||
if ($type !== $expectedType) {
|
||||
return $this->createError("Invalid client data json type. Expected: '$expectedType', Got: '$type'");
|
||||
} else if ($token->getData() !== base64url_decode($jsonData["challenge"] ?? "")) {
|
||||
return $this->createError("Challenge does not match");
|
||||
} else if (($jsonData["origin"] ?? null) !== $settings->getBaseURL()) {
|
||||
$baseUrl = $settings->getBaseURL();
|
||||
return $this->createError("Origin does not match. Expected: '$baseUrl', Got: '${$jsonData["origin"]}'");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace Core\API\TFA {
|
||||
|
||||
use Core\API\Parameter\StringType;
|
||||
use Core\API\TfaAPI;
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Objects\Context;
|
||||
use Core\Objects\TwoFactor\AttestationObject;
|
||||
use Core\Objects\TwoFactor\AuthenticationData;
|
||||
use Core\Objects\TwoFactor\KeyBasedTwoFactorToken;
|
||||
use Core\Objects\TwoFactor\TimeBasedTwoFactorToken;
|
||||
|
||||
// General
|
||||
class Remove extends TfaAPI {
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, [
|
||||
"password" => new StringType("password", 0, true)
|
||||
]);
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
|
||||
$currentUser = $this->context->getUser();
|
||||
$token = $currentUser->getTwoFactorToken();
|
||||
if (!$token) {
|
||||
return $this->createError("You do not have an active 2FA-Token");
|
||||
}
|
||||
|
||||
$sql = $this->context->getSQL();
|
||||
$password = $this->getParam("password");
|
||||
if ($password) {
|
||||
$res = $sql->select("password")
|
||||
->from("User")
|
||||
->where(new Compare("id", $currentUser->getId()))
|
||||
->execute();
|
||||
$this->success = !empty($res);
|
||||
$this->lastError = $sql->getLastError();
|
||||
if (!$this->success) {
|
||||
return false;
|
||||
} else if (!password_verify($password, $res[0]["password"])) {
|
||||
return $this->createError("Wrong password");
|
||||
}
|
||||
} else if ($token->isConfirmed()) {
|
||||
// if the token is fully confirmed, require a password to remove it
|
||||
return $this->createError("Missing parameter: password");
|
||||
}
|
||||
|
||||
$res = $sql->delete("2FA")
|
||||
->where(new Compare("id", $token->getId()))
|
||||
->execute();
|
||||
|
||||
$this->success = $res !== false;
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
if ($this->success && $token->isConfirmed()) {
|
||||
// send an email
|
||||
$settings = $this->context->getSettings();
|
||||
$req = new \Core\API\Template\Render($this->context);
|
||||
$this->success = $req->execute([
|
||||
"file" => "mail/2fa_remove.twig",
|
||||
"parameters" => [
|
||||
"username" => $currentUser->getFullName() ?? $currentUser->getUsername(),
|
||||
"site_name" => $settings->getSiteName(),
|
||||
"sender_mail" => $settings->getMailSender()
|
||||
]
|
||||
]);
|
||||
|
||||
if ($this->success) {
|
||||
$body = $req->getResult()["html"];
|
||||
$gpg = $currentUser->getGPG();
|
||||
$req = new \Core\API\Mail\Send($this->context);
|
||||
$this->success = $req->execute([
|
||||
"to" => $currentUser->getEmail(),
|
||||
"subject" => "[Security Lab] 2FA-Authentication removed",
|
||||
"body" => $body,
|
||||
"gpgFingerprint" => $gpg?->getFingerprint()
|
||||
]);
|
||||
}
|
||||
|
||||
$this->lastError = $req->getLastError();
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
// TOTP
|
||||
class GenerateQR extends TfaAPI {
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall);
|
||||
$this->csrfTokenRequired = false;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
|
||||
$currentUser = $this->context->getUser();
|
||||
$twoFactorToken = $currentUser->getTwoFactorToken();
|
||||
if ($twoFactorToken && $twoFactorToken->isConfirmed()) {
|
||||
return $this->createError("You already added a two factor token");
|
||||
} else if (!($twoFactorToken instanceof TimeBasedTwoFactorToken)) {
|
||||
$twoFactorToken = new TimeBasedTwoFactorToken(generateRandomString(32, "base32"));
|
||||
$sql = $this->context->getSQL();
|
||||
$this->success = $sql->insert("2FA", ["type", "data"])
|
||||
->addRow("totp", $twoFactorToken->getData())
|
||||
->returning("id")
|
||||
->execute() !== false;
|
||||
$this->lastError = $sql->getLastError();
|
||||
if ($this->success) {
|
||||
$this->success = $sql->update("User")
|
||||
->set("2fa_id", $sql->getLastInsertId())->where(new Compare("id", $currentUser->getId()))
|
||||
->execute() !== false;
|
||||
$this->lastError = $sql->getLastError();
|
||||
}
|
||||
|
||||
if (!$this->success) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
header("Content-Type: image/png");
|
||||
$this->disableCache();
|
||||
die($twoFactorToken->generateQRCode($this->context));
|
||||
}
|
||||
}
|
||||
|
||||
class ConfirmTotp extends VerifyTotp {
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall);
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
|
||||
$currentUser = $this->context->getUser();
|
||||
$twoFactorToken = $currentUser->getTwoFactorToken();
|
||||
if ($twoFactorToken->isConfirmed()) {
|
||||
return $this->createError("Your two factor token is already confirmed.");
|
||||
}
|
||||
|
||||
$sql = $this->context->getSQL();
|
||||
$this->success = $sql->update("2FA")
|
||||
->set("confirmed", true)
|
||||
->where(new Compare("id", $twoFactorToken->getId()))
|
||||
->execute() !== false;
|
||||
$this->lastError = $sql->getLastError();
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
class VerifyTotp extends TfaAPI {
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, [
|
||||
"code" => new StringType("code", 6)
|
||||
]);
|
||||
$this->loginRequired = true;
|
||||
$this->csrfTokenRequired = false;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
|
||||
$currentUser = $this->context->getUser();
|
||||
if (!$currentUser) {
|
||||
return $this->createError("You are not logged in.");
|
||||
}
|
||||
|
||||
$twoFactorToken = $currentUser->getTwoFactorToken();
|
||||
if (!$twoFactorToken) {
|
||||
return $this->createError("You did not add a two factor token yet.");
|
||||
} else if (!($twoFactorToken instanceof TimeBasedTwoFactorToken)) {
|
||||
return $this->createError("Invalid 2FA-token endpoint");
|
||||
}
|
||||
|
||||
$code = $this->getParam("code");
|
||||
if (!$twoFactorToken->verify($code)) {
|
||||
return $this->createError("Code does not match");
|
||||
}
|
||||
|
||||
$twoFactorToken->authenticate();
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
// Key
|
||||
class RegisterKey extends TfaAPI {
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, [
|
||||
"clientDataJSON" => new StringType("clientDataJSON", 0, true, "{}"),
|
||||
"attestationObject" => new StringType("attestationObject", 0, true, "")
|
||||
]);
|
||||
$this->loginRequired = true;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
|
||||
$currentUser = $this->context->getUser();
|
||||
$clientDataJSON = json_decode($this->getParam("clientDataJSON"), true);
|
||||
$attestationObjectRaw = base64_decode($this->getParam("attestationObject"));
|
||||
$twoFactorToken = $currentUser->getTwoFactorToken();
|
||||
$settings = $this->context->getSettings();
|
||||
$relyingParty = $settings->getSiteName();
|
||||
$sql = $this->context->getSQL();
|
||||
|
||||
// TODO: for react development, localhost / HTTP_HOST is required, otherwise a DOMException is thrown
|
||||
$domain = parse_url($settings->getBaseUrl(), PHP_URL_HOST);
|
||||
// $domain = "localhost";
|
||||
|
||||
if (!$clientDataJSON || !$attestationObjectRaw) {
|
||||
if ($twoFactorToken) {
|
||||
if (!($twoFactorToken instanceof KeyBasedTwoFactorToken) || $twoFactorToken->isConfirmed()) {
|
||||
return $this->createError("You already added a two factor token");
|
||||
} else {
|
||||
$challenge = base64_encode($twoFactorToken->getData());
|
||||
}
|
||||
} else {
|
||||
$challenge = base64_encode(generateRandomString(32, "raw"));
|
||||
$res = $sql->insert("2FA", ["type", "data"])
|
||||
->addRow("fido", $challenge)
|
||||
->returning("id")
|
||||
->execute();
|
||||
$this->success = ($res !== false);
|
||||
$this->lastError = $sql->getLastError();
|
||||
if (!$this->success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->success = $sql->update("User")
|
||||
->set("2fa_id", $sql->getLastInsertId())
|
||||
->where(new Compare("id", $currentUser->getId()))
|
||||
->execute() !== false;
|
||||
$this->lastError = $sql->getLastError();
|
||||
if (!$this->success) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$this->result["data"] = [
|
||||
"challenge" => $challenge,
|
||||
"id" => $currentUser->getId() . "@" . $domain, // <userId>@<domain>
|
||||
"relyingParty" => [
|
||||
"name" => $relyingParty,
|
||||
"id" => $domain
|
||||
],
|
||||
];
|
||||
} else {
|
||||
if ($twoFactorToken === null) {
|
||||
return $this->createError("Request a registration first.");
|
||||
} else if (!($twoFactorToken instanceof KeyBasedTwoFactorToken)) {
|
||||
return $this->createError("You already got a 2FA token");
|
||||
}
|
||||
|
||||
if (!$this->verifyClientDataJSON($clientDataJSON, $twoFactorToken)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$attestationObject = new AttestationObject($attestationObjectRaw);
|
||||
$authData = $attestationObject->getAuthData();
|
||||
if (!$this->verifyAuthData($authData)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$publicKey = $authData->getPublicKey();
|
||||
if ($publicKey->getUsedAlgorithm() !== -7) {
|
||||
return $this->createError("Unsupported key type. Expected: -7");
|
||||
}
|
||||
|
||||
$data = [
|
||||
"credentialID" => base64_encode($authData->getCredentialID()),
|
||||
"publicKey" => $publicKey->jsonSerialize()
|
||||
];
|
||||
|
||||
$this->success = $sql->update("2FA")
|
||||
->set("data", json_encode($data))
|
||||
->set("confirmed", true)
|
||||
->where(new Compare("id", $twoFactorToken->getId()))
|
||||
->execute() !== false;
|
||||
$this->lastError = $sql->getLastError();
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
class VerifyKey extends TfaAPI {
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, [
|
||||
"credentialID" => new StringType("credentialID"),
|
||||
"clientDataJSON" => new StringType("clientDataJSON"),
|
||||
"authData" => new StringType("authData"),
|
||||
"signature" => new StringType("signature"),
|
||||
]);
|
||||
$this->loginRequired = true;
|
||||
$this->csrfTokenRequired = false;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
|
||||
$currentUser = $this->context->getUser();
|
||||
if (!$currentUser) {
|
||||
return $this->createError("You are not logged in.");
|
||||
}
|
||||
|
||||
$twoFactorToken = $currentUser->getTwoFactorToken();
|
||||
if (!$twoFactorToken) {
|
||||
return $this->createError("You did not add a two factor token yet.");
|
||||
} else if (!($twoFactorToken instanceof KeyBasedTwoFactorToken)) {
|
||||
return $this->createError("Invalid 2FA-token endpoint");
|
||||
} else if (!$twoFactorToken->isConfirmed()) {
|
||||
return $this->createError("2FA-Key not confirmed yet");
|
||||
}
|
||||
|
||||
$credentialID = base64url_decode($this->getParam("credentialID"));
|
||||
if ($credentialID !== $twoFactorToken->getCredentialId()) {
|
||||
return $this->createError("credential ID does not match");
|
||||
}
|
||||
|
||||
$jsonData = $this->getParam("clientDataJSON");
|
||||
if (!$this->verifyClientDataJSON(json_decode($jsonData, true), $twoFactorToken)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$authDataRaw = base64_decode($this->getParam("authData"));
|
||||
$authData = new AuthenticationData($authDataRaw);
|
||||
if (!$this->verifyAuthData($authData)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$clientDataHash = hash("sha256", $jsonData, true);
|
||||
$signature = base64_decode($this->getParam("signature"));
|
||||
|
||||
$this->success = $twoFactorToken->verify($signature, $authDataRaw . $clientDataHash);
|
||||
if ($this->success) {
|
||||
$twoFactorToken->authenticate();
|
||||
} else {
|
||||
$this->lastError = "Verification failed";
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
}
|
||||
1866
Core/API/UserAPI.class.php
Normal file
1866
Core/API/UserAPI.class.php
Normal file
File diff suppressed because it is too large
Load Diff
62
Core/API/VerifyCaptcha.class.php
Normal file
62
Core/API/VerifyCaptcha.class.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Core\API;
|
||||
|
||||
use Core\API\Parameter\StringType;
|
||||
use Core\Objects\Context;
|
||||
|
||||
class VerifyCaptcha extends Request {
|
||||
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array(
|
||||
"captcha" => new StringType("captcha"),
|
||||
"action" => new StringType("action"),
|
||||
));
|
||||
|
||||
$this->isPublic = false;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
$settings = $this->context->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;
|
||||
}
|
||||
}
|
||||
115
Core/API/VisitorsAPI.class.php
Normal file
115
Core/API/VisitorsAPI.class.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace Core\API {
|
||||
|
||||
use Core\Objects\Context;
|
||||
|
||||
abstract class VisitorsAPI extends Request {
|
||||
public function __construct(Context $context, bool $externalCall = false, array $params = []) {
|
||||
parent::__construct($context, $externalCall, $params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace Core\API\Visitors {
|
||||
|
||||
use Core\API\Parameter\Parameter;
|
||||
use Core\API\Parameter\StringType;
|
||||
use Core\API\VisitorsAPI;
|
||||
use DateTime;
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Driver\SQL\Expression\Add;
|
||||
use Core\Driver\SQL\Query\Select;
|
||||
use Core\Driver\SQL\Strategy\UpdateStrategy;
|
||||
use Core\Objects\Context;
|
||||
|
||||
class ProcessVisit extends VisitorsAPI {
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array(
|
||||
"cookie" => new StringType("cookie")
|
||||
));
|
||||
$this->isPublic = false;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
$sql = $this->context->getSQL();
|
||||
$cookie = $this->getParam("cookie");
|
||||
$day = (new DateTime())->format("Ymd");
|
||||
$sql->insert("Visitor", array("cookie", "day"))
|
||||
->addRow($cookie, $day)
|
||||
->onDuplicateKeyStrategy(new UpdateStrategy(
|
||||
array("day", "cookie"),
|
||||
array("count" => new Add("Visitor.count", 1))))
|
||||
->execute();
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
class Stats extends VisitorsAPI {
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array(
|
||||
'type' => new StringType('type', 32),
|
||||
'date' => new Parameter('date', Parameter::TYPE_DATE, true, new DateTime())
|
||||
));
|
||||
}
|
||||
|
||||
private function setConditions(string $type, DateTime $date, Select $query): bool {
|
||||
if ($type === "yearly") {
|
||||
$yearStart = $date->format("Y0000");
|
||||
$yearEnd = $date->modify("+1 year")->format("Y0000");
|
||||
$query->where(new Compare("day", $yearStart, ">="));
|
||||
$query->where(new Compare("day", $yearEnd, "<"));
|
||||
} else if($type === "monthly") {
|
||||
$monthStart = $date->format("Ym00");
|
||||
$monthEnd = $date->modify("+1 month")->format("Ym00");
|
||||
$query->where(new Compare("day", $monthStart, ">="));
|
||||
$query->where(new Compare("day", $monthEnd, "<"));
|
||||
} else if($type === "weekly") {
|
||||
$weekStart = ($date->modify("monday this week"))->format("Ymd");
|
||||
$weekEnd = ($date->modify("sunday this week"))->format("Ymd");
|
||||
$query->where(new Compare("day", $weekStart, ">="));
|
||||
$query->where(new Compare("day", $weekEnd, "<="));
|
||||
} else {
|
||||
return $this->createError("Invalid scope: $type");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
$date = $this->getParam("date");
|
||||
$type = $this->getParam("type");
|
||||
|
||||
$sql = $this->context->getSQL();
|
||||
$query = $sql->select($sql->count(), "day")
|
||||
->from("Visitor")
|
||||
->where(new Compare("count", 1, ">"))
|
||||
->groupBy("day")
|
||||
->orderBy("day")
|
||||
->ascending();
|
||||
|
||||
$this->success = $this->setConditions($type, $date, $query);
|
||||
if (!$this->success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$res = $query->execute();
|
||||
$this->success = ($res !== FALSE);
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
if ($this->success) {
|
||||
$this->result["type"] = $type;
|
||||
$this->result["visitors"] = array();
|
||||
|
||||
foreach($res as $row) {
|
||||
$day = DateTime::createFromFormat("Ymd", $row["day"])->format("Y/m/d");
|
||||
$count = $row["count"];
|
||||
$this->result["visitors"][$day] = $count;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Core/Cache/.gitignore
vendored
Normal file
3
Core/Cache/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*
|
||||
!.gitkeep
|
||||
!.gitignore
|
||||
0
Core/Cache/.gitkeep
Normal file
0
Core/Cache/.gitkeep
Normal file
104
Core/Configuration/Configuration.class.php
Normal file
104
Core/Configuration/Configuration.class.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Configuration;
|
||||
|
||||
use Core\Objects\ConnectionData;
|
||||
|
||||
class Configuration {
|
||||
|
||||
const className = "\Site\Configuration\Database";
|
||||
private ?ConnectionData $database;
|
||||
private Settings $settings;
|
||||
|
||||
function __construct() {
|
||||
$this->database = null;
|
||||
$this->settings = Settings::loadDefaults();
|
||||
|
||||
$className = self::className;
|
||||
$path = getClassPath($className, ".class");
|
||||
if (file_exists($path) && is_readable($path)) {
|
||||
include_once $path;
|
||||
if (class_exists($className)) {
|
||||
$this->database = new $className();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getDatabase(): ?ConnectionData {
|
||||
return $this->database;
|
||||
}
|
||||
|
||||
public function getSettings(): Settings {
|
||||
return $this->settings;
|
||||
}
|
||||
|
||||
public static function create(string $className, $data) {
|
||||
$path = getClassPath($className);
|
||||
$classNameShort = explode("\\", $className);
|
||||
$classNameShort = end($classNameShort);
|
||||
|
||||
if ($data) {
|
||||
if (is_string($data)) {
|
||||
$key = var_export($data, true);
|
||||
$code = intendCode(
|
||||
"<?php
|
||||
|
||||
namespace Core\Configuration;
|
||||
|
||||
class $classNameShort extends KeyData {
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct($key);
|
||||
}
|
||||
|
||||
}", false
|
||||
);
|
||||
} else if ($data instanceof ConnectionData) {
|
||||
$superClass = get_class($data);
|
||||
$host = var_export($data->getHost(), true);
|
||||
$port = var_export($data->getPort(), true);
|
||||
$login = var_export($data->getLogin(), true);
|
||||
$password = var_export($data->getPassword(), true);
|
||||
|
||||
$properties = "";
|
||||
foreach ($data->getProperties() as $key => $val) {
|
||||
$key = var_export($key, true);
|
||||
$val = var_export($val, true);
|
||||
$properties .= "\n\$this->setProperty($key, $val);";
|
||||
}
|
||||
|
||||
$code = intendCode(
|
||||
"<?php
|
||||
|
||||
namespace Site\Configuration;
|
||||
|
||||
class $classNameShort extends \\$superClass {
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct($host, $port, $login, $password);$properties
|
||||
}
|
||||
}", false
|
||||
);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
$code = "<?php";
|
||||
}
|
||||
|
||||
return @file_put_contents($path, $code);
|
||||
}
|
||||
|
||||
public function delete(string $className): bool {
|
||||
$path = getClassPath("\\Configuration\\$className");
|
||||
if (file_exists($path)) {
|
||||
return unlink($path);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function setDatabase(ConnectionData $connectionData): void {
|
||||
$this->database = $connectionData;
|
||||
}
|
||||
}
|
||||
256
Core/Configuration/CreateDatabase.class.php
Normal file
256
Core/Configuration/CreateDatabase.class.php
Normal file
@@ -0,0 +1,256 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Configuration;
|
||||
|
||||
use Core\Driver\SQL\SQL;
|
||||
use Core\Driver\SQL\Strategy\SetNullStrategy;
|
||||
use Core\Driver\SQL\Strategy\CascadeStrategy;
|
||||
use Core\Objects\DatabaseEntity\DatabaseEntity;
|
||||
use PHPUnit\Util\Exception;
|
||||
|
||||
class CreateDatabase extends DatabaseScript {
|
||||
|
||||
public static function createQueries(SQL $sql): array {
|
||||
$queries = array();
|
||||
|
||||
self::loadEntities($queries, $sql);
|
||||
|
||||
$queries[] = $sql->insert("Language", array("code", "name"))
|
||||
->addRow("en_US", 'American English')
|
||||
->addRow("de_DE", 'Deutsch Standard');
|
||||
|
||||
$queries[] = $sql->createTable("UserToken")
|
||||
->addInt("user_id")
|
||||
->addString("token", 36)
|
||||
->addEnum("token_type", array("password_reset", "email_confirm", "invite", "gpg_confirm"))
|
||||
->addDateTime("valid_until")
|
||||
->addBool("used", false)
|
||||
->foreignKey("user_id", "User", "id", new CascadeStrategy());
|
||||
|
||||
$queries[] = $sql->insert("Group", array("name", "color"))
|
||||
->addRow(USER_GROUP_MODERATOR_NAME, "#007bff")
|
||||
->addRow(USER_GROUP_SUPPORT_NAME, "#28a745")
|
||||
->addRow(USER_GROUP_ADMIN_NAME, "#dc3545");
|
||||
|
||||
$queries[] = $sql->createTable("UserGroup")
|
||||
->addInt("user_id")
|
||||
->addInt("group_id")
|
||||
->unique("user_id", "group_id")
|
||||
->foreignKey("user_id", "User", "id", new CascadeStrategy())
|
||||
->foreignKey("group_id", "Group", "id", new CascadeStrategy());
|
||||
|
||||
$queries[] = $sql->createTable("UserNotification")
|
||||
->addInt("user_id")
|
||||
->addInt("notification_id")
|
||||
->addBool("seen", false)
|
||||
->foreignKey("user_id", "User", "id")
|
||||
->foreignKey("notification_id", "Notification", "id")
|
||||
->unique("user_id", "notification_id");
|
||||
|
||||
$queries[] = $sql->createTable("GroupNotification")
|
||||
->addInt("group_id")
|
||||
->addInt("notification_id")
|
||||
->addBool("seen", false)
|
||||
->foreignKey("group_id", "Group", "id")
|
||||
->foreignKey("notification_id", "Notification", "id")
|
||||
->unique("group_id", "notification_id");
|
||||
|
||||
$queries[] = $sql->createTable("Visitor")
|
||||
->addInt("day")
|
||||
->addInt("count", false, 1)
|
||||
->addString("cookie", 26)
|
||||
->unique("day", "cookie");
|
||||
|
||||
$queries[] = $sql->createTable("Route")
|
||||
->addSerial("id")
|
||||
->addString("request", 128)
|
||||
->addEnum("action", array("redirect_temporary", "redirect_permanently", "static", "dynamic"))
|
||||
->addString("target", 128)
|
||||
->addString("extra", 64, true)
|
||||
->addBool("active", true)
|
||||
->addBool("exact", true)
|
||||
->primaryKey("id")
|
||||
->unique("request");
|
||||
|
||||
$queries[] = $sql->insert("Route", ["request", "action", "target", "extra", "exact"])
|
||||
->addRow("/admin", "dynamic", "\\Core\\Documents\\Admin", NULL, false)
|
||||
->addRow("/register", "dynamic", "\\Core\\Documents\\Account", json_encode(["account/register.twig"]), true)
|
||||
->addRow("/confirmEmail", "dynamic", "\\Core\\Documents\\Account", json_encode(["account/confirm_email.twig"]), true)
|
||||
->addRow("/acceptInvite", "dynamic", "\\Core\\Documents\\Account", json_encode(["account/accept_invite.twig"]), true)
|
||||
->addRow("/resetPassword", "dynamic", "\\Core\\Documents\\Account", json_encode(["account/reset_password.twig"]), true)
|
||||
->addRow("/login", "dynamic", "\\Core\\Documents\\Account", json_encode(["account/login.twig"]), true)
|
||||
->addRow("/resendConfirmEmail", "dynamic", "\\Core\\Documents\\Account", json_encode(["account/resend_confirm_email.twig"]), true)
|
||||
->addRow("/debug", "dynamic", "\\Core\\Documents\\Info", NULL, true)
|
||||
->addRow("/", "static", "/static/welcome.html", NULL, true);
|
||||
|
||||
$queries[] = $sql->createTable("Settings")
|
||||
->addString("name", 32)
|
||||
->addString("value", 1024, true)
|
||||
->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"))
|
||||
// ->addRow("mail_enabled", "0") # this key will be set during installation
|
||||
->addRow("mail_host", "", false, false)
|
||||
->addRow("mail_port", "", false, false)
|
||||
->addRow("mail_username", "", false, false)
|
||||
->addRow("mail_password", "", true, false)
|
||||
->addRow("mail_from", "", false, false)
|
||||
->addRow("mail_last_sync", "", false, false)
|
||||
->addRow("mail_footer", "", false, false);
|
||||
|
||||
(Settings::loadDefaults())->addRows($settingsQuery);
|
||||
$queries[] = $settingsQuery;
|
||||
|
||||
$queries[] = $sql->createTable("ContactRequest")
|
||||
->addSerial("id")
|
||||
->addString("from_name", 32)
|
||||
->addString("from_email", 64)
|
||||
->addString("message", 512)
|
||||
->addString("messageId", 78, true) # null = don't sync with mails (usually if mail could not be sent)
|
||||
->addDateTime("created_at", false, $sql->currentTimestamp())
|
||||
->unique("messageId")
|
||||
->primaryKey("id");
|
||||
|
||||
$queries[] = $sql->createTable("ContactMessage")
|
||||
->addSerial("id")
|
||||
->addInt("request_id")
|
||||
->addInt("user_id", true) # null = customer has sent this message
|
||||
->addString("message", 512)
|
||||
->addString("messageId", 78)
|
||||
->addDateTime("created_at", false, $sql->currentTimestamp())
|
||||
->addBool("read", false)
|
||||
->unique("messageId")
|
||||
->primaryKey("id")
|
||||
->foreignKey("request_id", "ContactRequest", "id", new CascadeStrategy())
|
||||
->foreignKey("user_id", "User", "id", new SetNullStrategy());
|
||||
|
||||
$queries[] = $sql->createTable("ApiPermission")
|
||||
->addString("method", 32)
|
||||
->addJson("groups", true, '[]')
|
||||
->addString("description", 128, false, "")
|
||||
->primaryKey("method");
|
||||
|
||||
$queries[] = $sql->createTable("MailQueue")
|
||||
->addSerial("id")
|
||||
->addString("from", 64)
|
||||
->addString("to", 64)
|
||||
->addString("subject")
|
||||
->addString("body")
|
||||
->addString("replyTo", 64, true)
|
||||
->addString("replyName", 32, true)
|
||||
->addString("gpgFingerprint", 64, true)
|
||||
->addEnum("status", ["waiting", "success", "error"], false, 'waiting')
|
||||
->addInt("retryCount", false, 5)
|
||||
->addDateTime("nextTry", false, $sql->now())
|
||||
->addString("errorMessage", NULL, true)
|
||||
->primaryKey("id");
|
||||
|
||||
$queries = array_merge($queries, \Core\Configuration\Patch\EntityLog_2021_04_08::createTableLog($sql, "MailQueue", 30));
|
||||
|
||||
$queries[] = $sql->insert("ApiPermission", array("method", "groups", "description"))
|
||||
->addRow("ApiKey/create", array(), "Allows users to create API-Keys for themselves")
|
||||
->addRow("ApiKey/fetch", array(), "Allows users to list their API-Keys")
|
||||
->addRow("ApiKey/refresh", array(), "Allows users to refresh their API-Keys")
|
||||
->addRow("ApiKey/revoke", array(), "Allows users to revoke their API-Keys")
|
||||
->addRow("Groups/fetch", array(USER_GROUP_SUPPORT, USER_GROUP_ADMIN), "Allows users to list all available groups")
|
||||
->addRow("Groups/create", array(USER_GROUP_ADMIN), "Allows users to create a new groups")
|
||||
->addRow("Groups/delete", array(USER_GROUP_ADMIN), "Allows users to delete a group")
|
||||
->addRow("Routes/fetch", array(USER_GROUP_ADMIN), "Allows users to list all configured routes")
|
||||
->addRow("Routes/save", array(USER_GROUP_ADMIN), "Allows users to create, delete and modify routes")
|
||||
->addRow("Mail/test", array(USER_GROUP_SUPPORT, USER_GROUP_ADMIN), "Allows users to send a test email to a given address")
|
||||
->addRow("Mail/Sync", array(USER_GROUP_SUPPORT, USER_GROUP_ADMIN), "Allows users to synchronize mails with the database")
|
||||
->addRow("Settings/get", array(USER_GROUP_ADMIN), "Allows users to fetch server settings")
|
||||
->addRow("Settings/set", array(USER_GROUP_ADMIN), "Allows users create, delete or modify server settings")
|
||||
->addRow("Stats", array(USER_GROUP_ADMIN, USER_GROUP_SUPPORT), "Allows users to fetch server stats")
|
||||
->addRow("User/create", array(USER_GROUP_ADMIN), "Allows users to create a new user, email address does not need to be confirmed")
|
||||
->addRow("User/fetch", array(USER_GROUP_ADMIN, USER_GROUP_SUPPORT), "Allows users to list all registered users")
|
||||
->addRow("User/get", array(USER_GROUP_ADMIN, USER_GROUP_SUPPORT), "Allows users to get information about a single user")
|
||||
->addRow("User/invite", array(USER_GROUP_ADMIN), "Allows users to create a new user and send them an invitation link")
|
||||
->addRow("User/edit", array(USER_GROUP_ADMIN), "Allows users to edit details and group memberships of any user")
|
||||
->addRow("User/delete", array(USER_GROUP_ADMIN), "Allows users to delete any other user")
|
||||
->addRow("Permission/fetch", array(USER_GROUP_ADMIN), "Allows users to list all API permissions")
|
||||
->addRow("Visitors/stats", array(USER_GROUP_ADMIN, USER_GROUP_SUPPORT), "Allows users to see visitor statistics")
|
||||
->addRow("Contact/respond", array(USER_GROUP_ADMIN, USER_GROUP_SUPPORT), "Allows users to respond to contact requests")
|
||||
->addRow("Contact/fetch", array(USER_GROUP_ADMIN, USER_GROUP_SUPPORT), "Allows users to fetch all contact requests")
|
||||
->addRow("Contact/get", array(USER_GROUP_ADMIN, USER_GROUP_SUPPORT), "Allows users to see messages within a contact request");
|
||||
|
||||
self::loadPatches($queries, $sql);
|
||||
|
||||
return $queries;
|
||||
}
|
||||
|
||||
private static function loadPatches(&$queries, $sql) {
|
||||
$baseDirs = ["Core", "Site"];
|
||||
foreach ($baseDirs as $baseDir) {
|
||||
$patchDirectory = "./$baseDir/Configuration/Patch/";
|
||||
if (file_exists($patchDirectory) && is_dir($patchDirectory)) {
|
||||
$scan_arr = scandir($patchDirectory);
|
||||
$files_arr = array_diff($scan_arr, array('.', '..'));
|
||||
foreach ($files_arr as $file) {
|
||||
$suffix = ".class.php";
|
||||
if (endsWith($file, $suffix)) {
|
||||
$className = substr($file, 0, strlen($file) - strlen($suffix));
|
||||
$className = "\\$baseDir\\Configuration\\Patch\\$className";
|
||||
$method = "$className::createQueries";
|
||||
$patchQueries = call_user_func($method, $sql);
|
||||
foreach ($patchQueries as $query) $queries[] = $query;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function loadEntities(&$queries, $sql) {
|
||||
$handlers = [];
|
||||
$baseDirs = ["Core", "Site"];
|
||||
foreach ($baseDirs as $baseDir) {
|
||||
$entityDirectory = "./$baseDir/Objects/DatabaseEntity/";
|
||||
if (file_exists($entityDirectory) && is_dir($entityDirectory)) {
|
||||
$scan_arr = scandir($entityDirectory);
|
||||
$files_arr = array_diff($scan_arr, array('.', '..'));
|
||||
foreach ($files_arr as $file) {
|
||||
$suffix = ".class.php";
|
||||
if (endsWith($file, $suffix)) {
|
||||
$className = substr($file, 0, strlen($file) - strlen($suffix));
|
||||
if (!in_array($className, ["DatabaseEntity", "DatabaseEntityQuery", "DatabaseEntityHandler"])) {
|
||||
$className = "\\$baseDir\\Objects\\DatabaseEntity\\$className";
|
||||
$reflectionClass = new \ReflectionClass($className);
|
||||
if ($reflectionClass->isSubclassOf(DatabaseEntity::class)) {
|
||||
$method = "$className::getHandler";
|
||||
$handler = call_user_func($method, $sql);
|
||||
$handlers[$handler->getTableName()] = $handler;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$tableCount = count($handlers);
|
||||
$createdTables = [];
|
||||
while (!empty($handlers)) {
|
||||
$prevCount = $tableCount;
|
||||
$unmetDependenciesTotal = [];
|
||||
|
||||
foreach ($handlers as $tableName => $handler) {
|
||||
$dependsOn = $handler->dependsOn();
|
||||
$unmetDependencies = array_diff($dependsOn, $createdTables);
|
||||
if (empty($unmetDependencies)) {
|
||||
$queries[] = $handler->getTableQuery();
|
||||
$createdTables[] = $tableName;
|
||||
unset($handlers[$tableName]);
|
||||
} else {
|
||||
$unmetDependenciesTotal = array_merge($unmetDependenciesTotal, $unmetDependencies);
|
||||
}
|
||||
}
|
||||
|
||||
$tableCount = count($handlers);
|
||||
if ($tableCount === $prevCount) {
|
||||
throw new Exception("Circular or unmet table dependency detected. Unmet dependencies: "
|
||||
. implode(", ", $unmetDependenciesTotal));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
Core/Configuration/DatabaseScript.class.php
Normal file
9
Core/Configuration/DatabaseScript.class.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Configuration;
|
||||
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
abstract class DatabaseScript {
|
||||
public static abstract function createQueries(SQL $sql);
|
||||
}
|
||||
95
Core/Configuration/Patch/EntityLog_2021_04_08.class.php
Normal file
95
Core/Configuration/Patch/EntityLog_2021_04_08.class.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Configuration\Patch;
|
||||
|
||||
use Core\Configuration\DatabaseScript;
|
||||
use Core\Driver\SQL\Column\IntColumn;
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Driver\SQL\Query\CreateProcedure;
|
||||
use Core\Driver\SQL\SQL;
|
||||
use Core\Driver\SQL\Type\CurrentColumn;
|
||||
use Core\Driver\SQL\Type\CurrentTable;
|
||||
use Core\Driver\SQL\Type\Trigger;
|
||||
|
||||
class EntityLog_2021_04_08 extends DatabaseScript {
|
||||
|
||||
public static function createTableLog(SQL $sql, string $table, int $lifetime = 90): array {
|
||||
return [
|
||||
$sql->createTrigger("${table}_trg_insert")
|
||||
->after()->insert($table)
|
||||
->exec(new CreateProcedure($sql, "InsertEntityLog"), [
|
||||
"tableName" => new CurrentTable(),
|
||||
"entityId" => new CurrentColumn("id"),
|
||||
"lifetime" => $lifetime,
|
||||
]),
|
||||
|
||||
$sql->createTrigger("${table}_trg_update")
|
||||
->after()->update($table)
|
||||
->exec(new CreateProcedure($sql, "UpdateEntityLog"), [
|
||||
"tableName" => new CurrentTable(),
|
||||
"entityId" => new CurrentColumn("id"),
|
||||
]),
|
||||
|
||||
$sql->createTrigger("${table}_trg_delete")
|
||||
->after()->delete($table)
|
||||
->exec(new CreateProcedure($sql, "DeleteEntityLog"), [
|
||||
"tableName" => new CurrentTable(),
|
||||
"entityId" => new CurrentColumn("id"),
|
||||
])
|
||||
];
|
||||
}
|
||||
|
||||
public static function createQueries(SQL $sql): array {
|
||||
|
||||
$queries = array();
|
||||
|
||||
$queries[] = $sql->createTable("EntityLog")
|
||||
->addInt("entityId")
|
||||
->addString("tableName")
|
||||
->addDateTime("modified", false, $sql->now())
|
||||
->addInt("lifetime", false, 90);
|
||||
|
||||
$insertProcedure = $sql->createProcedure("InsertEntityLog")
|
||||
->param(new CurrentTable())
|
||||
->param(new IntColumn("id"))
|
||||
->param(new IntColumn("lifetime", false, 90))
|
||||
->returns(new Trigger())
|
||||
->exec(array(
|
||||
$sql->insert("EntityLog", ["entityId", "tableName", "lifetime"])
|
||||
->addRow(new CurrentColumn("id"), new CurrentTable(), new CurrentColumn("lifetime"))
|
||||
));
|
||||
|
||||
$updateProcedure = $sql->createProcedure("UpdateEntityLog")
|
||||
->param(new CurrentTable())
|
||||
->param(new IntColumn("id"))
|
||||
->returns(new Trigger())
|
||||
->exec(array(
|
||||
$sql->update("EntityLog")
|
||||
->set("modified", $sql->now())
|
||||
->where(new Compare("entityId", new CurrentColumn("id")))
|
||||
->where(new Compare("tableName", new CurrentTable()))
|
||||
));
|
||||
|
||||
$deleteProcedure = $sql->createProcedure("DeleteEntityLog")
|
||||
->param(new CurrentTable())
|
||||
->param(new IntColumn("id"))
|
||||
->returns(new Trigger())
|
||||
->exec(array(
|
||||
$sql->delete("EntityLog")
|
||||
->where(new Compare("entityId", new CurrentColumn("id")))
|
||||
->where(new Compare("tableName", new CurrentTable()))
|
||||
));
|
||||
|
||||
$queries[] = $insertProcedure;
|
||||
$queries[] = $updateProcedure;
|
||||
$queries[] = $deleteProcedure;
|
||||
|
||||
$tables = ["ContactRequest"];
|
||||
foreach ($tables as $table) {
|
||||
$queries = array_merge($queries, self::createTableLog($sql, $table));
|
||||
}
|
||||
|
||||
return $queries;
|
||||
}
|
||||
|
||||
}
|
||||
16
Core/Configuration/Patch/SystemLog_2022_03_30.class.php
Normal file
16
Core/Configuration/Patch/SystemLog_2022_03_30.class.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Configuration\Patch;
|
||||
|
||||
use Core\Configuration\DatabaseScript;
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
class SystemLog_2022_03_30 extends DatabaseScript {
|
||||
|
||||
public static function createQueries(SQL $sql): array {
|
||||
return [
|
||||
$sql->insert("ApiPermission", ["method", "groups", "description"])
|
||||
->addRow("Logs/get", [USER_GROUP_ADMIN], "Allows users to fetch system logs")
|
||||
];
|
||||
}
|
||||
}
|
||||
221
Core/Configuration/Settings.class.php
Normal file
221
Core/Configuration/Settings.class.php
Normal file
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Do not change settings here, they are dynamically loaded from database.
|
||||
*/
|
||||
|
||||
namespace Core\Configuration;
|
||||
|
||||
use Core\Driver\Logger\Logger;
|
||||
use Core\Driver\SQL\Query\Insert;
|
||||
use Core\Objects\Context;
|
||||
|
||||
class Settings {
|
||||
|
||||
//
|
||||
private bool $installationComplete;
|
||||
|
||||
// settings
|
||||
private string $siteName;
|
||||
private string $baseUrl;
|
||||
private ?string $jwtPublicKey;
|
||||
private ?string $jwtSecretKey;
|
||||
private string $jwtAlgorithm;
|
||||
private bool $registrationAllowed;
|
||||
private bool $recaptchaEnabled;
|
||||
private bool $mailEnabled;
|
||||
private string $recaptchaPublicKey;
|
||||
private string $recaptchaPrivateKey;
|
||||
private string $mailSender;
|
||||
private string $mailFooter;
|
||||
private array $allowedExtensions;
|
||||
|
||||
//
|
||||
private Logger $logger;
|
||||
|
||||
public function __construct() {
|
||||
$this->logger = new Logger("Settings");
|
||||
}
|
||||
|
||||
public function getJwtPublicKey(bool $allowPrivate = true): ?\Firebase\JWT\Key {
|
||||
if (empty($this->jwtPublicKey)) {
|
||||
// we might have a symmetric key, should we instead return the private key?
|
||||
return $allowPrivate ? new \Firebase\JWT\Key($this->jwtSecretKey, $this->jwtAlgorithm) : null;
|
||||
} else {
|
||||
return new \Firebase\JWT\Key($this->jwtPublicKey, $this->jwtAlgorithm);
|
||||
}
|
||||
}
|
||||
|
||||
public function getJwtSecretKey(): \Firebase\JWT\Key {
|
||||
return new \Firebase\JWT\Key($this->jwtSecretKey, $this->jwtAlgorithm);
|
||||
}
|
||||
|
||||
public function isInstalled(): bool {
|
||||
return $this->installationComplete;
|
||||
}
|
||||
|
||||
public static function loadDefaults(): Settings {
|
||||
$hostname = $_SERVER["SERVER_NAME"] ?? "localhost";
|
||||
$protocol = getProtocol();
|
||||
$settings = new Settings();
|
||||
|
||||
// General
|
||||
$settings->siteName = "WebBase";
|
||||
$settings->baseUrl = "$protocol://$hostname";
|
||||
$settings->allowedExtensions = ['png', 'jpg', 'jpeg', 'gif', 'htm', 'html'];
|
||||
$settings->installationComplete = false;
|
||||
$settings->registrationAllowed = false;
|
||||
|
||||
// JWT
|
||||
$settings->jwtSecretKey = null;
|
||||
$settings->jwtPublicKey = null;
|
||||
$settings->jwtAlgorithm = "HS256";
|
||||
|
||||
// Recaptcha
|
||||
$settings->recaptchaEnabled = false;
|
||||
$settings->recaptchaPublicKey = "";
|
||||
$settings->recaptchaPrivateKey = "";
|
||||
|
||||
// Mail
|
||||
$settings->mailEnabled = false;
|
||||
$settings->mailSender = "webmaster@localhost";
|
||||
$settings->mailFooter = "";
|
||||
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
public function generateJwtKey(string $algorithm = null): bool {
|
||||
$this->jwtAlgorithm = $algorithm ?? $this->jwtAlgorithm;
|
||||
|
||||
// TODO: key encryption necessary?
|
||||
if (in_array($this->jwtAlgorithm, ["HS256", "HS384", "HS512"])) {
|
||||
$this->jwtSecretKey = generateRandomString(32);
|
||||
$this->jwtPublicKey = null;
|
||||
} else if (in_array($this->jwtAlgorithm, ["RS256", "RS384", "RS512"])) {
|
||||
$bits = intval(substr($this->jwtAlgorithm, 2));
|
||||
$private_key = openssl_pkey_new(["private_key_bits" => $bits]);
|
||||
$this->jwtPublicKey = openssl_pkey_get_details($private_key)['key'];
|
||||
openssl_pkey_export($private_key, $this->jwtSecretKey);
|
||||
} else if (in_array($this->jwtAlgorithm, ["ES256", "ES384"])) {
|
||||
// $ec = new \Elliptic\EC('secp256k1'); ??
|
||||
$this->logger->error("JWT algorithm: '$this->jwtAlgorithm' is currently not supported.");
|
||||
return false;
|
||||
} else if ($this->jwtAlgorithm == "EdDSA") {
|
||||
$keyPair = sodium_crypto_sign_keypair();
|
||||
$this->jwtSecretKey = base64_encode(sodium_crypto_sign_secretkey($keyPair));
|
||||
$this->jwtPublicKey = base64_encode(sodium_crypto_sign_publickey($keyPair));
|
||||
} else {
|
||||
$this->logger->error("Invalid JWT algorithm: '$this->jwtAlgorithm', expected one of: " .
|
||||
implode(",", array_keys(\Firebase\JWT\JWT::$supported_algs)));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function isJwtAlgorithmSupported(string $algorithm): bool {
|
||||
return in_array(strtoupper($algorithm), ["HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "EDDSA"]);
|
||||
}
|
||||
|
||||
public function saveJwtKey(Context $context) {
|
||||
$req = new \Core\API\Settings\Set($context);
|
||||
$req->execute(array("settings" => array(
|
||||
"jwt_secret_key" => $this->jwtSecretKey,
|
||||
"jwt_public_key" => $this->jwtSecretKey,
|
||||
"jwt_algorithm" => $this->jwtAlgorithm,
|
||||
)));
|
||||
|
||||
return $req;
|
||||
}
|
||||
|
||||
public function loadFromDatabase(Context $context): bool {
|
||||
$this->logger = new Logger("Settings", $context->getSQL());
|
||||
$req = new \Core\API\Settings\Get($context);
|
||||
$success = $req->execute();
|
||||
|
||||
if ($success) {
|
||||
$result = $req->getResult()["settings"];
|
||||
$this->siteName = $result["site_name"] ?? $this->siteName;
|
||||
$this->baseUrl = $result["base_url"] ?? $this->baseUrl;
|
||||
$this->registrationAllowed = $result["user_registration_enabled"] ?? $this->registrationAllowed;
|
||||
$this->installationComplete = $result["installation_completed"] ?? $this->installationComplete;
|
||||
$this->jwtSecretKey = $result["jwt_secret_key"] ?? $this->jwtSecretKey;
|
||||
$this->jwtPublicKey = $result["jwt_public_key"] ?? $this->jwtPublicKey;
|
||||
$this->jwtAlgorithm = $result["jwt_algorithm"] ?? $this->jwtAlgorithm;
|
||||
$this->recaptchaEnabled = $result["recaptcha_enabled"] ?? $this->recaptchaEnabled;
|
||||
$this->recaptchaPublicKey = $result["recaptcha_public_key"] ?? $this->recaptchaPublicKey;
|
||||
$this->recaptchaPrivateKey = $result["recaptcha_private_key"] ?? $this->recaptchaPrivateKey;
|
||||
$this->mailEnabled = $result["mail_enabled"] ?? $this->mailEnabled;
|
||||
$this->mailSender = $result["mail_from"] ?? $this->mailSender;
|
||||
$this->mailFooter = $result["mail_footer"] ?? $this->mailFooter;
|
||||
$this->allowedExtensions = explode(",", $result["allowed_extensions"] ?? strtolower(implode(",", $this->allowedExtensions)));
|
||||
|
||||
if (!isset($result["jwt_secret_key"])) {
|
||||
if ($this->generateJwtKey()) {
|
||||
$this->saveJwtKey($context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function addRows(Insert $query) {
|
||||
$query->addRow("site_name", $this->siteName, false, false)
|
||||
->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_key", $this->jwtSecretKey, true, false)
|
||||
->addRow("jwt_public_key", $this->jwtPublicKey, false, false)
|
||||
->addRow("jwt_algorithm", $this->jwtAlgorithm, false, false)
|
||||
->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)
|
||||
->addRow("allowed_extensions", implode(",", $this->allowedExtensions), true, false);
|
||||
}
|
||||
|
||||
public function getSiteName(): string {
|
||||
return $this->siteName;
|
||||
}
|
||||
|
||||
public function getBaseUrl(): string {
|
||||
return $this->baseUrl;
|
||||
}
|
||||
|
||||
public function isRecaptchaEnabled(): bool {
|
||||
return $this->recaptchaEnabled;
|
||||
}
|
||||
|
||||
public function getRecaptchaSiteKey(): string {
|
||||
return $this->recaptchaPublicKey;
|
||||
}
|
||||
|
||||
public function getRecaptchaSecretKey(): string {
|
||||
return $this->recaptchaPrivateKey;
|
||||
}
|
||||
|
||||
public function isRegistrationAllowed(): bool {
|
||||
return $this->registrationAllowed;
|
||||
}
|
||||
|
||||
public function isMailEnabled(): bool {
|
||||
return $this->mailEnabled;
|
||||
}
|
||||
|
||||
public function getMailSender(): string {
|
||||
return $this->mailSender;
|
||||
}
|
||||
|
||||
public function isExtensionAllowed(string $ext): bool {
|
||||
return empty($this->allowedExtensions) || in_array(strtolower(trim($ext)), $this->allowedExtensions);
|
||||
}
|
||||
|
||||
public function getDomain(): string {
|
||||
return parse_url($this->getBaseUrl(), PHP_URL_HOST);
|
||||
}
|
||||
|
||||
public function getLogger(): Logger {
|
||||
return $this->logger;
|
||||
}
|
||||
}
|
||||
67
Core/Documents/Account.class.php
Normal file
67
Core/Documents/Account.class.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Core\Documents;
|
||||
|
||||
use Core\Elements\TemplateDocument;
|
||||
use Core\Objects\Router\Router;
|
||||
|
||||
|
||||
class Account extends TemplateDocument {
|
||||
public function __construct(Router $router, string $templateName) {
|
||||
parent::__construct($router, $templateName);
|
||||
$this->title = "Account";
|
||||
$this->searchable = false;
|
||||
$this->enableCSP();
|
||||
}
|
||||
|
||||
private function createError(string $message) {
|
||||
$this->parameters["view"]["success"] = false;
|
||||
$this->parameters["view"]["message"] = $message;
|
||||
}
|
||||
|
||||
protected function loadParameters() {
|
||||
$this->parameters["view"] = ["success" => true];
|
||||
if ($this->getTemplateName() === "account/reset_password.twig") {
|
||||
if (isset($_GET["token"]) && is_string($_GET["token"]) && !empty($_GET["token"])) {
|
||||
$this->parameters["view"]["token"] = $_GET["token"];
|
||||
$req = new \Core\API\User\CheckToken($this->getContext());
|
||||
$this->parameters["view"]["success"] = $req->execute(array("token" => $_GET["token"]));
|
||||
if ($this->parameters["view"]["success"]) {
|
||||
if (strcmp($req->getResult()["token"]["type"], "password_reset") !== 0) {
|
||||
$this->createError("The given token has a wrong type.");
|
||||
}
|
||||
} else {
|
||||
$this->createError("Error requesting password reset: " . $req->getLastError());
|
||||
}
|
||||
}
|
||||
} else if ($this->getTemplateName() === "account/register.twig") {
|
||||
$settings = $this->getSettings();
|
||||
if ($this->getUser()) {
|
||||
$this->createError("You are already logged in.");
|
||||
} else if (!$settings->isRegistrationAllowed()) {
|
||||
$this->createError("Registration is not enabled on this website.");
|
||||
}
|
||||
} else if ($this->getTemplateName() === "account/login.twig" && $this->getUser()) {
|
||||
header("Location: /admin");
|
||||
exit();
|
||||
} else if ($this->getTemplateName() === "account/accept_invite.twig") {
|
||||
if (isset($_GET["token"]) && is_string($_GET["token"]) && !empty($_GET["token"])) {
|
||||
$this->parameters["view"]["token"] = $_GET["token"];
|
||||
$req = new \Core\API\User\CheckToken($this->getContext());
|
||||
$this->parameters["view"]["success"] = $req->execute(array("token" => $_GET["token"]));
|
||||
if ($this->parameters["view"]["success"]) {
|
||||
if (strcmp($req->getResult()["token"]["type"], "invite") !== 0) {
|
||||
$this->createError("The given token has a wrong type.");
|
||||
} else {
|
||||
$this->parameters["view"]["invited_user"] = $req->getResult()["user"];
|
||||
}
|
||||
} else {
|
||||
$this->createError("Error confirming e-mail address: " . $req->getLastError());
|
||||
}
|
||||
} else {
|
||||
$this->createError("The link you visited is no longer valid");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
Core/Documents/Admin.class.php
Normal file
18
Core/Documents/Admin.class.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Documents;
|
||||
|
||||
use Core\Elements\TemplateDocument;
|
||||
use Core\Objects\Router\Router;
|
||||
|
||||
class Admin extends TemplateDocument {
|
||||
public function __construct(Router $router) {
|
||||
$user = $router->getContext()->getUser();
|
||||
$template = $user ? "admin.twig" : "redirect.twig";
|
||||
$params = $user ? [] : ["url" => "/login"];
|
||||
$this->title = "Administration";
|
||||
$this->searchable = false;
|
||||
parent::__construct($router, $template, $params);
|
||||
$this->enableCSP();
|
||||
}
|
||||
}
|
||||
28
Core/Documents/Info.class.php
Normal file
28
Core/Documents/Info.class.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Documents;
|
||||
|
||||
use Core\Elements\EmptyHead;
|
||||
use Core\Elements\HtmlDocument;
|
||||
use Core\Elements\SimpleBody;
|
||||
use Core\Objects\Router\Router;
|
||||
|
||||
class Info extends HtmlDocument {
|
||||
public function __construct(Router $router) {
|
||||
parent::__construct($router, EmptyHead::class, InfoBody::class);
|
||||
$this->searchable = false;
|
||||
}
|
||||
}
|
||||
|
||||
class InfoBody extends SimpleBody {
|
||||
protected function getContent(): string {
|
||||
$user = $this->getDocument()->getUser();
|
||||
if ($user && $user->hasGroup(USER_GROUP_ADMIN)) {
|
||||
phpinfo();
|
||||
return "";
|
||||
} else {
|
||||
$message = "You are not logged in or do not have the proper privileges to access this page.";
|
||||
return $this->getDocument()->getRouter()->returnStatusCode(403, [ "message" => $message] );
|
||||
}
|
||||
}
|
||||
}
|
||||
931
Core/Documents/Install.class.php
Normal file
931
Core/Documents/Install.class.php
Normal file
@@ -0,0 +1,931 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Documents {
|
||||
|
||||
use Documents\Install\InstallBody;
|
||||
use Documents\Install\InstallHead;
|
||||
use Core\Elements\HtmlDocument;
|
||||
use Core\Objects\Router\Router;
|
||||
|
||||
class Install extends HtmlDocument {
|
||||
public function __construct(Router $router) {
|
||||
parent::__construct($router, InstallHead::class, InstallBody::class);
|
||||
$this->databaseRequired = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace Documents\Install {
|
||||
|
||||
use Core\Configuration\Configuration;
|
||||
use Core\Configuration\CreateDatabase;
|
||||
use Core\Driver\SQL\Query\Commit;
|
||||
use Core\Driver\SQL\Query\RollBack;
|
||||
use Core\Driver\SQL\Query\StartTransaction;
|
||||
use Core\Driver\SQL\SQL;
|
||||
use Core\Elements\Body;
|
||||
use Core\Elements\Head;
|
||||
use Core\Elements\Link;
|
||||
use Core\Elements\Script;
|
||||
use Core\External\PHPMailer\Exception;
|
||||
use Core\External\PHPMailer\PHPMailer;
|
||||
use Core\Objects\ConnectionData;
|
||||
|
||||
class InstallHead extends Head {
|
||||
|
||||
public function __construct($document) {
|
||||
parent::__construct($document);
|
||||
}
|
||||
|
||||
protected function initSources() {
|
||||
$this->loadJQuery();
|
||||
$this->loadBootstrap();
|
||||
$this->loadFontawesome();
|
||||
$this->addJS(Script::CORE);
|
||||
$this->addCSS(Link::CORE);
|
||||
$this->addJS(Script::INSTALL);
|
||||
}
|
||||
|
||||
protected function initMetas(): array {
|
||||
return array(
|
||||
array('name' => 'viewport', 'content' => 'width=device-width, initial-scale=1.0'),
|
||||
array('name' => 'format-detection', 'content' => 'telephone=yes'),
|
||||
array('charset' => 'utf-8'),
|
||||
array("http-equiv" => 'expires', 'content' => '0'),
|
||||
array("name" => 'robots', 'content' => 'noarchive'),
|
||||
);
|
||||
}
|
||||
|
||||
protected function initRawFields(): array {
|
||||
return array();
|
||||
}
|
||||
|
||||
protected function initTitle(): string {
|
||||
return "WebBase - Installation";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class InstallBody extends Body {
|
||||
|
||||
// Status enum
|
||||
const NOT_STARTED = 0;
|
||||
const PENDING = 1;
|
||||
const SUCCESSFUL = 2;
|
||||
const ERROR = 3;
|
||||
|
||||
// Step enum
|
||||
const CHECKING_REQUIREMENTS = 1;
|
||||
const INSTALL_DEPENDENCIES = 2;
|
||||
const DATABASE_CONFIGURATION = 3;
|
||||
const CREATE_USER = 4;
|
||||
const ADD_MAIL_SERVICE = 5;
|
||||
const FINISH_INSTALLATION = 6;
|
||||
|
||||
//
|
||||
private string $errorString;
|
||||
private int $currentStep;
|
||||
private array $steps;
|
||||
|
||||
function __construct($document) {
|
||||
parent::__construct($document);
|
||||
$this->errorString = "";
|
||||
$this->currentStep = InstallBody::CHECKING_REQUIREMENTS;
|
||||
$this->steps = array();
|
||||
}
|
||||
|
||||
function isDocker(): bool {
|
||||
return file_exists("/.dockerenv");
|
||||
}
|
||||
|
||||
private function getParameter($name): ?string {
|
||||
if (isset($_REQUEST[$name]) && is_string($_REQUEST[$name])) {
|
||||
return trim($_REQUEST[$name]);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
private function composerInstall(bool $dryRun = false): array {
|
||||
$command = "composer install";
|
||||
if ($dryRun) {
|
||||
$command .= " --dry-run";
|
||||
}
|
||||
|
||||
$fds = [
|
||||
"1" => ["pipe", "w"],
|
||||
"2" => ["pipe", "w"],
|
||||
];
|
||||
|
||||
$dir = $this->getExternalDirectory();
|
||||
$env = null;
|
||||
if (!getenv("HOME")) {
|
||||
$env = ["COMPOSER_HOME" => $dir];
|
||||
}
|
||||
|
||||
$proc = proc_open($command, $fds, $pipes, $dir, $env);
|
||||
$output = stream_get_contents($pipes[1]) . stream_get_contents($pipes[2]);
|
||||
$status = proc_close($proc);
|
||||
return [$status, $output];
|
||||
}
|
||||
|
||||
private function getExternalDirectory(bool $absolute = true): string {
|
||||
if ($absolute) {
|
||||
return implode(DIRECTORY_SEPARATOR, [WEBROOT, "Core", "External"]);
|
||||
} else {
|
||||
return implode(DIRECTORY_SEPARATOR, ["Core", "External"]);
|
||||
}
|
||||
}
|
||||
|
||||
private function getCurrentStep(): int {
|
||||
|
||||
if (!$this->checkRequirements()["success"]) {
|
||||
return self::CHECKING_REQUIREMENTS;
|
||||
}
|
||||
|
||||
$externalDir = $this->getExternalDirectory();
|
||||
$autoload = implode(DIRECTORY_SEPARATOR, [$externalDir, "vendor", "autoload.php"]);
|
||||
if (!is_file($autoload)) {
|
||||
return self::INSTALL_DEPENDENCIES;
|
||||
} else {
|
||||
list ($status, $output) = $this->composerInstall(true);
|
||||
if ($status !== 0) {
|
||||
$this->errorString = "Error executing 'composer install --dry-run'. Please verify that the command succeeds locally and then try again. Status Code: $status, Output: $output";
|
||||
return self::CHECKING_REQUIREMENTS;
|
||||
} else {
|
||||
if (!contains($output, "Nothing to install, update or remove")) {
|
||||
return self::INSTALL_DEPENDENCIES;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$context = $this->getDocument()->getContext();
|
||||
$config = $context->getConfig();
|
||||
|
||||
// Check if database configuration exists
|
||||
if (!$config->getDatabase()) {
|
||||
return self::DATABASE_CONFIGURATION;
|
||||
}
|
||||
|
||||
$sql = $context->getSQL();
|
||||
if (!$sql || !$sql->isConnected()) {
|
||||
return self::DATABASE_CONFIGURATION;
|
||||
}
|
||||
|
||||
$countKeyword = $sql->count();
|
||||
$res = $sql->select($countKeyword)->from("User")->execute();
|
||||
if ($res === FALSE) {
|
||||
return self::DATABASE_CONFIGURATION;
|
||||
} else {
|
||||
if ($res[0]["count"] > 0) {
|
||||
$step = self::ADD_MAIL_SERVICE;
|
||||
} else {
|
||||
return self::CREATE_USER;
|
||||
}
|
||||
}
|
||||
|
||||
if ($step === self::ADD_MAIL_SERVICE) {
|
||||
$req = new \Core\API\Settings\Get($context);
|
||||
$success = $req->execute(array("key" => "^mail_enabled$"));
|
||||
if (!$success) {
|
||||
$this->errorString = $req->getLastError();
|
||||
return self::DATABASE_CONFIGURATION;
|
||||
} else if (isset($req->getResult()["settings"]["mail_enabled"])) {
|
||||
$step = self::FINISH_INSTALLATION;
|
||||
|
||||
$req = new \Core\API\Settings\Set($context);
|
||||
$success = $req->execute(array("settings" => array("installation_completed" => "1")));
|
||||
if (!$success) {
|
||||
$this->errorString = $req->getLastError();
|
||||
} else {
|
||||
$req = new \Core\API\Notifications\Create($context);
|
||||
$req->execute(array(
|
||||
"title" => "Welcome",
|
||||
"message" => "Your Web-base was successfully installed. Check out the admin dashboard. Have fun!",
|
||||
"groupId" => USER_GROUP_ADMIN
|
||||
)
|
||||
);
|
||||
$this->errorString = $req->getLastError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $step;
|
||||
}
|
||||
|
||||
private function command_exist(string $cmd): bool {
|
||||
$return = shell_exec(sprintf("which %s 2>/dev/null", escapeshellarg($cmd)));
|
||||
return !empty($return);
|
||||
}
|
||||
|
||||
private function checkRequirements(): array {
|
||||
|
||||
$msg = $this->errorString;
|
||||
$success = true;
|
||||
$failedRequirements = array();
|
||||
|
||||
if (!is_writeable(WEBROOT)) {
|
||||
$failedRequirements[] = sprintf("<b>%s</b> is not writeable. Try running <b>chmod 700 %s</b>", WEBROOT, WEBROOT);
|
||||
$success = false;
|
||||
}
|
||||
|
||||
if (function_exists("posix_getuid")) {
|
||||
$userId = posix_getuid();
|
||||
if (fileowner(WEBROOT) !== $userId) {
|
||||
$username = posix_getpwuid($userId)['name'];
|
||||
$failedRequirements[] = sprintf("<b>%s</b> is not owned by current user: $username ($userId). " .
|
||||
"Try running <b>chown -R $userId %s</b> or give the required directories write permissions: " .
|
||||
"<b>core/Configuration</b>, <b>core/Cache</b>, <b>core/External</b>",
|
||||
WEBROOT, WEBROOT);
|
||||
$success = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists("yaml_emit")) {
|
||||
$failedRequirements[] = "<b>YAML</b> extension is not installed.";
|
||||
$success = false;
|
||||
}
|
||||
|
||||
$requiredVersion = '8.0';
|
||||
if (version_compare(PHP_VERSION, $requiredVersion, '<')) {
|
||||
$failedRequirements[] = "PHP Version <b>>= $requiredVersion</b> is required. Got: <b>" . PHP_VERSION . "</b>";
|
||||
$success = false;
|
||||
}
|
||||
|
||||
if (!$this->command_exist("composer")) {
|
||||
$failedRequirements[] = "<b>Composer</b> is not installed or cannot be found.";
|
||||
$success = false;
|
||||
}
|
||||
|
||||
if (!$success) {
|
||||
$msg = "The following requirements failed the check:<br>" .
|
||||
$this->createUnorderedList($failedRequirements);
|
||||
$this->errorString = $msg;
|
||||
}
|
||||
|
||||
return array("success" => $success, "msg" => $msg);
|
||||
}
|
||||
|
||||
private function installDependencies(): array {
|
||||
list ($status, $output) = $this->composerInstall();
|
||||
return ["success" => $status === 0, "msg" => $output];
|
||||
}
|
||||
|
||||
private function databaseConfiguration(): array {
|
||||
|
||||
$host = $this->getParameter("host");
|
||||
$port = $this->getParameter("port");
|
||||
$username = $this->getParameter("username");
|
||||
$password = $this->getParameter("password");
|
||||
$database = $this->getParameter("database");
|
||||
$type = $this->getParameter("type");
|
||||
$encoding = $this->getParameter("encoding") ?? "UTF8";
|
||||
$success = true;
|
||||
|
||||
$missingInputs = array();
|
||||
if (empty($host)) {
|
||||
$success = false;
|
||||
$missingInputs[] = "Host";
|
||||
}
|
||||
|
||||
if (empty($port)) {
|
||||
$success = false;
|
||||
$missingInputs[] = "Port";
|
||||
}
|
||||
|
||||
if (empty($username)) {
|
||||
$success = false;
|
||||
$missingInputs[] = "Username";
|
||||
}
|
||||
|
||||
if (is_null($password)) {
|
||||
$success = false;
|
||||
$missingInputs[] = "Password";
|
||||
}
|
||||
|
||||
if (empty($database)) {
|
||||
$success = false;
|
||||
$missingInputs[] = "Database";
|
||||
}
|
||||
|
||||
if (empty($type)) {
|
||||
$success = false;
|
||||
$missingInputs[] = "Type";
|
||||
}
|
||||
|
||||
$supportedTypes = array("mysql", "postgres");
|
||||
if (!$success) {
|
||||
$msg = "Please fill out the following inputs:<br>" .
|
||||
$this->createUnorderedList($missingInputs);
|
||||
} else if (!is_numeric($port) || ($port = intval($port)) < 1 || $port > 65535) {
|
||||
$msg = "Port must be in range of 1-65535.";
|
||||
$success = false;
|
||||
} else if (!in_array($type, $supportedTypes)) {
|
||||
$msg = "Unsupported database type. Must be one of: " . implode(", ", $supportedTypes);
|
||||
$success = false;
|
||||
} else {
|
||||
$connectionData = new ConnectionData($host, $port, $username, $password);
|
||||
$connectionData->setProperty('database', $database);
|
||||
$connectionData->setProperty('encoding', $encoding);
|
||||
$connectionData->setProperty('type', $type);
|
||||
$connectionData->setProperty('isDocker', $this->isDocker());
|
||||
$sql = SQL::createConnection($connectionData);
|
||||
$success = false;
|
||||
if (is_string($sql)) {
|
||||
$msg = "Error connecting to database: $sql";
|
||||
} else if (!$sql->isConnected()) {
|
||||
if (!$sql->checkRequirements()) {
|
||||
$driverName = $sql->getDriverName();
|
||||
$installLink = "https://www.php.net/manual/en/$driverName.setup.php";
|
||||
$link = $this->createExternalLink($installLink);
|
||||
$msg = "$driverName is not enabled yet. See: $link";
|
||||
} else {
|
||||
$msg = "Error connecting to database:<br>" . $sql->getLastError();
|
||||
}
|
||||
} else {
|
||||
|
||||
$msg = "";
|
||||
$success = true;
|
||||
$queries = CreateDatabase::createQueries($sql);
|
||||
array_unshift($queries, new StartTransaction($sql));
|
||||
$queries[] = new Commit($sql);
|
||||
foreach ($queries as $query) {
|
||||
try {
|
||||
if (!$query->execute()) {
|
||||
$msg = "Error creating tables: " . $sql->getLastError();
|
||||
$success = false;
|
||||
}
|
||||
} finally {
|
||||
if (!$success) {
|
||||
(new RollBack($sql))->execute();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$success) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($success) {
|
||||
$context = $this->getDocument()->getContext();
|
||||
$config = $context->getConfig();
|
||||
if (Configuration::create(\Site\Configuration\Database::class, $connectionData) === false) {
|
||||
$success = false;
|
||||
$msg = "Unable to write database file";
|
||||
} else {
|
||||
$config->setDatabase($connectionData);
|
||||
if (!$context->initSQL()) {
|
||||
$success = false;
|
||||
$msg = "Unable to verify database connection after installation";
|
||||
} else {
|
||||
$req = new \Core\API\Routes\GenerateCache($context);
|
||||
if (!$req->execute()) {
|
||||
$success = false;
|
||||
$msg = "Unable to write route file: " . $req->getLastError();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$sql->close();
|
||||
}
|
||||
}
|
||||
|
||||
return array("success" => $success, "msg" => $msg);
|
||||
}
|
||||
|
||||
private function createUser(): array {
|
||||
|
||||
$context = $this->getDocument()->getContext();
|
||||
if ($this->getParameter("prev") === "true") {
|
||||
$success = $context->getConfig()->delete("Database");
|
||||
$msg = $success ? "" : error_get_last();
|
||||
return array("success" => $success, "msg" => $msg);
|
||||
}
|
||||
|
||||
$username = $this->getParameter("username");
|
||||
$password = $this->getParameter("password");
|
||||
$confirmPassword = $this->getParameter("confirmPassword");
|
||||
$email = $this->getParameter("email") ?? "";
|
||||
|
||||
$success = true;
|
||||
$missingInputs = array();
|
||||
|
||||
if (empty($username)) {
|
||||
$success = false;
|
||||
$missingInputs[] = "Username";
|
||||
}
|
||||
|
||||
if (empty($password)) {
|
||||
$success = false;
|
||||
$missingInputs[] = "Password";
|
||||
}
|
||||
|
||||
if (empty($confirmPassword)) {
|
||||
$success = false;
|
||||
$missingInputs[] = "Confirm Password";
|
||||
}
|
||||
|
||||
if (!$success) {
|
||||
$msg = "Please fill out the following inputs:<br>" .
|
||||
$this->createUnorderedList($missingInputs);
|
||||
} else {
|
||||
$req = new \Core\API\User\Create($context);
|
||||
$success = $req->execute(array(
|
||||
'username' => $username,
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
'confirmPassword' => $confirmPassword,
|
||||
));
|
||||
|
||||
$msg = $req->getLastError();
|
||||
if ($success) {
|
||||
$sql = $context->getSQL();
|
||||
$success = $sql->insert("UserGroup", array("group_id", "user_id"))
|
||||
->addRow(USER_GROUP_ADMIN, $req->getResult()["userId"])
|
||||
->execute();
|
||||
$msg = $sql->getLastError();
|
||||
}
|
||||
}
|
||||
|
||||
return array("msg" => $msg, "success" => $success);
|
||||
}
|
||||
|
||||
private function addMailService(): array {
|
||||
|
||||
$context = $this->getDocument()->getContext();
|
||||
if ($this->getParameter("prev") === "true") {
|
||||
$sql = $context->getSQL();
|
||||
$success = $sql->delete("User")->execute();
|
||||
$msg = $sql->getLastError();
|
||||
return array("success" => $success, "msg" => $msg);
|
||||
}
|
||||
|
||||
if ($this->getParameter("skip") === "true") {
|
||||
$req = new \Core\API\Settings\Set($context);
|
||||
$success = $req->execute(array("settings" => array("mail_enabled" => "0")));
|
||||
$msg = $req->getLastError();
|
||||
} else {
|
||||
|
||||
$address = $this->getParameter("address");
|
||||
$port = $this->getParameter("port");
|
||||
$username = $this->getParameter("username");
|
||||
$password = $this->getParameter("password");
|
||||
$success = true;
|
||||
|
||||
$missingInputs = array();
|
||||
if (empty($address)) {
|
||||
$success = false;
|
||||
$missingInputs[] = "SMTP Address";
|
||||
}
|
||||
|
||||
if (empty($port)) {
|
||||
$success = false;
|
||||
$missingInputs[] = "Port";
|
||||
}
|
||||
|
||||
if (empty($username)) {
|
||||
$success = false;
|
||||
$missingInputs[] = "Username";
|
||||
}
|
||||
|
||||
if (is_null($password)) {
|
||||
$success = false;
|
||||
$missingInputs[] = "Password";
|
||||
}
|
||||
|
||||
if (!$success) {
|
||||
$msg = "Please fill out the following inputs:<br>" .
|
||||
$this->createUnorderedList($missingInputs);
|
||||
} else if (!is_numeric($port) || ($port = intval($port)) < 1 || $port > 65535) {
|
||||
$msg = "Port must be in range of 1-65535.";
|
||||
$success = false;
|
||||
} else {
|
||||
$success = false;
|
||||
|
||||
$mail = new PHPMailer(true);
|
||||
$mail->IsSMTP();
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = $username;
|
||||
$mail->Password = $password;
|
||||
$mail->Host = $address;
|
||||
$mail->Port = $port;
|
||||
$mail->SMTPSecure = 'tls';
|
||||
$mail->Timeout = 10;
|
||||
|
||||
try {
|
||||
$success = $mail->SmtpConnect();
|
||||
if (!$success) {
|
||||
$error = empty($mail->ErrorInfo) ? "Unknown Error" : $mail->ErrorInfo;
|
||||
$msg = "Could not connect to SMTP Server: $error";
|
||||
} else {
|
||||
$success = true;
|
||||
$msg = "";
|
||||
$mail->smtpClose();
|
||||
}
|
||||
} catch (Exception $error) {
|
||||
$msg = "Could not connect to SMTP Server: " . $error->errorMessage();
|
||||
}
|
||||
|
||||
if ($success) {
|
||||
$req = new \Core\API\Settings\Set($context);
|
||||
$success = $req->execute(array("settings" => array(
|
||||
"mail_enabled" => "1",
|
||||
"mail_host" => "$address",
|
||||
"mail_port" => "$port",
|
||||
"mail_username" => "$username",
|
||||
"mail_password" => "$password",
|
||||
)));
|
||||
$msg = $req->getLastError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array("success" => $success, "msg" => $msg);
|
||||
}
|
||||
|
||||
private function performStep(): array {
|
||||
|
||||
switch ($this->currentStep) {
|
||||
|
||||
case self::CHECKING_REQUIREMENTS:
|
||||
return $this->checkRequirements();
|
||||
|
||||
case self::INSTALL_DEPENDENCIES:
|
||||
return $this->installDependencies();
|
||||
|
||||
case self::DATABASE_CONFIGURATION:
|
||||
return $this->databaseConfiguration();
|
||||
|
||||
case self::CREATE_USER:
|
||||
return $this->createUser();
|
||||
|
||||
case self::ADD_MAIL_SERVICE:
|
||||
return $this->addMailService();
|
||||
|
||||
default:
|
||||
return array(
|
||||
"success" => false,
|
||||
"msg" => "Invalid step number"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function createProgressSidebar(): string {
|
||||
$items = array();
|
||||
foreach ($this->steps as $num => $step) {
|
||||
|
||||
$title = $step["title"];
|
||||
$status = $step["status"];
|
||||
$currentStep = ($num == $this->currentStep) ? " id=\"currentStep\"" : "";
|
||||
|
||||
switch ($status) {
|
||||
case self::PENDING:
|
||||
$statusIcon = $this->createIcon("spinner");
|
||||
$statusText = "Loading…";
|
||||
$statusColor = "muted";
|
||||
break;
|
||||
|
||||
case self::SUCCESSFUL:
|
||||
$statusIcon = $this->createIcon("check-circle");
|
||||
$statusText = "Successful";
|
||||
$statusColor = "success";
|
||||
break;
|
||||
|
||||
case self::ERROR:
|
||||
$statusIcon = $this->createIcon("times-circle");
|
||||
$statusText = "Failed";
|
||||
$statusColor = "danger";
|
||||
break;
|
||||
|
||||
case self::NOT_STARTED:
|
||||
default:
|
||||
$statusIcon = $this->createIcon("circle", "far");
|
||||
$statusText = "Pending";
|
||||
$statusColor = "muted";
|
||||
break;
|
||||
}
|
||||
|
||||
$items[] = "
|
||||
<li class=\"list-group-item d-flex justify-content-between lh-condensed\"$currentStep>
|
||||
<div>
|
||||
<h6 class=\"my-0\">$title</h6>
|
||||
<small class=\"text-$statusColor\">$statusText</small>
|
||||
</div>
|
||||
<span class=\"text-$statusColor\">$statusIcon</span>
|
||||
</li>";
|
||||
}
|
||||
|
||||
return implode("", $items);
|
||||
}
|
||||
|
||||
private function createFormItem($formItem, $inline = false): string {
|
||||
|
||||
$title = $formItem["title"];
|
||||
$name = $formItem["name"];
|
||||
$type = $formItem["type"];
|
||||
|
||||
$attributes = array(
|
||||
"name" => $name,
|
||||
"id" => $name,
|
||||
"class" => "form-control"
|
||||
);
|
||||
|
||||
if (isset($formItem["required"]) && $formItem["required"]) {
|
||||
$attributes["required"] = "";
|
||||
}
|
||||
|
||||
if ($type !== "select") {
|
||||
$attributes["type"] = $type;
|
||||
|
||||
if (isset($formItem["value"]) && $formItem["value"]) {
|
||||
$attributes["value"] = $formItem["value"];
|
||||
}
|
||||
|
||||
if ($type === "number") {
|
||||
if (isset($formItem["min"]) && is_numeric($formItem["min"]))
|
||||
$attributes["min"] = $formItem["min"];
|
||||
if (isset($formItem["max"]) && is_numeric($formItem["max"]))
|
||||
$attributes["max"] = $formItem["max"];
|
||||
if (isset($formItem["step"]) && is_numeric($formItem["step"]))
|
||||
$attributes["step"] = $formItem["step"];
|
||||
} else {
|
||||
if (isset($formItem["default"])) {
|
||||
$attributes["value"] = $formItem["default"];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// $attributes = html_attributes($attributes);
|
||||
if ($type === "select") {
|
||||
$items = $formItem["items"] ?? array();
|
||||
$options = [];
|
||||
foreach ($items as $key => $val) {
|
||||
$options[] = html_tag_ex("option", ["value" => $key], $val, true, false);
|
||||
}
|
||||
|
||||
$element = html_tag_ex("select", $attributes, $options, false);
|
||||
} else {
|
||||
$element = html_tag_short("input", $attributes);
|
||||
}
|
||||
|
||||
$label = html_tag_ex("label", ["for" => $name], $title, true, false);
|
||||
$className = ($inline ? "col-md-6 mb-3" : "d-block my-3");
|
||||
return html_tag_ex("div", ["class" => $className], $label . $element, false);
|
||||
}
|
||||
|
||||
private function createProgressMainview(): string {
|
||||
|
||||
$isDocker = $this->isDocker();
|
||||
$defaultHost = ($isDocker ? "db" : "localhost");
|
||||
$defaultUsername = ($isDocker ? "root" : "");
|
||||
$defaultPassword = ($isDocker ? "webbasedb" : "");
|
||||
$defaultDatabase = ($isDocker ? "webbase" : "");
|
||||
|
||||
$views = array(
|
||||
self::CHECKING_REQUIREMENTS => array(
|
||||
"title" => "Application Requirements",
|
||||
"progressText" => "Checking requirements, please wait a moment…"
|
||||
),
|
||||
self::INSTALL_DEPENDENCIES => array(
|
||||
"title" => "Installing Dependencies",
|
||||
"progressText" => "Please wait while required dependencies are being installed…",
|
||||
),
|
||||
self::DATABASE_CONFIGURATION => array(
|
||||
"title" => "Database configuration",
|
||||
"form" => array(
|
||||
array("title" => "Database Type", "name" => "type", "type" => "select", "required" => true, "items" => array(
|
||||
"mysql" => "MySQL", "postgres" => "PostgreSQL"
|
||||
)),
|
||||
array("title" => "Username", "name" => "username", "type" => "text", "required" => true, "default" => $defaultUsername),
|
||||
array("title" => "Password", "name" => "password", "type" => "password", "default" => $defaultPassword),
|
||||
array("title" => "Database", "name" => "database", "type" => "text", "required" => true, "default" => $defaultDatabase),
|
||||
array("type" => "row", "items" => array(
|
||||
array(
|
||||
"title" => "Address", "name" => "host", "type" => "text", "required" => true,
|
||||
"value" => "localhost", "row" => true, "default" => $defaultHost
|
||||
),
|
||||
array(
|
||||
"title" => "Port", "name" => "port", "type" => "number", "required" => true,
|
||||
"value" => "3306", "min" => "1", "max" => "65535", "row" => true
|
||||
)
|
||||
)),
|
||||
array(
|
||||
"title" => "Encoding", "name" => "encoding", "type" => "text", "required" => false,
|
||||
"value" => "UTF8"
|
||||
),
|
||||
)
|
||||
),
|
||||
self::CREATE_USER => array(
|
||||
"title" => "Create a User",
|
||||
"form" => array(
|
||||
array("title" => "Username", "name" => "username", "type" => "text", "required" => true),
|
||||
array("title" => "Email", "name" => "email", "type" => "text"),
|
||||
array("title" => "Password", "name" => "password", "type" => "password", "required" => true),
|
||||
array("title" => "Confirm Password", "name" => "confirmPassword", "type" => "password", "required" => true),
|
||||
),
|
||||
"previousButton" => true
|
||||
),
|
||||
self::ADD_MAIL_SERVICE => array(
|
||||
"title" => "Optional: Add Mail Service",
|
||||
"form" => array(
|
||||
array("title" => "Username", "name" => "username", "type" => "text", "required" => true),
|
||||
array("title" => "Password", "name" => "password", "type" => "password"),
|
||||
array("type" => "row", "items" => array(
|
||||
array(
|
||||
"title" => "SMTP Address", "name" => "address", "type" => "text", "required" => true,
|
||||
"value" => "localhost", "row" => true
|
||||
),
|
||||
array(
|
||||
"title" => "Port", "name" => "port", "type" => "number", "required" => true,
|
||||
"value" => "587", "min" => "1", "max" => "65535", "row" => true
|
||||
)
|
||||
)),
|
||||
),
|
||||
"skip" => true,
|
||||
"previousButton" => true
|
||||
),
|
||||
self::FINISH_INSTALLATION => array(
|
||||
"title" => "Finish Installation",
|
||||
"text" => "Installation finished, you can now customize your own website, check the source code and stuff."
|
||||
)
|
||||
);
|
||||
|
||||
if (!isset($views[$this->currentStep])) {
|
||||
return "";
|
||||
}
|
||||
|
||||
$currentView = $views[$this->currentStep];
|
||||
$prevDisabled = !isset($currentView["previousButton"]) || !$currentView["previousButton"];
|
||||
$spinnerIcon = $this->createIcon("spinner");
|
||||
$title = $currentView["title"];
|
||||
|
||||
$html = "<h4 class=\"mb-3\">$title</h4><hr class=\"mb-4\" />";
|
||||
|
||||
if (isset($currentView["text"])) {
|
||||
$text = $currentView["text"];
|
||||
$html .= "<div class=\"my-3\">$text</i></div>";
|
||||
}
|
||||
|
||||
if (isset($currentView["progressText"])) {
|
||||
$progressText = $currentView["progressText"];
|
||||
$hidden = (!in_array($this->currentStep, [self::CHECKING_REQUIREMENTS, self::INSTALL_DEPENDENCIES]))
|
||||
? " hidden" : "";
|
||||
$html .= "<div id=\"progressText\" class=\"my-3$hidden\">$progressText$spinnerIcon</i></div>";
|
||||
}
|
||||
|
||||
if (isset($currentView["form"])) {
|
||||
$html .= "<form id=\"installForm\">";
|
||||
|
||||
foreach ($currentView["form"] as $formItem) {
|
||||
|
||||
if ($formItem["type"] === "row") {
|
||||
$html .= "<div class=\"row\">";
|
||||
foreach ($formItem["items"] as $item) {
|
||||
$html .= $this->createFormItem($item, true);
|
||||
}
|
||||
$html .= "</div>";
|
||||
} else {
|
||||
$html .= $this->createFormItem($formItem);
|
||||
}
|
||||
}
|
||||
|
||||
$html .= "</form>";
|
||||
}
|
||||
|
||||
$buttons = array(
|
||||
array("title" => "Go Back", "type" => "info", "id" => "btnPrev", "float" => "left", "disabled" => $prevDisabled)
|
||||
);
|
||||
|
||||
if ($this->currentStep != self::FINISH_INSTALLATION) {
|
||||
if (in_array($this->currentStep, [self::CHECKING_REQUIREMENTS, self::INSTALL_DEPENDENCIES])) {
|
||||
$buttons[] = array("title" => "Retry", "type" => "success", "id" => "btnRetry", "float" => "right", "hidden" => true);
|
||||
} else {
|
||||
$buttons[] = array("title" => "Submit", "type" => "success", "id" => "btnSubmit", "float" => "right");
|
||||
}
|
||||
} else {
|
||||
$buttons[] = array("title" => "Finish", "type" => "success", "id" => "btnFinish", "float" => "right");
|
||||
}
|
||||
|
||||
if (isset($currentView["skip"])) {
|
||||
$buttons[] = array("title" => "Skip", "type" => "secondary", "id" => "btnSkip", "float" => "right");
|
||||
}
|
||||
|
||||
$buttonsLeft = "";
|
||||
$buttonsRight = "";
|
||||
|
||||
foreach ($buttons as $button) {
|
||||
$title = $button["title"];
|
||||
$type = $button["type"];
|
||||
$id = $button["id"];
|
||||
$float = $button["float"];
|
||||
$disabled = (isset($button["disabled"]) && $button["disabled"]) ? " disabled" : "";
|
||||
$hidden = (isset($button["hidden"]) && $button["hidden"]) ? " hidden" : "";
|
||||
$button = "<button type=\"button\" id=\"$id\" class=\"btn btn-$type m-1$hidden\"$disabled>$title</button>";
|
||||
|
||||
if ($float === "left") {
|
||||
$buttonsLeft .= $button;
|
||||
} else {
|
||||
$buttonsRight .= $button;
|
||||
}
|
||||
}
|
||||
|
||||
$html .=
|
||||
"<div class=\"row\">
|
||||
<div class=\"col-6 float-left text-left\">$buttonsLeft</div>
|
||||
<div class=\"col-6 float-right text-right\">$buttonsRight</div>
|
||||
</div>";
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
function getCode(): string {
|
||||
$html = parent::getCode();
|
||||
|
||||
$this->steps = array(
|
||||
self::CHECKING_REQUIREMENTS => array(
|
||||
"title" => "Checking requirements",
|
||||
"status" => self::ERROR
|
||||
),
|
||||
self::INSTALL_DEPENDENCIES => array(
|
||||
"title" => "Install dependencies",
|
||||
"status" => self::NOT_STARTED
|
||||
),
|
||||
self::DATABASE_CONFIGURATION => array(
|
||||
"title" => "Database configuration",
|
||||
"status" => self::NOT_STARTED
|
||||
),
|
||||
self::CREATE_USER => array(
|
||||
"title" => "Create User",
|
||||
"status" => self::NOT_STARTED
|
||||
),
|
||||
self::ADD_MAIL_SERVICE => array(
|
||||
"title" => "Add Mail Service",
|
||||
"status" => self::NOT_STARTED
|
||||
),
|
||||
self::FINISH_INSTALLATION => array(
|
||||
"title" => "Finish Installation",
|
||||
"status" => self::NOT_STARTED
|
||||
),
|
||||
);
|
||||
|
||||
$this->currentStep = $this->getCurrentStep();
|
||||
|
||||
// set status
|
||||
for ($step = self::CHECKING_REQUIREMENTS; $step < $this->currentStep; $step++) {
|
||||
$this->steps[$step]["status"] = self::SUCCESSFUL;
|
||||
}
|
||||
|
||||
if ($this->currentStep == self::FINISH_INSTALLATION) {
|
||||
$this->steps[$this->currentStep]["status"] = self::SUCCESSFUL;
|
||||
}
|
||||
|
||||
// POST
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||
if (!isset($_REQUEST['status'])) {
|
||||
$response = $this->performStep();
|
||||
} else {
|
||||
$response = ["error" => $this->errorString];
|
||||
}
|
||||
$response["step"] = $this->currentStep;
|
||||
die(json_encode($response));
|
||||
}
|
||||
|
||||
$progressSidebar = $this->createProgressSidebar();
|
||||
$progressMainView = $this->createProgressMainview();
|
||||
|
||||
$errorStyle = ($this->errorString ? '' : ' style="display:none"');
|
||||
$errorClass = ($this->errorString ? ' alert-danger' : '');
|
||||
|
||||
$html .= "
|
||||
<body class=\"bg-light\">
|
||||
<div class=\"container\">
|
||||
<div class=\"py-5 text-center\">
|
||||
<h2>WebBase - Installation</h2>
|
||||
<p class=\"lead\">
|
||||
Process the following steps and fill out the required forms to install your WebBase-Installation.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class=\"row\">
|
||||
<div class=\"col-md-4 order-md-2 mb-4\">
|
||||
<h4 class=\"d-flex justify-content-between align-items-center mb-3\">
|
||||
<span class=\"text-muted\">Progress</span>
|
||||
</h4>
|
||||
|
||||
<ul class=\"list-group mb-3\">
|
||||
$progressSidebar
|
||||
</ul>
|
||||
</div>
|
||||
<div class=\"col-md-8 order-md-1\">
|
||||
$progressMainView
|
||||
<div class=\"alert$errorClass mt-4\" id=\"status\"$errorStyle>$this->errorString</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>";
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
}
|
||||
126
Core/Driver/Logger/Logger.class.php
Normal file
126
Core/Driver/Logger/Logger.class.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\Logger;
|
||||
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
class Logger {
|
||||
|
||||
public const LOG_FILE_DATE_FORMAT = "Y-m-d_H-i-s_v";
|
||||
public const LOG_LEVELS = [
|
||||
0 => "debug",
|
||||
1 => "info",
|
||||
2 => "warning",
|
||||
3 => "error",
|
||||
4 => "severe"
|
||||
];
|
||||
|
||||
public static Logger $INSTANCE;
|
||||
|
||||
private ?SQL $sql;
|
||||
private string $module;
|
||||
|
||||
// unit tests
|
||||
private bool $unitTestMode;
|
||||
private ?string $lastMessage;
|
||||
private ?string $lastLevel;
|
||||
|
||||
public function __construct(string $module = "Unknown", ?SQL $sql = null) {
|
||||
$this->module = $module;
|
||||
$this->sql = $sql;
|
||||
$this->unitTestMode = false;
|
||||
$this->lastMessage = null;
|
||||
$this->lastLevel = null;
|
||||
}
|
||||
|
||||
protected function getStackTrace(int $pop = 2): string {
|
||||
$debugTrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
|
||||
if ($pop > 0) {
|
||||
array_splice($debugTrace, 0, $pop);
|
||||
}
|
||||
return implode("\n", array_map(function ($trace) {
|
||||
if (isset($trace["file"])) {
|
||||
return $trace["file"] . "#" . $trace["line"] . ": " . $trace["function"] . "()";
|
||||
} else {
|
||||
return $trace["function"] . "()";
|
||||
}
|
||||
}, $debugTrace));
|
||||
}
|
||||
|
||||
public function log(string $message, string $severity, bool $appendStackTrace = true) {
|
||||
|
||||
if ($appendStackTrace) {
|
||||
$message .= "\n" . $this->getStackTrace();
|
||||
}
|
||||
|
||||
$this->lastMessage = $message;
|
||||
$this->lastLevel = $severity;
|
||||
if ($this->unitTestMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->sql !== null && $this->sql->isConnected()) {
|
||||
$success = $this->sql->insert("SystemLog", ["module", "message", "severity"])
|
||||
->addRow($this->module, $message, $severity)
|
||||
->execute();
|
||||
if ($success !== false) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// database logging failed, try to log to file
|
||||
$module = preg_replace("/[^a-zA-Z0-9-]/", "-", $this->module);
|
||||
$date = (\DateTime::createFromFormat('U.u', microtime(true)))->format(self::LOG_FILE_DATE_FORMAT);
|
||||
$logFile = implode("_", [$module, $severity, $date]) . ".log";
|
||||
$logPath = implode(DIRECTORY_SEPARATOR, [WEBROOT, "core", "Logs", $logFile]);
|
||||
@file_put_contents($logPath, $message);
|
||||
}
|
||||
|
||||
public function error(string $message): string {
|
||||
$this->log($message, "error");
|
||||
return $message;
|
||||
}
|
||||
|
||||
public function severe(string $message): string {
|
||||
$this->log($message, "severe");
|
||||
return $message;
|
||||
}
|
||||
|
||||
public function warning(string $message): string {
|
||||
$this->log($message, "warning", false);
|
||||
return $message;
|
||||
}
|
||||
|
||||
public function info(string $message): string {
|
||||
$this->log($message, "info", false);
|
||||
return $message;
|
||||
}
|
||||
|
||||
public function debug(string $message, bool $appendStackTrace = false): string {
|
||||
$this->log($message, "debug", $appendStackTrace);
|
||||
return $message;
|
||||
}
|
||||
|
||||
public static function instance(): Logger {
|
||||
if (self::$INSTANCE === null) {
|
||||
self::$INSTANCE = new Logger("Global");
|
||||
}
|
||||
|
||||
return self::$INSTANCE;
|
||||
}
|
||||
|
||||
public function getLastMessage(): ?string {
|
||||
return $this->lastMessage;
|
||||
}
|
||||
|
||||
public function getLastLevel(): ?string {
|
||||
return $this->lastLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calling this method will prevent the logger from persisting log messages (writing to database/file),
|
||||
*/
|
||||
public function unitTestMode() {
|
||||
$this->unitTestMode = true;
|
||||
}
|
||||
}
|
||||
11
Core/Driver/SQL/Column/BigIntColumn.class.php
Normal file
11
Core/Driver/SQL/Column/BigIntColumn.class.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Column;
|
||||
|
||||
class BigIntColumn extends IntColumn {
|
||||
|
||||
public function __construct(string $name, bool $nullable, $defaultValue, bool $unsigned) {
|
||||
parent::__construct($name, $nullable, $defaultValue, $unsigned);
|
||||
$this->type = "BIGINT";
|
||||
}
|
||||
}
|
||||
11
Core/Driver/SQL/Column/BoolColumn.class.php
Normal file
11
Core/Driver/SQL/Column/BoolColumn.class.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Column;
|
||||
|
||||
class BoolColumn extends Column {
|
||||
|
||||
public function __construct(string $name, bool $defaultValue = false) {
|
||||
parent::__construct($name, false, $defaultValue);
|
||||
}
|
||||
|
||||
}
|
||||
23
Core/Driver/SQL/Column/Column.class.php
Normal file
23
Core/Driver/SQL/Column/Column.class.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Column;
|
||||
|
||||
use Core\Driver\SQL\Expression\Expression;
|
||||
|
||||
class Column extends Expression {
|
||||
|
||||
private string $name;
|
||||
private bool $nullable;
|
||||
private $defaultValue;
|
||||
|
||||
public function __construct(string $name, bool $nullable = false, $defaultValue = NULL) {
|
||||
$this->name = $name;
|
||||
$this->nullable = $nullable;
|
||||
$this->defaultValue = $defaultValue;
|
||||
}
|
||||
|
||||
public function getName(): string { return $this->name; }
|
||||
public function notNull(): bool { return !$this->nullable; }
|
||||
public function getDefaultValue() { return $this->defaultValue; }
|
||||
|
||||
}
|
||||
10
Core/Driver/SQL/Column/DateTimeColumn.class.php
Normal file
10
Core/Driver/SQL/Column/DateTimeColumn.class.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Column;
|
||||
|
||||
class DateTimeColumn extends Column {
|
||||
|
||||
public function __construct(string $name, bool $nullable = false, $defaultValue = NULL) {
|
||||
parent::__construct($name, $nullable, $defaultValue);
|
||||
}
|
||||
}
|
||||
10
Core/Driver/SQL/Column/DoubleColumn.class.php
Normal file
10
Core/Driver/SQL/Column/DoubleColumn.class.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Column;
|
||||
|
||||
class DoubleColumn extends NumericColumn {
|
||||
public function __construct(string $name, bool $nullable, $defaultValue = NULL, ?int $totalDigits = null, ?int $decimalDigits = null) {
|
||||
parent::__construct($name, $nullable, $defaultValue, $totalDigits, $decimalDigits);
|
||||
$this->type = "DOUBLE";
|
||||
}
|
||||
}
|
||||
19
Core/Driver/SQL/Column/EnumColumn.class.php
Normal file
19
Core/Driver/SQL/Column/EnumColumn.class.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Column;
|
||||
|
||||
class EnumColumn extends Column {
|
||||
|
||||
private array $values;
|
||||
|
||||
public function __construct(string $name, array $values, bool $nullable = false, $defaultValue = NULL) {
|
||||
parent::__construct($name, $nullable, $defaultValue);
|
||||
$this->values = $values;
|
||||
}
|
||||
|
||||
public function addValue(string $value) {
|
||||
$this->values[] = $value;
|
||||
}
|
||||
|
||||
public function getValues(): array { return $this->values; }
|
||||
}
|
||||
10
Core/Driver/SQL/Column/FloatColumn.class.php
Normal file
10
Core/Driver/SQL/Column/FloatColumn.class.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Column;
|
||||
|
||||
class FloatColumn extends NumericColumn {
|
||||
public function __construct(string $name, bool $nullable, $defaultValue = NULL, ?int $totalDigits = null, ?int $decimalDigits = null) {
|
||||
parent::__construct($name, $nullable, $defaultValue, $totalDigits, $decimalDigits);
|
||||
$this->type = "FLOAT";
|
||||
}
|
||||
}
|
||||
23
Core/Driver/SQL/Column/IntColumn.class.php
Normal file
23
Core/Driver/SQL/Column/IntColumn.class.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Column;
|
||||
|
||||
class IntColumn extends Column {
|
||||
|
||||
protected string $type;
|
||||
private bool $unsigned;
|
||||
|
||||
public function __construct(string $name, bool $nullable = false, $defaultValue = NULL, bool $unsigned = false) {
|
||||
parent::__construct($name, $nullable, $defaultValue);
|
||||
$this->type = "INTEGER";
|
||||
$this->unsigned = $unsigned;
|
||||
}
|
||||
|
||||
public function isUnsigned(): bool {
|
||||
return $this->unsigned;
|
||||
}
|
||||
|
||||
public function getType(): string {
|
||||
return $this->type;
|
||||
}
|
||||
}
|
||||
11
Core/Driver/SQL/Column/JsonColumn.class.php
Normal file
11
Core/Driver/SQL/Column/JsonColumn.class.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Column;
|
||||
|
||||
class JsonColumn extends Column {
|
||||
|
||||
public function __construct(string $name, bool $nullable = false, $defaultValue = null) {
|
||||
parent::__construct($name, $nullable, $defaultValue);
|
||||
}
|
||||
|
||||
}
|
||||
31
Core/Driver/SQL/Column/NumericColumn.class.php
Normal file
31
Core/Driver/SQL/Column/NumericColumn.class.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Column;
|
||||
|
||||
use Core\Driver\SQL\Column\Column;
|
||||
|
||||
class NumericColumn extends Column {
|
||||
|
||||
protected string $type;
|
||||
private ?int $totalDigits;
|
||||
private ?int $decimalDigits;
|
||||
|
||||
public function __construct(string $name, bool $nullable, $defaultValue = NULL, ?int $totalDigits = null, ?int $decimalDigits = null) {
|
||||
parent::__construct($name, $nullable, $defaultValue);
|
||||
$this->totalDigits = $totalDigits;
|
||||
$this->decimalDigits = $decimalDigits;
|
||||
$this->type = "NUMERIC";
|
||||
}
|
||||
|
||||
public function getDecimalDigits(): ?int {
|
||||
return $this->decimalDigits;
|
||||
}
|
||||
|
||||
public function getTotalDigits(): ?int {
|
||||
return $this->totalDigits;
|
||||
}
|
||||
|
||||
public function getTypeName(): string {
|
||||
return $this->type;
|
||||
}
|
||||
}
|
||||
11
Core/Driver/SQL/Column/SerialColumn.class.php
Normal file
11
Core/Driver/SQL/Column/SerialColumn.class.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Column;
|
||||
|
||||
class SerialColumn extends Column {
|
||||
|
||||
public function __construct(string $name, $defaultValue = NULL) {
|
||||
parent::__construct($name, false, $defaultValue); # not nullable
|
||||
}
|
||||
|
||||
}
|
||||
15
Core/Driver/SQL/Column/StringColumn.class.php
Normal file
15
Core/Driver/SQL/Column/StringColumn.class.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Column;
|
||||
|
||||
class StringColumn extends Column {
|
||||
|
||||
private ?int $maxSize;
|
||||
|
||||
public function __construct(string $name, ?int $maxSize = null, bool $nullable = false, $defaultValue = null) {
|
||||
parent::__construct($name, $nullable, $defaultValue);
|
||||
$this->maxSize = $maxSize;
|
||||
}
|
||||
|
||||
public function getMaxSize(): ?int { return $this->maxSize; }
|
||||
}
|
||||
21
Core/Driver/SQL/Condition/Compare.class.php
Normal file
21
Core/Driver/SQL/Condition/Compare.class.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Condition;
|
||||
|
||||
class Compare extends Condition {
|
||||
|
||||
private string $operator;
|
||||
private string $column;
|
||||
private $value;
|
||||
|
||||
public function __construct(string $col, $val, string $operator = '=') {
|
||||
$this->operator = $operator;
|
||||
$this->column = $col;
|
||||
$this->value = $val;
|
||||
}
|
||||
|
||||
public function getColumn(): string { return $this->column; }
|
||||
public function getValue() { return $this->value; }
|
||||
public function getOperator(): string { return $this->operator; }
|
||||
|
||||
}
|
||||
14
Core/Driver/SQL/Condition/CondAnd.class.php
Normal file
14
Core/Driver/SQL/Condition/CondAnd.class.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Condition;
|
||||
|
||||
class CondAnd extends Condition {
|
||||
|
||||
private array $conditions;
|
||||
|
||||
public function __construct(...$conditions) {
|
||||
$this->conditions = $conditions;
|
||||
}
|
||||
|
||||
public function getConditions(): array { return $this->conditions; }
|
||||
}
|
||||
15
Core/Driver/SQL/Condition/CondBool.class.php
Normal file
15
Core/Driver/SQL/Condition/CondBool.class.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Condition;
|
||||
|
||||
class CondBool extends Condition {
|
||||
|
||||
private $value;
|
||||
|
||||
public function __construct($val) {
|
||||
$this->value = $val;
|
||||
}
|
||||
|
||||
public function getValue() { return $this->value; }
|
||||
|
||||
}
|
||||
17
Core/Driver/SQL/Condition/CondIn.class.php
Normal file
17
Core/Driver/SQL/Condition/CondIn.class.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Condition;
|
||||
|
||||
class CondIn extends Condition {
|
||||
|
||||
private $needle;
|
||||
private $haystack;
|
||||
|
||||
public function __construct($needle, $haystack) {
|
||||
$this->needle = $needle;
|
||||
$this->haystack = $haystack;
|
||||
}
|
||||
|
||||
public function getNeedle() { return $this->needle; }
|
||||
public function getHaystack() { return $this->haystack; }
|
||||
}
|
||||
20
Core/Driver/SQL/Condition/CondKeyword.class.php
Normal file
20
Core/Driver/SQL/Condition/CondKeyword.class.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Condition;
|
||||
|
||||
abstract class CondKeyword extends Condition {
|
||||
|
||||
private $leftExpression;
|
||||
private $rightExpression;
|
||||
private string $keyword;
|
||||
|
||||
public function __construct(string $keyword, $leftExpression, $rightExpression) {
|
||||
$this->leftExpression = $leftExpression;
|
||||
$this->rightExpression = $rightExpression;
|
||||
$this->keyword = $keyword;
|
||||
}
|
||||
|
||||
public function getLeftExp() { return $this->leftExpression; }
|
||||
public function getRightExp() { return $this->rightExpression; }
|
||||
public function getKeyword(): string { return $this->keyword; }
|
||||
}
|
||||
10
Core/Driver/SQL/Condition/CondLike.class.php
Normal file
10
Core/Driver/SQL/Condition/CondLike.class.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Condition;
|
||||
|
||||
class CondLike extends CondKeyword {
|
||||
|
||||
public function __construct($leftExpression, $rightExpression) {
|
||||
parent::__construct("LIKE", $leftExpression, $rightExpression);
|
||||
}
|
||||
}
|
||||
16
Core/Driver/SQL/Condition/CondNot.class.php
Normal file
16
Core/Driver/SQL/Condition/CondNot.class.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Condition;
|
||||
|
||||
class CondNot extends Condition {
|
||||
|
||||
private $expression; // string or condition
|
||||
|
||||
public function __construct($expression) {
|
||||
$this->expression = $expression;
|
||||
}
|
||||
|
||||
public function getExpression() {
|
||||
return $this->expression;
|
||||
}
|
||||
}
|
||||
14
Core/Driver/SQL/Condition/CondNull.class.php
Normal file
14
Core/Driver/SQL/Condition/CondNull.class.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Condition;
|
||||
|
||||
class CondNull extends Condition {
|
||||
|
||||
private string $column;
|
||||
|
||||
public function __construct(string $col) {
|
||||
$this->column = $col;
|
||||
}
|
||||
|
||||
public function getColumn(): string { return $this->column; }
|
||||
}
|
||||
14
Core/Driver/SQL/Condition/CondOr.class.php
Normal file
14
Core/Driver/SQL/Condition/CondOr.class.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Condition;
|
||||
|
||||
class CondOr extends Condition {
|
||||
|
||||
private array $conditions;
|
||||
|
||||
public function __construct(...$conditions) {
|
||||
$this->conditions = (!empty($conditions) && is_array($conditions[0])) ? $conditions[0] : $conditions;
|
||||
}
|
||||
|
||||
public function getConditions(): array { return $this->conditions; }
|
||||
}
|
||||
11
Core/Driver/SQL/Condition/CondRegex.class.php
Normal file
11
Core/Driver/SQL/Condition/CondRegex.class.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Condition;
|
||||
|
||||
class CondRegex extends CondKeyword {
|
||||
|
||||
public function __construct($leftExpression, $rightExpression) {
|
||||
parent::__construct("REGEXP", $leftExpression, $rightExpression);
|
||||
}
|
||||
|
||||
}
|
||||
9
Core/Driver/SQL/Condition/Condition.class.php
Normal file
9
Core/Driver/SQL/Condition/Condition.class.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Condition;
|
||||
|
||||
use Core\Driver\SQL\Expression\Expression;
|
||||
|
||||
abstract class Condition extends Expression {
|
||||
|
||||
}
|
||||
22
Core/Driver/SQL/Condition/Exists.class.php
Normal file
22
Core/Driver/SQL/Condition/Exists.class.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Core\Driver\SQL\Condition;
|
||||
|
||||
|
||||
use Core\Driver\SQL\Query\Select;
|
||||
|
||||
class Exists extends Condition
|
||||
{
|
||||
private Select $subQuery;
|
||||
|
||||
public function __construct(Select $subQuery)
|
||||
{
|
||||
$this->subQuery = $subQuery;
|
||||
}
|
||||
|
||||
public function getSubQuery(): Select
|
||||
{
|
||||
return $this->subQuery;
|
||||
}
|
||||
}
|
||||
18
Core/Driver/SQL/Constraint/Constraint.class.php
Normal file
18
Core/Driver/SQL/Constraint/Constraint.class.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Constraint;
|
||||
|
||||
abstract class Constraint {
|
||||
|
||||
private array $columnNames;
|
||||
private ?string $name;
|
||||
|
||||
public function __construct($columnNames, ?string $constraintName = NULL) {
|
||||
$this->columnNames = (!is_array($columnNames) ? array($columnNames) : $columnNames);
|
||||
$this->name = $constraintName;
|
||||
}
|
||||
|
||||
public function getColumnNames(): array { return $this->columnNames; }
|
||||
public function getName(): ?string { return $this->name; }
|
||||
public function setName(string $name) { $this->name = $name; }
|
||||
}
|
||||
23
Core/Driver/SQL/Constraint/ForeignKey.class.php
Normal file
23
Core/Driver/SQL/Constraint/ForeignKey.class.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Constraint;
|
||||
|
||||
use Core\Driver\SQL\Strategy\Strategy;
|
||||
|
||||
class ForeignKey extends Constraint {
|
||||
|
||||
private string $referencedTable;
|
||||
private string $referencedColumn;
|
||||
private ?Strategy $strategy;
|
||||
|
||||
public function __construct(string $columnName, string $refTable, string $refColumn, ?Strategy $strategy = NULL) {
|
||||
parent::__construct($columnName);
|
||||
$this->referencedTable = $refTable;
|
||||
$this->referencedColumn = $refColumn;
|
||||
$this->strategy = $strategy;
|
||||
}
|
||||
|
||||
public function getReferencedTable(): string { return $this->referencedTable; }
|
||||
public function getReferencedColumn(): string { return $this->referencedColumn; }
|
||||
public function onDelete(): ?Strategy { return $this->strategy; }
|
||||
}
|
||||
11
Core/Driver/SQL/Constraint/PrimaryKey.class.php
Normal file
11
Core/Driver/SQL/Constraint/PrimaryKey.class.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Constraint;
|
||||
|
||||
class PrimaryKey extends Constraint {
|
||||
|
||||
public function __construct(...$names) {
|
||||
parent::__construct((!empty($names) && is_array($names[0])) ? $names[0] : $names);
|
||||
}
|
||||
|
||||
}
|
||||
11
Core/Driver/SQL/Constraint/Unique.class.php
Normal file
11
Core/Driver/SQL/Constraint/Unique.class.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Constraint;
|
||||
|
||||
class Unique extends Constraint {
|
||||
|
||||
public function __construct(...$names) {
|
||||
parent::__construct((!empty($names) && is_array($names[0])) ? $names[0] : $names);
|
||||
}
|
||||
|
||||
}
|
||||
14
Core/Driver/SQL/Expression/Add.class.php
Normal file
14
Core/Driver/SQL/Expression/Add.class.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Expression;
|
||||
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
|
||||
# TODO: change confusing class inheritance here
|
||||
class Add extends Compare {
|
||||
|
||||
public function __construct(string $col, $val) {
|
||||
parent::__construct($col, $val, "+");
|
||||
}
|
||||
|
||||
}
|
||||
23
Core/Driver/SQL/Expression/CaseWhen.class.php
Normal file
23
Core/Driver/SQL/Expression/CaseWhen.class.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Expression;
|
||||
|
||||
use Core\Driver\SQL\Condition\Condition;
|
||||
|
||||
class CaseWhen extends Expression {
|
||||
|
||||
private Condition $condition;
|
||||
private $trueCase;
|
||||
private $falseCase;
|
||||
|
||||
public function __construct(Condition $condition, $trueCase, $falseCase) {
|
||||
$this->condition = $condition;
|
||||
$this->trueCase = $trueCase;
|
||||
$this->falseCase = $falseCase;
|
||||
}
|
||||
|
||||
public function getCondition(): Condition { return $this->condition; }
|
||||
public function getTrueCase() { return $this->trueCase; }
|
||||
public function getFalseCase() { return $this->falseCase; }
|
||||
|
||||
}
|
||||
7
Core/Driver/SQL/Expression/CurrentTimeStamp.class.php
Normal file
7
Core/Driver/SQL/Expression/CurrentTimeStamp.class.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Expression;
|
||||
|
||||
class CurrentTimeStamp extends Expression {
|
||||
|
||||
}
|
||||
23
Core/Driver/SQL/Expression/DateAdd.class.php
Normal file
23
Core/Driver/SQL/Expression/DateAdd.class.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Expression;
|
||||
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
class DateAdd extends Expression {
|
||||
|
||||
private Expression $lhs;
|
||||
private Expression $rhs;
|
||||
private string $unit;
|
||||
|
||||
public function __construct(Expression $lhs, Expression $rhs, string $unit) {
|
||||
$this->lhs = $lhs;
|
||||
$this->rhs = $rhs;
|
||||
$this->unit = $unit;
|
||||
}
|
||||
|
||||
public function getLHS(): Expression { return $this->lhs; }
|
||||
public function getRHS(): Expression { return $this->rhs; }
|
||||
public function getUnit(): string { return $this->unit; }
|
||||
|
||||
}
|
||||
23
Core/Driver/SQL/Expression/DateSub.class.php
Normal file
23
Core/Driver/SQL/Expression/DateSub.class.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Expression;
|
||||
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
class DateSub extends Expression {
|
||||
|
||||
private Expression $lhs;
|
||||
private Expression $rhs;
|
||||
private string $unit;
|
||||
|
||||
public function __construct(Expression $lhs, Expression $rhs, string $unit) {
|
||||
$this->lhs = $lhs;
|
||||
$this->rhs = $rhs;
|
||||
$this->unit = $unit;
|
||||
}
|
||||
|
||||
public function getLHS(): Expression { return $this->lhs; }
|
||||
public function getRHS(): Expression { return $this->rhs; }
|
||||
public function getUnit(): string { return $this->unit; }
|
||||
|
||||
}
|
||||
9
Core/Driver/SQL/Expression/Expression.class.php
Normal file
9
Core/Driver/SQL/Expression/Expression.class.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Expression;
|
||||
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
abstract class Expression {
|
||||
|
||||
}
|
||||
18
Core/Driver/SQL/Expression/JsonArrayAgg.class.php
Normal file
18
Core/Driver/SQL/Expression/JsonArrayAgg.class.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Expression;
|
||||
|
||||
class JsonArrayAgg extends Expression {
|
||||
|
||||
private $value;
|
||||
private string $alias;
|
||||
|
||||
public function __construct($value, string $alias) {
|
||||
$this->value = $value;
|
||||
$this->alias = $alias;
|
||||
}
|
||||
|
||||
public function getValue() { return $this->value; }
|
||||
public function getAlias(): string { return $this->alias; }
|
||||
|
||||
}
|
||||
18
Core/Driver/SQL/Expression/Sum.class.php
Normal file
18
Core/Driver/SQL/Expression/Sum.class.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Expression;
|
||||
|
||||
class Sum extends Expression {
|
||||
|
||||
private $value;
|
||||
private string $alias;
|
||||
|
||||
public function __construct($value, string $alias) {
|
||||
$this->value = $value;
|
||||
$this->alias = $alias;
|
||||
}
|
||||
|
||||
public function getValue() { return $this->value; }
|
||||
public function getAlias(): string { return $this->alias; }
|
||||
|
||||
}
|
||||
34
Core/Driver/SQL/Join.class.php
Normal file
34
Core/Driver/SQL/Join.class.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL;
|
||||
|
||||
use Core\Driver\SQL\Column\Column;
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
|
||||
class Join {
|
||||
|
||||
private string $type;
|
||||
private string $table;
|
||||
private string $columnA;
|
||||
private string $columnB;
|
||||
private ?string $tableAlias;
|
||||
private array $conditions;
|
||||
|
||||
public function __construct(string $type, string $table, string $columnA, string $columnB, ?string $tableAlias = null, array $conditions = []) {
|
||||
$this->type = $type;
|
||||
$this->table = $table;
|
||||
$this->columnA = $columnA;
|
||||
$this->columnB = $columnB;
|
||||
$this->tableAlias = $tableAlias;
|
||||
$this->conditions = $conditions;
|
||||
array_unshift($this->conditions , new Compare($columnA, new Column($columnB), "="));
|
||||
}
|
||||
|
||||
public function getType(): string { return $this->type; }
|
||||
public function getTable(): string { return $this->table; }
|
||||
public function getColumnA(): string { return $this->columnA; }
|
||||
public function getColumnB(): string { return $this->columnB; }
|
||||
public function getTableAlias(): ?string { return $this->tableAlias; }
|
||||
public function getConditions(): array { return $this->conditions; }
|
||||
|
||||
}
|
||||
17
Core/Driver/SQL/Keyword.class.php
Normal file
17
Core/Driver/SQL/Keyword.class.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL;
|
||||
|
||||
use Core\Driver\SQL\Expression\Expression;
|
||||
|
||||
class Keyword extends Expression {
|
||||
|
||||
private string $value;
|
||||
|
||||
public function __construct(string $value) {
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
public function getValue(): string { return $this->value; }
|
||||
|
||||
}
|
||||
520
Core/Driver/SQL/MySQL.class.php
Normal file
520
Core/Driver/SQL/MySQL.class.php
Normal file
@@ -0,0 +1,520 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL;
|
||||
|
||||
use Core\API\Parameter\Parameter;
|
||||
|
||||
use DateTime;
|
||||
use Core\Driver\SQL\Column\Column;
|
||||
use Core\Driver\SQL\Column\IntColumn;
|
||||
use Core\Driver\SQL\Column\NumericColumn;
|
||||
use Core\Driver\SQL\Column\SerialColumn;
|
||||
use Core\Driver\SQL\Column\StringColumn;
|
||||
use Core\Driver\SQL\Column\EnumColumn;
|
||||
use Core\Driver\SQL\Column\DateTimeColumn;
|
||||
use Core\Driver\SQL\Column\BoolColumn;
|
||||
use Core\Driver\SQL\Column\JsonColumn;
|
||||
|
||||
use Core\Driver\SQL\Expression\Add;
|
||||
use Core\Driver\SQL\Expression\CurrentTimeStamp;
|
||||
use Core\Driver\SQL\Expression\DateAdd;
|
||||
use Core\Driver\SQL\Expression\DateSub;
|
||||
use Core\Driver\SQL\Expression\Expression;
|
||||
use Core\Driver\SQL\Expression\JsonArrayAgg;
|
||||
use Core\Driver\SQL\Query\CreateProcedure;
|
||||
use Core\Driver\SQL\Query\CreateTrigger;
|
||||
use Core\Driver\SQL\Query\Query;
|
||||
use Core\Driver\SQL\Strategy\Strategy;
|
||||
use Core\Driver\SQL\Strategy\UpdateStrategy;
|
||||
use Core\Driver\SQL\Type\CurrentColumn;
|
||||
use Core\Driver\SQL\Type\CurrentTable;
|
||||
use Core\Driver\SQL\Type\Trigger;
|
||||
|
||||
class MySQL extends SQL {
|
||||
|
||||
public function __construct($connectionData) {
|
||||
parent::__construct($connectionData);
|
||||
}
|
||||
|
||||
public function checkRequirements() {
|
||||
return function_exists('mysqli_connect');
|
||||
}
|
||||
|
||||
public function getDriverName() {
|
||||
return 'mysqli';
|
||||
}
|
||||
|
||||
// Connection Management
|
||||
public function connect() {
|
||||
|
||||
if (!is_null($this->connection)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@$this->connection = mysqli_connect(
|
||||
$this->connectionData->getHost(),
|
||||
$this->connectionData->getLogin(),
|
||||
$this->connectionData->getPassword(),
|
||||
$this->connectionData->getProperty('database'),
|
||||
$this->connectionData->getPort()
|
||||
);
|
||||
|
||||
if (mysqli_connect_errno()) {
|
||||
$this->lastError = $this->logger->severe("Failed to connect to MySQL: " . mysqli_connect_error());
|
||||
$this->connection = NULL;
|
||||
return false;
|
||||
}
|
||||
|
||||
mysqli_set_charset($this->connection, $this->connectionData->getProperty('encoding', 'UTF8'));
|
||||
return true;
|
||||
}
|
||||
|
||||
public function disconnect() {
|
||||
if (is_null($this->connection)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
mysqli_close($this->connection);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getLastError(): string {
|
||||
$lastError = parent::getLastError();
|
||||
if (empty($lastError)) {
|
||||
$lastError = mysqli_error($this->connection);
|
||||
}
|
||||
|
||||
return $lastError;
|
||||
}
|
||||
|
||||
private function getPreparedParams($values): array {
|
||||
$sqlParams = array('');
|
||||
foreach ($values as $value) {
|
||||
$paramType = Parameter::parseType($value);
|
||||
switch ($paramType) {
|
||||
case Parameter::TYPE_BOOLEAN:
|
||||
$value = $value ? 1 : 0;
|
||||
$sqlParams[0] .= 'i';
|
||||
break;
|
||||
case Parameter::TYPE_INT:
|
||||
$sqlParams[0] .= 'i';
|
||||
break;
|
||||
case Parameter::TYPE_FLOAT:
|
||||
$sqlParams[0] .= 'd';
|
||||
break;
|
||||
case Parameter::TYPE_DATE:
|
||||
if ($value instanceof DateTime) {
|
||||
$value = $value->format('Y-m-d');
|
||||
}
|
||||
$sqlParams[0] .= 's';
|
||||
break;
|
||||
case Parameter::TYPE_TIME:
|
||||
if ($value instanceof DateTime) {
|
||||
$value = $value->format('H:i:s');
|
||||
}
|
||||
$sqlParams[0] .= 's';
|
||||
break;
|
||||
case Parameter::TYPE_DATE_TIME:
|
||||
if ($value instanceof DateTime) {
|
||||
$value = $value->format('Y-m-d H:i:s');
|
||||
}
|
||||
$sqlParams[0] .= 's';
|
||||
break;
|
||||
case Parameter::TYPE_ARRAY:
|
||||
$value = json_encode($value);
|
||||
$sqlParams[0] .= 's';
|
||||
break;
|
||||
case Parameter::TYPE_EMAIL:
|
||||
default:
|
||||
$sqlParams[0] .= 's';
|
||||
}
|
||||
|
||||
$sqlParams[] = $value;
|
||||
}
|
||||
|
||||
return $sqlParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
protected function execute($query, $values = NULL, int $fetchType = self::FETCH_NONE) {
|
||||
|
||||
$result = null;
|
||||
$this->lastError = "";
|
||||
$stmt = null;
|
||||
$res = null;
|
||||
$success = false;
|
||||
|
||||
try {
|
||||
if (empty($values)) {
|
||||
$res = mysqli_query($this->connection, $query);
|
||||
$success = ($res !== FALSE);
|
||||
if ($success) {
|
||||
switch ($fetchType) {
|
||||
case self::FETCH_NONE:
|
||||
$result = true;
|
||||
break;
|
||||
case self::FETCH_ONE:
|
||||
$result = $res->fetch_assoc();
|
||||
break;
|
||||
case self::FETCH_ALL:
|
||||
$result = $res->fetch_all(MYSQLI_ASSOC);
|
||||
break;
|
||||
case self::FETCH_ITERATIVE:
|
||||
$result = new RowIteratorMySQL($res);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if ($stmt = $this->connection->prepare($query)) {
|
||||
|
||||
$sqlParams = $this->getPreparedParams($values);
|
||||
if ($stmt->bind_param(...$sqlParams)) {
|
||||
if ($stmt->execute()) {
|
||||
if ($fetchType === self::FETCH_NONE) {
|
||||
$result = true;
|
||||
$success = true;
|
||||
} else {
|
||||
$res = $stmt->get_result();
|
||||
if ($res) {
|
||||
switch ($fetchType) {
|
||||
case self::FETCH_ONE:
|
||||
$result = $res->fetch_assoc();
|
||||
break;
|
||||
case self::FETCH_ALL:
|
||||
$result = $res->fetch_all(MYSQLI_ASSOC);
|
||||
break;
|
||||
case self::FETCH_ITERATIVE:
|
||||
$result = new RowIteratorMySQL($res);
|
||||
break;
|
||||
}
|
||||
$success = true;
|
||||
} else {
|
||||
$this->lastError = $this->logger->error("PreparedStatement::get_result failed: $stmt->error ($stmt->errno)");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->lastError = $this->logger->error("PreparedStatement::execute failed: $stmt->error ($stmt->errno)");
|
||||
}
|
||||
} else {
|
||||
$this->lastError = $this->logger->error("PreparedStatement::prepare failed: $stmt->error ($stmt->errno)");
|
||||
}
|
||||
}
|
||||
} catch (\mysqli_sql_exception $exception) {
|
||||
$this->lastError = $this->logger->error("MySQL::execute failed: $stmt->error ($stmt->errno)");
|
||||
} finally {
|
||||
|
||||
if ($res !== null && !is_bool($res) && $fetchType !== self::FETCH_ITERATIVE) {
|
||||
$res->close();
|
||||
}
|
||||
|
||||
if ($stmt !== null && !is_bool($stmt)) {
|
||||
$stmt->close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $success ? $result : false;
|
||||
}
|
||||
|
||||
public function getOnDuplicateStrategy(?Strategy $strategy, &$params): ?string {
|
||||
if (is_null($strategy)) {
|
||||
return "";
|
||||
} else if ($strategy instanceof UpdateStrategy) {
|
||||
$updateValues = array();
|
||||
foreach ($strategy->getValues() as $key => $value) {
|
||||
$leftColumn = $this->columnName($key);
|
||||
if ($value instanceof Column) {
|
||||
$columnName = $this->columnName($value->getName());
|
||||
$updateValues[] = "$leftColumn=VALUES($columnName)";
|
||||
} else if ($value instanceof Add) {
|
||||
$columnName = $this->columnName($value->getColumn());
|
||||
$operator = $value->getOperator();
|
||||
$value = $value->getValue();
|
||||
$updateValues[] = "$leftColumn=$columnName$operator" . $this->addValue($value, $params);
|
||||
} else {
|
||||
$updateValues[] = "$leftColumn=" . $this->addValue($value, $params);
|
||||
}
|
||||
}
|
||||
|
||||
return " ON DUPLICATE KEY UPDATE " . implode(",", $updateValues);
|
||||
} else {
|
||||
$strategyClass = get_class($strategy);
|
||||
$this->lastError = $this->logger->error("ON DUPLICATE Strategy $strategyClass is not supported yet.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected function fetchReturning($res, string $returningCol) {
|
||||
$this->lastInsertId = mysqli_insert_id($this->connection);
|
||||
}
|
||||
|
||||
public function getColumnType(Column $column): ?string {
|
||||
if ($column instanceof StringColumn) {
|
||||
$maxSize = $column->getMaxSize();
|
||||
if ($maxSize) {
|
||||
return "VARCHAR($maxSize)";
|
||||
} else {
|
||||
return "TEXT";
|
||||
}
|
||||
} else if ($column instanceof SerialColumn) {
|
||||
return "INTEGER AUTO_INCREMENT";
|
||||
} else if ($column instanceof IntColumn) {
|
||||
$unsigned = $column->isUnsigned() ? " UNSIGNED" : "";
|
||||
return $column->getType() . $unsigned;
|
||||
} else if ($column instanceof DateTimeColumn) {
|
||||
return "DATETIME";
|
||||
} else if ($column instanceof BoolColumn) {
|
||||
return "BOOLEAN";
|
||||
} else if ($column instanceof JsonColumn) {
|
||||
return "LONGTEXT"; # some maria db setups don't allow JSON here…
|
||||
} else if ($column instanceof NumericColumn) {
|
||||
$digitsTotal = $column->getTotalDigits();
|
||||
$digitsDecimal = $column->getDecimalDigits();
|
||||
$type = $column->getTypeName();
|
||||
if ($digitsTotal !== null) {
|
||||
if ($digitsDecimal !== null) {
|
||||
return "$type($digitsTotal,$digitsDecimal)";
|
||||
} else {
|
||||
return "$type($digitsTotal)";
|
||||
}
|
||||
} else {
|
||||
return $type;
|
||||
}
|
||||
} else {
|
||||
$this->lastError = $this->logger->error("Unsupported Column Type: " . get_class($column));
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
public function getColumnDefinition(Column $column): ?string {
|
||||
$columnName = $this->columnName($column->getName());
|
||||
$defaultValue = $column->getDefaultValue();
|
||||
if ($column instanceof EnumColumn) { // check this, shouldn't it be in getColumnType?
|
||||
$values = array();
|
||||
foreach ($column->getValues() as $value) {
|
||||
$values[] = $this->getValueDefinition($value);
|
||||
}
|
||||
|
||||
$values = implode(",", $values);
|
||||
$type = "ENUM($values)";
|
||||
} else {
|
||||
$type = $this->getColumnType($column);
|
||||
if (!$type) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($type === "LONGTEXT") {
|
||||
$defaultValue = NULL; # must be null :(
|
||||
}
|
||||
|
||||
$notNull = $column->notNull() ? " NOT NULL" : "";
|
||||
if (!is_null($defaultValue) || !$column->notNull()) {
|
||||
$defaultValue = " DEFAULT " . $this->getValueDefinition($defaultValue);
|
||||
} else {
|
||||
$defaultValue = "";
|
||||
}
|
||||
|
||||
return "$columnName $type$notNull$defaultValue";
|
||||
}
|
||||
|
||||
public function getValueDefinition($value) {
|
||||
if (is_numeric($value)) {
|
||||
return $value;
|
||||
} else if (is_bool($value)) {
|
||||
return $value ? "TRUE" : "FALSE";
|
||||
} else if (is_null($value)) {
|
||||
return "NULL";
|
||||
} else if ($value instanceof Keyword) {
|
||||
return $value->getValue();
|
||||
} else if ($value instanceof CurrentTimeStamp) {
|
||||
return "CURRENT_TIMESTAMP";
|
||||
} else {
|
||||
$str = addslashes($value);
|
||||
return "'$str'";
|
||||
}
|
||||
}
|
||||
|
||||
public function addValue($val, &$params = NULL, bool $unsafe = false) {
|
||||
if ($val instanceof Keyword) {
|
||||
return $val->getValue();
|
||||
} else if ($val instanceof CurrentColumn) {
|
||||
return $val->getName();
|
||||
} else if ($val instanceof Column) {
|
||||
return $this->columnName($val->getName());
|
||||
} else if ($val instanceof Expression) {
|
||||
return $this->createExpression($val, $params);
|
||||
} else {
|
||||
if ($unsafe) {
|
||||
return $this->getUnsafeValue($val);
|
||||
} else {
|
||||
$params[] = $val;
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function tableName($table): string {
|
||||
if (is_array($table)) {
|
||||
$tables = array();
|
||||
foreach ($table as $t) $tables[] = $this->tableName($t);
|
||||
return implode(",", $tables);
|
||||
} else {
|
||||
$parts = explode(" ", $table);
|
||||
if (count($parts) === 2) {
|
||||
list ($name, $alias) = $parts;
|
||||
return "`$name` $alias";
|
||||
} else {
|
||||
return "`$table`";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function columnName($col): string {
|
||||
if ($col instanceof Keyword) {
|
||||
return $col->getValue();
|
||||
} elseif (is_array($col)) {
|
||||
$columns = array();
|
||||
foreach ($col as $c) $columns[] = $this->columnName($c);
|
||||
return implode(",", $columns);
|
||||
} else {
|
||||
if (($index = strrpos($col, ".")) !== FALSE) {
|
||||
$tableName = $this->tableName(substr($col, 0, $index));
|
||||
$columnName = $this->columnName(substr($col, $index + 1));
|
||||
return "$tableName.$columnName";
|
||||
} else if (($index = stripos($col, " as ")) !== FALSE) {
|
||||
$columnName = $this->columnName(trim(substr($col, 0, $index)));
|
||||
$alias = trim(substr($col, $index + 4));
|
||||
return "$columnName as $alias";
|
||||
} else {
|
||||
return "`$col`";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getStatus() {
|
||||
return mysqli_stat($this->connection);
|
||||
}
|
||||
|
||||
public function createTriggerBody(CreateTrigger $trigger, array $parameters = []): ?string {
|
||||
$values = array();
|
||||
|
||||
foreach ($parameters as $paramValue) {
|
||||
if ($paramValue instanceof CurrentTable) {
|
||||
$values[] = $this->getUnsafeValue($trigger->getTable());
|
||||
} elseif ($paramValue instanceof CurrentColumn) {
|
||||
$prefix = ($trigger->getEvent() !== "DELETE" ? "NEW." : "OLD.");
|
||||
$values[] = $this->columnName($prefix . $paramValue->getName());
|
||||
} else {
|
||||
$values[] = $paramValue;
|
||||
}
|
||||
}
|
||||
|
||||
$procName = $trigger->getProcedure()->getName();
|
||||
$procParameters = implode(",", $values);
|
||||
return "CALL $procName($procParameters)";
|
||||
}
|
||||
|
||||
private function getParameterDefinition(Column $parameter, bool $out = false): string {
|
||||
$out = ($out ? "OUT" : "IN");
|
||||
$name = $parameter->getName();
|
||||
$type = $this->getColumnType($parameter);
|
||||
return "$out $name $type";
|
||||
}
|
||||
|
||||
public function getProcedureHead(CreateProcedure $procedure): ?string {
|
||||
$name = $procedure->getName();
|
||||
$returns = $procedure->getReturns();
|
||||
$paramDefs = [];
|
||||
|
||||
foreach ($procedure->getParameters() as $param) {
|
||||
if ($param instanceof Column) {
|
||||
$paramDefs[] = $this->getParameterDefinition($param);
|
||||
} else {
|
||||
$this->lastError = $this->logger->error("PROCEDURE parameter type " . gettype($returns) . " is not implemented yet");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($returns) {
|
||||
if ($returns instanceof Column) {
|
||||
$paramDefs[] = $this->getParameterDefinition($returns, true);
|
||||
} else if (!($returns instanceof Trigger)) { // mysql does not need to return triggers here
|
||||
$this->lastError = $this->logger->error("PROCEDURE RETURN type " . gettype($returns) . " is not implemented yet");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$paramDefs = implode(",", $paramDefs);
|
||||
return "CREATE PROCEDURE $name($paramDefs)";
|
||||
}
|
||||
|
||||
protected function buildUnsafe(Query $statement): string {
|
||||
$params = [];
|
||||
$query = $statement->build($params);
|
||||
|
||||
foreach ($params as $value) {
|
||||
$query = preg_replace("?", $this->getUnsafeValue($value), $query, 1);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
protected function createExpression(Expression $exp, array &$params): ?string {
|
||||
if ($exp instanceof DateAdd || $exp instanceof DateSub) {
|
||||
$lhs = $this->addValue($exp->getLHS(), $params);
|
||||
$rhs = $this->addValue($exp->getRHS(), $params);
|
||||
$unit = $exp->getUnit();
|
||||
$dateFunction = ($exp instanceof DateAdd ? "DATE_ADD" : "DATE_SUB");
|
||||
return "$dateFunction($lhs, INTERVAL $rhs $unit)";
|
||||
} else if ($exp instanceof CurrentTimeStamp) {
|
||||
return "NOW()";
|
||||
} else if ($exp instanceof JsonArrayAgg) {
|
||||
$value = $this->addValue($exp->getValue(), $params);
|
||||
$alias = $this->columnName($exp->getAlias());
|
||||
return "JSON_ARRAYAGG($value) as $alias";
|
||||
} else {
|
||||
return parent::createExpression($exp, $params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RowIteratorMySQL extends RowIterator {
|
||||
|
||||
public function __construct($resultSet, bool $useCache = false) {
|
||||
parent::__construct($resultSet, $useCache);
|
||||
}
|
||||
|
||||
protected function getNumRows(): int {
|
||||
return $this->resultSet->num_rows;
|
||||
}
|
||||
|
||||
protected function fetchRow(int $index): array {
|
||||
// check if we already fetched that row
|
||||
if (!$this->useCache || $index >= count($this->fetchedRows)) {
|
||||
// if not, fetch it from the result set
|
||||
$row = $this->resultSet->fetch_assoc();
|
||||
if ($this->useCache) {
|
||||
$this->fetchedRows[] = $row;
|
||||
}
|
||||
|
||||
// close result set, after everything's fetched
|
||||
if ($index >= $this->numRows - 1) {
|
||||
$this->resultSet->close();
|
||||
}
|
||||
} else {
|
||||
$row = $this->fetchedRows[$index];
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
public function rewind() {
|
||||
if ($this->useCache) {
|
||||
$this->rowIndex = 0;
|
||||
} else if ($this->rowIndex !== 0) {
|
||||
throw new \Exception("RowIterator::rewind() not supported, when caching is disabled");
|
||||
}
|
||||
}
|
||||
}
|
||||
497
Core/Driver/SQL/PostgreSQL.class.php
Normal file
497
Core/Driver/SQL/PostgreSQL.class.php
Normal file
@@ -0,0 +1,497 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL;
|
||||
|
||||
use Core\API\Parameter\Parameter;
|
||||
|
||||
use Core\Driver\SQL\Column\Column;
|
||||
use Core\Driver\SQL\Column\IntColumn;
|
||||
use Core\Driver\SQL\Column\NumericColumn;
|
||||
use Core\Driver\SQL\Column\SerialColumn;
|
||||
use Core\Driver\SQL\Column\StringColumn;
|
||||
use Core\Driver\SQL\Column\EnumColumn;
|
||||
use Core\Driver\SQL\Column\DateTimeColumn;
|
||||
use Core\Driver\SQL\Column\BoolColumn;
|
||||
use Core\Driver\SQL\Column\JsonColumn;
|
||||
|
||||
use Core\Driver\SQL\Condition\CondRegex;
|
||||
use Core\Driver\SQL\Expression\Add;
|
||||
use Core\Driver\SQL\Expression\CurrentTimeStamp;
|
||||
use Core\Driver\SQL\Expression\DateAdd;
|
||||
use Core\Driver\SQL\Expression\DateSub;
|
||||
use Core\Driver\SQL\Expression\Expression;
|
||||
use Core\Driver\SQL\Expression\JsonArrayAgg;
|
||||
use Core\Driver\SQL\Query\CreateProcedure;
|
||||
use Core\Driver\SQL\Query\CreateTrigger;
|
||||
use Core\Driver\SQL\Query\Insert;
|
||||
use Core\Driver\SQL\Query\Query;
|
||||
use Core\Driver\SQL\Strategy\Strategy;
|
||||
use Core\Driver\SQL\Strategy\UpdateStrategy;
|
||||
use Core\Driver\SQL\Type\CurrentColumn;
|
||||
use Core\Driver\SQL\Type\CurrentTable;
|
||||
use Core\Driver\SQL\Type\Trigger;
|
||||
|
||||
class PostgreSQL extends SQL {
|
||||
|
||||
public function __construct($connectionData) {
|
||||
parent::__construct($connectionData);
|
||||
}
|
||||
|
||||
public function checkRequirements() {
|
||||
return function_exists('pg_connect');
|
||||
}
|
||||
|
||||
public function getDriverName() {
|
||||
return 'pgsql';
|
||||
}
|
||||
|
||||
// Connection Management
|
||||
public function connect() {
|
||||
if (!is_null($this->connection)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$config = array(
|
||||
"host" => $this->connectionData->getHost(),
|
||||
"port" => $this->connectionData->getPort(),
|
||||
"dbname" => $this->connectionData->getProperty('database', 'public'),
|
||||
"user" => $this->connectionData->getLogin(),
|
||||
"password" => $this->connectionData->getPassword()
|
||||
);
|
||||
|
||||
$connectionString = array();
|
||||
foreach ($config as $key => $val) {
|
||||
if (!empty($val)) {
|
||||
$connectionString[] = "$key=$val";
|
||||
}
|
||||
}
|
||||
|
||||
$this->connection = @pg_connect(implode(" ", $connectionString), PGSQL_CONNECT_FORCE_NEW);
|
||||
if (!$this->connection) {
|
||||
$this->lastError = $this->logger->severe("Failed to connect to Database");
|
||||
$this->connection = NULL;
|
||||
return false;
|
||||
}
|
||||
|
||||
pg_set_client_encoding($this->connection, $this->connectionData->getProperty('encoding', 'UTF-8'));
|
||||
return true;
|
||||
}
|
||||
|
||||
public function disconnect() {
|
||||
if (is_null($this->connection))
|
||||
return;
|
||||
|
||||
@pg_close($this->connection);
|
||||
}
|
||||
|
||||
public function getLastError(): string {
|
||||
$lastError = parent::getLastError();
|
||||
if (empty($lastError)) {
|
||||
$lastError = trim(pg_last_error($this->connection) . " " . pg_last_error($this->connection));
|
||||
}
|
||||
|
||||
return $lastError;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
protected function execute($query, $values = NULL, int $fetchType = self::FETCH_NONE) {
|
||||
|
||||
$this->lastError = "";
|
||||
$stmt_name = uniqid();
|
||||
$pgParams = array();
|
||||
|
||||
if (!is_null($values)) {
|
||||
foreach ($values as $value) {
|
||||
$paramType = Parameter::parseType($value);
|
||||
switch ($paramType) {
|
||||
case Parameter::TYPE_DATE:
|
||||
$value = $value->format("Y-m-d");
|
||||
break;
|
||||
case Parameter::TYPE_TIME:
|
||||
$value = $value->format("H:i:s");
|
||||
break;
|
||||
case Parameter::TYPE_DATE_TIME:
|
||||
$value = $value->format("Y-m-d H:i:s");
|
||||
break;
|
||||
case Parameter::TYPE_ARRAY:
|
||||
$value = json_encode($value);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
$pgParams[] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = @pg_prepare($this->connection, $stmt_name, $query);
|
||||
if ($stmt === FALSE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$result = @pg_execute($this->connection, $stmt_name, $pgParams);
|
||||
if ($result === FALSE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch ($fetchType) {
|
||||
case self::FETCH_NONE:
|
||||
return true;
|
||||
case self::FETCH_ONE:
|
||||
return pg_fetch_assoc($result);
|
||||
case self::FETCH_ALL:
|
||||
$rows = pg_fetch_all($result);
|
||||
if ($rows === FALSE) {
|
||||
if (empty(trim($this->getLastError()))) {
|
||||
$rows = array();
|
||||
}
|
||||
}
|
||||
return $rows;
|
||||
case self::FETCH_ITERATIVE:
|
||||
return new RowIteratorPostgreSQL($result);
|
||||
}
|
||||
}
|
||||
|
||||
public function getOnDuplicateStrategy(?Strategy $strategy, &$params): ?string {
|
||||
if (!is_null($strategy)) {
|
||||
if ($strategy instanceof UpdateStrategy) {
|
||||
$updateValues = array();
|
||||
foreach ($strategy->getValues() as $key => $value) {
|
||||
$leftColumn = $this->columnName($key);
|
||||
if ($value instanceof Column) {
|
||||
$columnName = $this->columnName($value->getName());
|
||||
$updateValues[] = "$leftColumn=EXCLUDED.$columnName";
|
||||
} else if ($value instanceof Add) {
|
||||
$columnName = $this->columnName($value->getColumn());
|
||||
$operator = $value->getOperator();
|
||||
$value = $value->getValue();
|
||||
$updateValues[] = "$leftColumn=$columnName$operator" . $this->addValue($value, $params);
|
||||
} else {
|
||||
$updateValues[] = "$leftColumn=" . $this->addValue($value, $parameters);
|
||||
}
|
||||
}
|
||||
|
||||
$conflictingColumns = $this->columnName($strategy->getConflictingColumns());
|
||||
$updateValues = implode(",", $updateValues);
|
||||
return " ON CONFLICT ($conflictingColumns) DO UPDATE SET $updateValues";
|
||||
} else {
|
||||
$strategyClass = get_class($strategy);
|
||||
$this->lastError = $this->logger->error("ON DUPLICATE Strategy $strategyClass is not supported yet.");
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public function getReturning(?string $columns): string {
|
||||
return $columns ? (" RETURNING " . $this->columnName($columns)) : "";
|
||||
}
|
||||
|
||||
public function executeQuery(Query $query, int $fetchType = self::FETCH_NONE) {
|
||||
|
||||
if ($query instanceof Insert && !empty($query->getReturning())) {
|
||||
$fetchType = self::FETCH_ONE;
|
||||
}
|
||||
|
||||
return parent::executeQuery($query, $fetchType);
|
||||
}
|
||||
|
||||
protected function fetchReturning($res, string $returningCol) {
|
||||
$this->lastInsertId = $res[0][$returningCol];
|
||||
}
|
||||
|
||||
// UGLY but.. what should i do?
|
||||
private function createEnum(EnumColumn $enumColumn, string $typeName): string {
|
||||
$values = array();
|
||||
foreach ($enumColumn->getValues() as $value) {
|
||||
$values[] = $this->getValueDefinition($value);
|
||||
}
|
||||
|
||||
$values = implode(",", $values);
|
||||
$query =
|
||||
"DO $$ BEGIN
|
||||
CREATE TYPE \"$typeName\" AS ENUM ($values);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;";
|
||||
|
||||
return $this->execute($query);
|
||||
}
|
||||
|
||||
public function getColumnType(Column $column): ?string {
|
||||
if ($column instanceof StringColumn) {
|
||||
$maxSize = $column->getMaxSize();
|
||||
if ($maxSize) {
|
||||
return "VARCHAR($maxSize)";
|
||||
} else {
|
||||
return "TEXT";
|
||||
}
|
||||
} else if ($column instanceof SerialColumn) {
|
||||
return "SERIAL";
|
||||
} else if ($column instanceof IntColumn) {
|
||||
return $column->getType();
|
||||
} else if ($column instanceof DateTimeColumn) {
|
||||
return "TIMESTAMP";
|
||||
} else if ($column instanceof EnumColumn) {
|
||||
$typeName = $column->getName();
|
||||
if (!endsWith($typeName, "_type")) {
|
||||
$typeName = "${typeName}_type";
|
||||
}
|
||||
return $typeName;
|
||||
} else if ($column instanceof BoolColumn) {
|
||||
return "BOOLEAN";
|
||||
} else if ($column instanceof JsonColumn) {
|
||||
return "JSON";
|
||||
} else if ($column instanceof NumericColumn) {
|
||||
$digitsDecimal = $column->getDecimalDigits();
|
||||
$type = $column->getTypeName();
|
||||
if ($digitsDecimal !== null) {
|
||||
if ($type === "double") {
|
||||
$type = "float"; // postgres doesn't know about double :/
|
||||
}
|
||||
return "$type($digitsDecimal)";
|
||||
} else {
|
||||
return $type;
|
||||
}
|
||||
} else {
|
||||
$this->lastError = $this->logger->error("Unsupported Column Type: " . get_class($column));
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
public function getColumnDefinition($column): ?string {
|
||||
$columnName = $this->columnName($column->getName());
|
||||
|
||||
$type = $this->getColumnType($column);
|
||||
if (!$type) {
|
||||
return null;
|
||||
} else if ($column instanceof EnumColumn) {
|
||||
if (!$this->createEnum($column, $type)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$notNull = $column->notNull() ? " NOT NULL" : "";
|
||||
$defaultValue = "";
|
||||
if (!is_null($column->getDefaultValue()) || !$column->notNull()) {
|
||||
$defaultValue = " DEFAULT " . $this->getValueDefinition($column->getDefaultValue());
|
||||
}
|
||||
|
||||
return "$columnName $type$notNull$defaultValue";
|
||||
}
|
||||
|
||||
protected function getValueDefinition($value) {
|
||||
if (is_numeric($value)) {
|
||||
return $value;
|
||||
} else if (is_bool($value)) {
|
||||
return $value ? "TRUE" : "FALSE";
|
||||
} else if (is_null($value)) {
|
||||
return "NULL";
|
||||
} else if ($value instanceof Keyword) {
|
||||
return $value->getValue();
|
||||
} else if ($value instanceof CurrentTimeStamp) {
|
||||
return "CURRENT_TIMESTAMP";
|
||||
} else {
|
||||
$str = str_replace("'", "''", $value);
|
||||
return "'$str'";
|
||||
}
|
||||
}
|
||||
|
||||
public function addValue($val, &$params = NULL, bool $unsafe = false) {
|
||||
if ($val instanceof Keyword) {
|
||||
return $val->getValue();
|
||||
} else if ($val instanceof CurrentTable) {
|
||||
return "TG_TABLE_NAME";
|
||||
} else if ($val instanceof CurrentColumn) {
|
||||
return "NEW." . $this->columnName($val->getName());
|
||||
} else if ($val instanceof Column) {
|
||||
return $this->columnName($val->getName());
|
||||
} else if ($val instanceof Expression) {
|
||||
return $this->createExpression($val, $params);
|
||||
} else {
|
||||
if ($unsafe) {
|
||||
return $this->getUnsafeValue($val);
|
||||
} else {
|
||||
$params[] = is_bool($val) ? ($val ? "TRUE" : "FALSE") : $val;
|
||||
return '$' . count($params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function tableName($table): string {
|
||||
if (is_array($table)) {
|
||||
$tables = array();
|
||||
foreach ($table as $t) $tables[] = $this->tableName($t);
|
||||
return implode(",", $tables);
|
||||
} else {
|
||||
$parts = explode(" ", $table);
|
||||
if (count($parts) === 2) {
|
||||
list ($name, $alias) = $parts;
|
||||
return "\"$name\" $alias";
|
||||
} else {
|
||||
return "\"$table\"";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function columnName($col): string {
|
||||
if ($col instanceof KeyWord) {
|
||||
return $col->getValue();
|
||||
} elseif (is_array($col)) {
|
||||
$columns = array_map(function ($c) {
|
||||
return $this->columnName($c);
|
||||
}, $col);
|
||||
return implode(",", $columns);
|
||||
} else {
|
||||
if (($index = strrpos($col, ".")) !== FALSE) {
|
||||
$tableName = $this->tableName(substr($col, 0, $index));
|
||||
$columnName = $this->columnName(substr($col, $index + 1));
|
||||
return "$tableName.$columnName";
|
||||
} else if (($index = stripos($col, " as ")) !== FALSE) {
|
||||
$columnName = $this->columnName(trim(substr($col, 0, $index)));
|
||||
$alias = $this->columnName(trim(substr($col, $index + 4)));
|
||||
return "$columnName as $alias";
|
||||
} else {
|
||||
return "\"$col\"";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getStatus() {
|
||||
$version = pg_version($this->connection)["client"] ?? "??";
|
||||
$status = pg_connection_status($this->connection);
|
||||
static $statusTexts = array(
|
||||
PGSQL_CONNECTION_OK => "PGSQL_CONNECTION_OK",
|
||||
PGSQL_CONNECTION_BAD => "PGSQL_CONNECTION_BAD",
|
||||
);
|
||||
|
||||
return ($statusTexts[$status] ?? "Unknown") . " (v$version)";
|
||||
}
|
||||
|
||||
public function buildCondition($condition, &$params) {
|
||||
if ($condition instanceof CondRegex) {
|
||||
$left = $condition->getLeftExp();
|
||||
$right = $condition->getRightExp();
|
||||
$left = ($left instanceof Column) ? $this->columnName($left->getName()) : $this->addValue($left, $params);
|
||||
$right = ($right instanceof Column) ? $this->columnName($right->getName()) : $this->addValue($right, $params);
|
||||
return $left . " ~ " . $right;
|
||||
} else {
|
||||
return parent::buildCondition($condition, $params);
|
||||
}
|
||||
}
|
||||
|
||||
private function createTriggerProcedure(string $name, array $statements) {
|
||||
$params = [];
|
||||
$query = "CREATE OR REPLACE FUNCTION $name() RETURNS TRIGGER AS \$table\$ BEGIN ";
|
||||
foreach ($statements as $stmt) {
|
||||
if ($stmt instanceof Keyword) {
|
||||
$query .= $stmt->getValue() . ";";
|
||||
} else {
|
||||
$query .= $stmt->build($this, $params) . ";";
|
||||
}
|
||||
}
|
||||
$query .= "END;";
|
||||
$query .= "\$table\$ LANGUAGE plpgsql;";
|
||||
|
||||
return $this->execute($query, $params);
|
||||
}
|
||||
|
||||
public function createTriggerBody(CreateTrigger $trigger, array $params = []): ?string {
|
||||
$procName = $this->tableName($trigger->getProcedure()->getName());
|
||||
return "EXECUTE PROCEDURE $procName()";
|
||||
}
|
||||
|
||||
public function getProcedureHead(CreateProcedure $procedure): ?string {
|
||||
$name = $this->tableName($procedure->getName());
|
||||
$returns = $procedure->getReturns() ?? "";
|
||||
$paramDefs = [];
|
||||
|
||||
if (!($procedure->getReturns() instanceof Trigger)) {
|
||||
foreach ($procedure->getParameters() as $parameter) {
|
||||
$paramDefs[] = $parameter->getName() . " " . $this->getColumnType($parameter);
|
||||
}
|
||||
}
|
||||
|
||||
$paramDefs = implode(",", $paramDefs);
|
||||
if ($returns) {
|
||||
if ($returns instanceof Column) {
|
||||
$returns = " RETURNS " . $this->getColumnType($returns);
|
||||
} else if ($returns instanceof Keyword) {
|
||||
$returns = " RETURNS " . $returns->getValue();
|
||||
}
|
||||
}
|
||||
|
||||
return "CREATE OR REPLACE FUNCTION $name($paramDefs)$returns AS $$";
|
||||
}
|
||||
|
||||
public function getProcedureTail(): string {
|
||||
return "$$ LANGUAGE plpgsql;";
|
||||
}
|
||||
|
||||
public function getProcedureBody(CreateProcedure $procedure): string {
|
||||
$statements = parent::getProcedureBody($procedure);
|
||||
if ($procedure->getReturns() instanceof Trigger) {
|
||||
$statements .= "RETURN NEW;";
|
||||
}
|
||||
return $statements;
|
||||
}
|
||||
|
||||
protected function buildUnsafe(Query $statement): string {
|
||||
$params = [];
|
||||
$query = $statement->build($params);
|
||||
|
||||
foreach ($params as $index => $value) {
|
||||
$value = $this->getUnsafeValue($value);
|
||||
$query = preg_replace("\$$index", $value, $query, 1);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
protected function createExpression(Expression $exp, array &$params): ?string {
|
||||
if ($exp instanceof DateAdd || $exp instanceof DateSub) {
|
||||
$lhs = $this->addValue($exp->getLHS(), $params);
|
||||
$rhs = $this->addValue($exp->getRHS(), $params);
|
||||
$unit = $exp->getUnit();
|
||||
|
||||
if ($exp->getRHS() instanceof Column) {
|
||||
$rhs = "$rhs * INTERVAL '1 $unit'";
|
||||
} else {
|
||||
$rhs = "$rhs $unit";
|
||||
}
|
||||
|
||||
$operator = ($exp instanceof DateAdd ? "+" : "-");
|
||||
return "$lhs $operator $rhs";
|
||||
} else if ($exp instanceof CurrentTimeStamp) {
|
||||
return "CURRENT_TIMESTAMP";
|
||||
} else if ($exp instanceof JsonArrayAgg) {
|
||||
$value = $this->addValue($exp->getValue(), $params);
|
||||
$alias = $this->columnName($exp->getAlias());
|
||||
return "JSON_AGG($value) as $alias";
|
||||
} else {
|
||||
return parent::createExpression($exp, $params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RowIteratorPostgreSQL extends RowIterator {
|
||||
|
||||
public function __construct($resultSet, bool $useCache = false) {
|
||||
parent::__construct($resultSet, false); // caching not needed
|
||||
}
|
||||
|
||||
protected function getNumRows(): int {
|
||||
return pg_num_rows($this->resultSet);
|
||||
}
|
||||
|
||||
public function rewind() {
|
||||
$this->rowIndex = 0;
|
||||
}
|
||||
|
||||
protected function fetchRow(int $index): array {
|
||||
return pg_fetch_assoc($this->resultSet, $index);
|
||||
}
|
||||
}
|
||||
133
Core/Driver/SQL/Query/AlterTable.class.php
Normal file
133
Core/Driver/SQL/Query/AlterTable.class.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Query;
|
||||
|
||||
use Core\Driver\SQL\Column\Column;
|
||||
use Core\Driver\SQL\Column\EnumColumn;
|
||||
use Core\Driver\SQL\Constraint\Constraint;
|
||||
use Core\Driver\SQL\Constraint\ForeignKey;
|
||||
use Core\Driver\SQL\Constraint\PrimaryKey;
|
||||
use Core\Driver\SQL\PostgreSQL;
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
class AlterTable extends Query {
|
||||
|
||||
private string $table;
|
||||
private string $action;
|
||||
private $data;
|
||||
|
||||
private ?Column $column;
|
||||
private ?Constraint $constraint;
|
||||
|
||||
public function __construct(SQL $sql, string $table) {
|
||||
parent::__construct($sql);
|
||||
$this->table = $table;
|
||||
$this->column = null;
|
||||
$this->constraint = null;
|
||||
}
|
||||
|
||||
public function add($what): AlterTable {
|
||||
if ($what instanceof Column) {
|
||||
$this->column = $what;
|
||||
} else if ($what instanceof Constraint) {
|
||||
$this->constraint = $what;
|
||||
} else {
|
||||
$this->column = new Column($what);
|
||||
}
|
||||
|
||||
$this->action = "ADD";
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function modify(Column $column): AlterTable {
|
||||
$this->column = $column;
|
||||
$this->action = "MODIFY";
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function drop($what): AlterTable {
|
||||
if ($what instanceof Column) {
|
||||
$this->column = $what;
|
||||
} else if ($what instanceof Constraint) {
|
||||
$this->constraint = $what;
|
||||
} else {
|
||||
$this->column = new Column($what);
|
||||
}
|
||||
$this->action = "DROP";
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function resetAutoIncrement(): AlterTable {
|
||||
$this->action = "RESET_AUTO_INCREMENT";
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addToEnum(EnumColumn $column, string $newValue): AlterTable {
|
||||
$this->action = "MODIFY";
|
||||
$this->column = $column;
|
||||
$this->data = $newValue;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAction(): string { return $this->action; }
|
||||
public function getColumn(): ?Column { return $this->column; }
|
||||
public function getConstraint(): ?Constraint { return $this->constraint; }
|
||||
public function getTable(): string { return $this->table; }
|
||||
|
||||
public function build(array &$params): ?string {
|
||||
$tableName = $this->sql->tableName($this->getTable());
|
||||
$action = $this->getAction();
|
||||
$column = $this->getColumn();
|
||||
$constraint = $this->getConstraint();
|
||||
|
||||
if ($action === "RESET_AUTO_INCREMENT") {
|
||||
return "ALTER TABLE $tableName AUTO_INCREMENT=1";
|
||||
}
|
||||
|
||||
$query = "ALTER TABLE $tableName $action ";
|
||||
|
||||
if ($column) {
|
||||
$query .= "COLUMN ";
|
||||
if ($action === "DROP") {
|
||||
$query .= $this->sql->columnName($column->getName());
|
||||
} else {
|
||||
// ADD or modify
|
||||
if ($column instanceof EnumColumn) {
|
||||
if ($this->sql instanceof PostgreSQL) {
|
||||
$typeName = $this->sql->getColumnType($column);
|
||||
$value = $this->sql->addValue($this->data, $params);
|
||||
return "ALTER TYPE $typeName ADD VALUE $value";
|
||||
}
|
||||
$column->addValue($this->data);
|
||||
}
|
||||
|
||||
$query .= $this->sql->getColumnDefinition($column);
|
||||
}
|
||||
} else if ($constraint) {
|
||||
if ($action === "DROP") {
|
||||
if ($constraint instanceof PrimaryKey) {
|
||||
$query .= "PRIMARY KEY";
|
||||
} else {
|
||||
$constraintName = $constraint->getName();
|
||||
if ($constraintName) {
|
||||
$query .= "CONSTRAINT " . $this->sql->columnName($constraintName);
|
||||
} else {
|
||||
$this->sql->setLastError("Cannot DROP CONSTRAINT without a constraint name.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} else if ($action === "ADD") {
|
||||
$query .= "CONSTRAINT ";
|
||||
$query .= $this->sql->getConstraintDefinition($constraint);
|
||||
} else if ($action === "MODIFY") {
|
||||
$this->sql->setLastError("MODIFY CONSTRAINT foreign key is not supported.");
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
$this->sql->setLastError("'ALTER TABLE' requires at least a column or a constraint.");
|
||||
return null;
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
15
Core/Driver/SQL/Query/Commit.class.php
Normal file
15
Core/Driver/SQL/Query/Commit.class.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Query;
|
||||
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
class Commit extends Query {
|
||||
public function __construct(SQL $sql) {
|
||||
parent::__construct($sql);
|
||||
}
|
||||
|
||||
public function build(array &$params): ?string {
|
||||
return "COMMIT";
|
||||
}
|
||||
}
|
||||
50
Core/Driver/SQL/Query/CreateProcedure.class.php
Normal file
50
Core/Driver/SQL/Query/CreateProcedure.class.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Core\Driver\SQL\Query;
|
||||
|
||||
use Core\Driver\SQL\Column\Column;
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
class CreateProcedure extends Query {
|
||||
|
||||
private string $name;
|
||||
private array $parameters;
|
||||
private array $statements;
|
||||
private $returns;
|
||||
|
||||
public function __construct(SQL $sql, string $procName) {
|
||||
parent::__construct($sql);
|
||||
$this->name = $procName;
|
||||
$this->parameters = [];
|
||||
$this->statements = [];
|
||||
$this->returns = NULL;
|
||||
}
|
||||
|
||||
public function param(Column $parameter): CreateProcedure {
|
||||
$this->parameters[] = $parameter;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function returns($column): CreateProcedure {
|
||||
$this->returns = $column;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function exec(array $statements): CreateProcedure {
|
||||
$this->statements = $statements;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function build(array &$params): ?string {
|
||||
$head = $this->sql->getProcedureHead($this);
|
||||
$body = $this->sql->getProcedureBody($this);
|
||||
$tail = $this->sql->getProcedureTail();
|
||||
return "$head BEGIN $body END; $tail";
|
||||
}
|
||||
|
||||
public function getName(): string { return $this->name; }
|
||||
public function getParameters(): array { return $this->parameters; }
|
||||
public function getReturns() { return $this->returns; }
|
||||
public function getStatements(): array { return $this->statements; }
|
||||
}
|
||||
156
Core/Driver/SQL/Query/CreateTable.class.php
Normal file
156
Core/Driver/SQL/Query/CreateTable.class.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Query;
|
||||
|
||||
use Core\Driver\SQL\Column\BigIntColumn;
|
||||
use Core\Driver\SQL\Column\Column;
|
||||
use Core\Driver\SQL\Column\DoubleColumn;
|
||||
use Core\Driver\SQL\Column\FloatColumn;
|
||||
use Core\Driver\SQL\Column\NumericColumn;
|
||||
use Core\Driver\SQL\Column\SerialColumn;
|
||||
use Core\Driver\SQL\Column\StringColumn;
|
||||
use Core\Driver\SQL\Column\IntColumn;
|
||||
use Core\Driver\SQL\Column\DateTimeColumn;
|
||||
use Core\Driver\SQL\Column\EnumColumn;
|
||||
use Core\Driver\SQL\Column\BoolColumn;
|
||||
use Core\Driver\SQL\Column\JsonColumn;
|
||||
|
||||
use Core\Driver\SQL\Constraint\Constraint;
|
||||
use Core\Driver\SQL\Constraint\PrimaryKey;
|
||||
use Core\Driver\SQL\Constraint\Unique;
|
||||
use Core\Driver\SQL\Constraint\ForeignKey;
|
||||
use Core\Driver\SQL\SQL;
|
||||
use Core\Driver\SQL\Strategy\Strategy;
|
||||
|
||||
class CreateTable extends Query {
|
||||
|
||||
private string $tableName;
|
||||
private array $columns;
|
||||
private array $constraints;
|
||||
private bool $ifNotExists;
|
||||
|
||||
public function __construct(SQL $sql, string $name) {
|
||||
parent::__construct($sql);
|
||||
$this->tableName = $name;
|
||||
$this->columns = array();
|
||||
$this->constraints = array();
|
||||
$this->ifNotExists = false;
|
||||
}
|
||||
|
||||
public function addColumn(Column $column): CreateTable {
|
||||
$this->columns[$column->getName()] = $column;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addConstraint(Constraint $constraint): CreateTable {
|
||||
$this->constraints[] = $constraint;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addSerial(string $name): CreateTable {
|
||||
$this->columns[$name] = new SerialColumn($name);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addString(string $name, ?int $maxSize = NULL, bool $nullable = false, $defaultValue = NULL): CreateTable {
|
||||
$this->columns[$name] = new StringColumn($name, $maxSize, $nullable, $defaultValue);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addDateTime(string $name, bool $nullable = false, $defaultValue = NULL): CreateTable {
|
||||
$this->columns[$name] = new DateTimeColumn($name, $nullable, $defaultValue);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addInt(string $name, bool $nullable = false, $defaultValue = NULL, bool $unsigned = false): CreateTable {
|
||||
$this->columns[$name] = new IntColumn($name, $nullable, $defaultValue, $unsigned);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addBigInt(string $name, bool $nullable = false, $defaultValue = NULL, bool $unsigned = false): CreateTable {
|
||||
$this->columns[$name] = new BigIntColumn($name, $nullable, $defaultValue, $unsigned);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addBool(string $name, $defaultValue = false): CreateTable {
|
||||
$this->columns[$name] = new BoolColumn($name, $defaultValue);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addJson(string $name, bool $nullable = false, $defaultValue = NULL): CreateTable {
|
||||
$this->columns[$name] = new JsonColumn($name, $nullable, $defaultValue);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addEnum(string $name, array $values, bool $nullable = false, $defaultValue = NULL): CreateTable {
|
||||
$this->columns[$name] = new EnumColumn($name, $values, $nullable, $defaultValue);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addNumeric(string $name, bool $nullable = false, $defaultValue = NULL, ?int $digitsTotal = 10, ?int $digitsDecimal = 0): CreateTable {
|
||||
$this->columns[$name] = new NumericColumn($name, $nullable, $defaultValue, $digitsTotal, $digitsDecimal);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addFloat(string $name, bool $nullable = false, $defaultValue = NULL, ?int $digitsTotal = null, ?int $digitsDecimal = null): CreateTable {
|
||||
$this->columns[$name] = new FloatColumn($name, $nullable, $defaultValue, $digitsTotal, $digitsDecimal);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addDouble(string $name, bool $nullable = false, $defaultValue = NULL, ?int $digitsTotal = null, ?int $digitsDecimal = null): CreateTable {
|
||||
$this->columns[$name] = new DoubleColumn($name, $nullable, $defaultValue, $digitsTotal, $digitsDecimal);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function primaryKey(...$names): CreateTable {
|
||||
$pk = new PrimaryKey($names);
|
||||
$pk->setName(strtolower("pk_{$this->tableName}"));
|
||||
$this->constraints[] = $pk;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function unique(...$names): CreateTable {
|
||||
$this->constraints[] = new Unique($names);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function foreignKey(string $column, string $refTable, string $refColumn, ?Strategy $strategy = NULL): CreateTable {
|
||||
$fk = new ForeignKey($column, $refTable, $refColumn, $strategy);
|
||||
$fk->setName(strtolower("fk_{$this->tableName}_${refTable}_${refColumn}"));
|
||||
$this->constraints[] = $fk;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function onlyIfNotExists(): CreateTable {
|
||||
$this->ifNotExists = true;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function ifNotExists(): bool { return $this->ifNotExists; }
|
||||
public function getTableName(): string { return $this->tableName; }
|
||||
public function getColumns(): array { return $this->columns; }
|
||||
public function getConstraints(): array { return $this->constraints; }
|
||||
|
||||
public function build(array &$params): ?string {
|
||||
$tableName = $this->sql->tableName($this->getTableName());
|
||||
$ifNotExists = $this->ifNotExists() ? " IF NOT EXISTS" : "";
|
||||
|
||||
$entries = array();
|
||||
foreach ($this->getColumns() as $column) {
|
||||
$entries[] = ($tmp = $this->sql->getColumnDefinition($column));
|
||||
if (is_null($tmp)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->getConstraints() as $constraint) {
|
||||
$entries[] = ($tmp = $this->sql->getConstraintDefinition($constraint));
|
||||
if (is_null($tmp)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$entries = implode(",", $entries);
|
||||
return "CREATE TABLE$ifNotExists $tableName ($entries)";
|
||||
}
|
||||
}
|
||||
83
Core/Driver/SQL/Query/CreateTrigger.class.php
Normal file
83
Core/Driver/SQL/Query/CreateTrigger.class.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Query;
|
||||
|
||||
use Core\API\User\Create;
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
class CreateTrigger extends Query {
|
||||
|
||||
private string $name;
|
||||
private string $time;
|
||||
private string $event;
|
||||
private string $tableName;
|
||||
private array $parameters;
|
||||
private ?CreateProcedure $procedure;
|
||||
|
||||
public function __construct(SQL $sql, string $triggerName) {
|
||||
parent::__construct($sql);
|
||||
$this->name = $triggerName;
|
||||
$this->time = "AFTER";
|
||||
$this->tableName = "";
|
||||
$this->event = "";
|
||||
$this->parameters = [];
|
||||
$this->procedure = null;
|
||||
}
|
||||
|
||||
public function before(): CreateTrigger {
|
||||
$this->time = "BEFORE";
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function after(): CreateTrigger {
|
||||
$this->time = "AFTER";
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function update(string $table): CreateTrigger {
|
||||
$this->tableName = $table;
|
||||
$this->event = "UPDATE";
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function insert(string $table): CreateTrigger {
|
||||
$this->tableName = $table;
|
||||
$this->event = "INSERT";
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function delete(string $table): CreateTrigger {
|
||||
$this->tableName = $table;
|
||||
$this->event = "DELETE";
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function exec(CreateProcedure $procedure, array $parameters = []): CreateTrigger {
|
||||
$this->procedure = $procedure;
|
||||
$this->parameters = $parameters;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getName(): string { return $this->name; }
|
||||
public function getTime(): string { return $this->time; }
|
||||
public function getEvent(): string { return $this->event; }
|
||||
public function getTable(): string { return $this->tableName; }
|
||||
public function getProcedure(): CreateProcedure { return $this->procedure; }
|
||||
|
||||
public function build(array &$params): ?string {
|
||||
$name = $this->sql->tableName($this->getName());
|
||||
$time = $this->getTime();
|
||||
$event = $this->getEvent();
|
||||
$tableName = $this->sql->tableName($this->getTable());
|
||||
|
||||
$params = array();
|
||||
$query = "CREATE TRIGGER $name $time $event ON $tableName FOR EACH ROW ";
|
||||
$triggerBody = $this->sql->createTriggerBody($this, $this->parameters);
|
||||
if ($triggerBody === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$query .= $triggerBody;
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
32
Core/Driver/SQL/Query/Delete.class.php
Normal file
32
Core/Driver/SQL/Query/Delete.class.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Query;
|
||||
|
||||
use Core\Driver\SQL\Condition\CondOr;
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
class Delete extends Query {
|
||||
|
||||
private string $table;
|
||||
private array $conditions;
|
||||
|
||||
public function __construct(SQL $sql, string $table) {
|
||||
parent::__construct($sql);
|
||||
$this->table = $table;
|
||||
$this->conditions = array();
|
||||
}
|
||||
|
||||
public function where(...$conditions): Delete {
|
||||
$this->conditions[] = (count($conditions) === 1 ? $conditions : new CondOr($conditions));
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTable(): string { return $this->table; }
|
||||
public function getConditions(): array { return $this->conditions; }
|
||||
|
||||
public function build(array &$params): ?string {
|
||||
$table = $this->sql->tableName($this->getTable());
|
||||
$where = $this->sql->getWhereClause($this->getConditions(), $params);
|
||||
return "DELETE FROM $table$where";
|
||||
}
|
||||
}
|
||||
29
Core/Driver/SQL/Query/Drop.php
Normal file
29
Core/Driver/SQL/Query/Drop.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Core\Driver\SQL\Query;
|
||||
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
class Drop extends Query {
|
||||
|
||||
private string $table;
|
||||
|
||||
/**
|
||||
* Drop constructor.
|
||||
* @param SQL $sql
|
||||
* @param string $table
|
||||
*/
|
||||
public function __construct(SQL $sql, string $table) {
|
||||
parent::__construct($sql);
|
||||
$this->table = $table;
|
||||
}
|
||||
|
||||
public function getTable(): string {
|
||||
return $this->table;
|
||||
}
|
||||
|
||||
public function build(array &$params): ?string {
|
||||
return "DROP TABLE " . $this->sql->tableName($this->getTable());
|
||||
}
|
||||
}
|
||||
83
Core/Driver/SQL/Query/Insert.class.php
Normal file
83
Core/Driver/SQL/Query/Insert.class.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Query;
|
||||
|
||||
use Core\Driver\SQL\SQL;
|
||||
use Core\Driver\SQL\Strategy\Strategy;
|
||||
|
||||
class Insert extends Query {
|
||||
|
||||
private string $tableName;
|
||||
private array $columns;
|
||||
private array $rows;
|
||||
private ?Strategy $onDuplicateKey;
|
||||
private ?string $returning;
|
||||
|
||||
public function __construct(SQL $sql, string $name, array $columns = array()) {
|
||||
parent::__construct($sql);
|
||||
$this->tableName = $name;
|
||||
$this->columns = $columns;
|
||||
$this->rows = array();
|
||||
$this->onDuplicateKey = NULL;
|
||||
$this->returning = NULL;
|
||||
}
|
||||
|
||||
public function addRow(...$values): Insert {
|
||||
$this->rows[] = $values;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function onDuplicateKeyStrategy(Strategy $strategy): Insert {
|
||||
$this->onDuplicateKey = $strategy;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function returning(string $column): Insert {
|
||||
$this->returning = $column;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTableName(): string { return $this->tableName; }
|
||||
public function getColumns(): array { return $this->columns; }
|
||||
public function getRows(): array { return $this->rows; }
|
||||
public function onDuplicateKey(): ?Strategy { return $this->onDuplicateKey; }
|
||||
public function getReturning(): ?string { return $this->returning; }
|
||||
|
||||
public function build(array &$params): ?string {
|
||||
$tableName = $this->sql->tableName($this->getTableName());
|
||||
$columns = $this->getColumns();
|
||||
$rows = $this->getRows();
|
||||
|
||||
if (empty($rows)) {
|
||||
$this->sql->setLastError("No rows to insert given.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_null($columns) || empty($columns)) {
|
||||
$columnStr = "";
|
||||
} else {
|
||||
$columnStr = " (" . $this->sql->columnName($columns) . ")";
|
||||
}
|
||||
|
||||
$values = array();
|
||||
foreach ($rows as $row) {
|
||||
$rowPlaceHolder = array();
|
||||
foreach ($row as $val) {
|
||||
$rowPlaceHolder[] = $this->sql->addValue($val, $params);
|
||||
}
|
||||
|
||||
$values[] = "(" . implode(",", $rowPlaceHolder) . ")";
|
||||
}
|
||||
|
||||
$values = implode(",", $values);
|
||||
|
||||
$onDuplicateKey = $this->sql->getOnDuplicateStrategy($this->onDuplicateKey(), $params);
|
||||
if ($onDuplicateKey === FALSE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$returningCol = $this->getReturning();
|
||||
$returning = $this->sql->getReturning($returningCol);
|
||||
return "INSERT INTO $tableName$columnStr VALUES $values$onDuplicateKey$returning";
|
||||
}
|
||||
}
|
||||
29
Core/Driver/SQL/Query/Query.class.php
Normal file
29
Core/Driver/SQL/Query/Query.class.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Query;
|
||||
|
||||
use Core\Driver\SQL\Expression\Expression;
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
abstract class Query extends Expression {
|
||||
|
||||
protected SQL $sql;
|
||||
public bool $dump;
|
||||
|
||||
public function __construct(SQL $sql) {
|
||||
$this->sql = $sql;
|
||||
$this->dump = false;
|
||||
}
|
||||
|
||||
public function dump(): Query {
|
||||
$this->dump = true;
|
||||
return $this;
|
||||
}
|
||||
|
||||
// can actually return bool|array (depending on success and query type)
|
||||
public function execute() {
|
||||
return $this->sql->executeQuery($this);
|
||||
}
|
||||
|
||||
public abstract function build(array &$params): ?string;
|
||||
}
|
||||
15
Core/Driver/SQL/Query/RollBack.class.php
Normal file
15
Core/Driver/SQL/Query/RollBack.class.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Query;
|
||||
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
class RollBack extends Query {
|
||||
public function __construct(SQL $sql) {
|
||||
parent::__construct($sql);
|
||||
}
|
||||
|
||||
public function build(array &$params): ?string {
|
||||
return "ROLLBACK";
|
||||
}
|
||||
}
|
||||
214
Core/Driver/SQL/Query/Select.class.php
Normal file
214
Core/Driver/SQL/Query/Select.class.php
Normal file
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Query;
|
||||
|
||||
use Core\Driver\SQL\Condition\CondOr;
|
||||
use Core\Driver\SQL\Expression\JsonArrayAgg;
|
||||
use Core\Driver\SQL\Join;
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
class Select extends Query {
|
||||
|
||||
private array $selectValues;
|
||||
private array $tables;
|
||||
private array $conditions;
|
||||
private array $joins;
|
||||
private array $orderColumns;
|
||||
private array $groupColumns;
|
||||
private array $havings;
|
||||
private bool $sortAscending;
|
||||
private int $limit;
|
||||
private int $offset;
|
||||
private bool $forUpdate;
|
||||
private int $fetchType;
|
||||
|
||||
public function __construct($sql, ...$selectValues) {
|
||||
parent::__construct($sql);
|
||||
$this->selectValues = (!empty($selectValues) && is_array($selectValues[0])) ? $selectValues[0] : $selectValues;
|
||||
$this->tables = array();
|
||||
$this->conditions = array();
|
||||
$this->havings = array();
|
||||
$this->joins = array();
|
||||
$this->orderColumns = array();
|
||||
$this->groupColumns = array();
|
||||
$this->limit = 0;
|
||||
$this->offset = 0;
|
||||
$this->sortAscending = true;
|
||||
$this->forUpdate = false;
|
||||
$this->fetchType = SQL::FETCH_ALL;
|
||||
}
|
||||
|
||||
public function from(...$tables): Select {
|
||||
$this->tables = array_merge($this->tables, $tables);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addValue($value): Select {
|
||||
$this->selectValues[] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function where(...$conditions): Select {
|
||||
$this->conditions[] = (count($conditions) === 1 ? $conditions : new CondOr($conditions));
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function having(...$conditions): Select {
|
||||
$this->havings[] = (count($conditions) === 1 ? $conditions : new CondOr($conditions));
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function innerJoin(string $table, string $columnA, string $columnB, ?string $tableAlias = null, array $conditions = []): Select {
|
||||
$this->joins[] = new Join("INNER", $table, $columnA, $columnB, $tableAlias, $conditions);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function leftJoin(string $table, string $columnA, string $columnB, ?string $tableAlias = null, array $conditions = []): Select {
|
||||
$this->joins[] = new Join("LEFT", $table, $columnA, $columnB, $tableAlias, $conditions);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addJoin(Join $join): Select {
|
||||
$this->joins[] = $join;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function groupBy(...$columns): Select {
|
||||
$this->groupColumns = $columns;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function orderBy(...$columns): Select {
|
||||
$this->orderColumns = $columns;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function ascending(): Select {
|
||||
$this->sortAscending = true;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function descending(): Select {
|
||||
$this->sortAscending = false;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function limit(int $limit): Select {
|
||||
$this->limit = $limit;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function offset(int $offset): Select {
|
||||
$this->offset = $offset;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function lockForUpdate(): Select {
|
||||
$this->forUpdate = true;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function iterator(): Select {
|
||||
$this->fetchType = SQL::FETCH_ITERATIVE;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function first(): Select {
|
||||
$this->fetchType = SQL::FETCH_ONE;
|
||||
$this->limit = 1;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function execute() {
|
||||
return $this->sql->executeQuery($this, $this->fetchType);
|
||||
}
|
||||
|
||||
public function getSelectValues(): array { return $this->selectValues; }
|
||||
public function getTables(): array { return $this->tables; }
|
||||
public function getConditions(): array { return $this->conditions; }
|
||||
public function getJoins(): array { return $this->joins; }
|
||||
public function isOrderedAscending(): bool { return $this->sortAscending; }
|
||||
public function getOrderBy(): array { return $this->orderColumns; }
|
||||
public function getLimit(): int { return $this->limit; }
|
||||
public function getOffset(): int { return $this->offset; }
|
||||
public function getGroupBy(): array { return $this->groupColumns; }
|
||||
public function getHavings(): array { return $this->havings; }
|
||||
|
||||
public function build(array &$params): ?string {
|
||||
|
||||
$selectValues = [];
|
||||
foreach ($this->selectValues as $value) {
|
||||
if (is_string($value)) {
|
||||
$selectValues[] = $this->sql->columnName($value);
|
||||
} else if ($value instanceof Select) {
|
||||
$subSelect = $value->build($params);
|
||||
if (count($value->getSelectValues()) !== 1) {
|
||||
$selectValues[] = "($subSelect)";
|
||||
} else {
|
||||
$columnAlias = null;
|
||||
$subSelectColumn = $value->getSelectValues()[0];
|
||||
if (is_string($subSelectColumn) && ($index = stripos($subSelectColumn, " as ")) !== FALSE) {
|
||||
$columnAlias = substr($subSelectColumn, $index + 4);
|
||||
} else if ($subSelectColumn instanceof JsonArrayAgg) {
|
||||
$columnAlias = $subSelectColumn->getAlias();
|
||||
}
|
||||
|
||||
if ($columnAlias) {
|
||||
$selectValues[] = "($subSelect) as $columnAlias";
|
||||
} else {
|
||||
$selectValues[] = "($subSelect)";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$selectValues[] = $this->sql->addValue($value, $params);
|
||||
}
|
||||
}
|
||||
|
||||
$tables = $this->getTables();
|
||||
$selectValues = implode(",", $selectValues);
|
||||
|
||||
if (!$tables) {
|
||||
return "SELECT $selectValues";
|
||||
}
|
||||
|
||||
$tables = $this->sql->tableName($tables);
|
||||
$where = $this->sql->getWhereClause($this->getConditions(), $params);
|
||||
$havingClause = "";
|
||||
if (count($this->havings) > 0) {
|
||||
$havingClause = " HAVING " . $this->sql->buildCondition($this->getHavings(), $params);
|
||||
}
|
||||
|
||||
$joinStr = "";
|
||||
$joins = $this->getJoins();
|
||||
if (!empty($joins)) {
|
||||
foreach ($joins as $join) {
|
||||
$type = $join->getType();
|
||||
$joinTable = $this->sql->tableName($join->getTable());
|
||||
$tableAlias = ($join->getTableAlias() ? " " . $join->getTableAlias() : "");
|
||||
$condition = $this->sql->buildCondition($join->getConditions(), $params);
|
||||
$joinStr .= " $type JOIN $joinTable$tableAlias ON ($condition)";
|
||||
}
|
||||
}
|
||||
|
||||
$groupBy = "";
|
||||
$groupColumns = $this->getGroupBy();
|
||||
if (!empty($groupColumns)) {
|
||||
$groupBy = " GROUP BY " . $this->sql->columnName($groupColumns);
|
||||
}
|
||||
|
||||
$orderBy = "";
|
||||
$orderColumns = $this->getOrderBy();
|
||||
if (!empty($orderColumns)) {
|
||||
$orderBy = " ORDER BY " . $this->sql->columnName($orderColumns);
|
||||
$orderBy .= ($this->isOrderedAscending() ? " ASC" : " DESC");
|
||||
}
|
||||
|
||||
$limit = ($this->getLimit() > 0 ? (" LIMIT " . $this->getLimit()) : "");
|
||||
$offset = ($this->getOffset() > 0 ? (" OFFSET " . $this->getOffset()) : "");
|
||||
$forUpdate = ($this->forUpdate ? " FOR UPDATE" : "");
|
||||
return "SELECT $selectValues FROM $tables$joinStr$where$groupBy$havingClause$orderBy$limit$offset$forUpdate";
|
||||
}
|
||||
}
|
||||
15
Core/Driver/SQL/Query/StartTransaction.class.php
Normal file
15
Core/Driver/SQL/Query/StartTransaction.class.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Query;
|
||||
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
class StartTransaction extends Query {
|
||||
public function __construct(SQL $sql) {
|
||||
parent::__construct($sql);
|
||||
}
|
||||
|
||||
public function build(array &$params): ?string {
|
||||
return "START TRANSACTION";
|
||||
}
|
||||
}
|
||||
21
Core/Driver/SQL/Query/Truncate.class.php
Normal file
21
Core/Driver/SQL/Query/Truncate.class.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Query;
|
||||
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
class Truncate extends Query {
|
||||
|
||||
private string $tableName;
|
||||
|
||||
public function __construct(SQL $sql, string $name) {
|
||||
parent::__construct($sql);
|
||||
$this->tableName = $name;
|
||||
}
|
||||
|
||||
public function getTable(): string { return $this->tableName; }
|
||||
|
||||
public function build(array &$params): ?string {
|
||||
return "TRUNCATE " . $this->sql->tableName($this->getTable());
|
||||
}
|
||||
}
|
||||
47
Core/Driver/SQL/Query/Update.class.php
Normal file
47
Core/Driver/SQL/Query/Update.class.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Query;
|
||||
|
||||
use Core\Driver\SQL\Condition\CondOr;
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
class Update extends Query {
|
||||
|
||||
private array $values;
|
||||
private string $table;
|
||||
private array $conditions;
|
||||
|
||||
public function __construct(SQL $sql, string $table) {
|
||||
parent::__construct($sql);
|
||||
$this->values = array();
|
||||
$this->table = $table;
|
||||
$this->conditions = array();
|
||||
}
|
||||
|
||||
public function where(...$conditions): Update {
|
||||
$this->conditions[] = (count($conditions) === 1 ? $conditions : new CondOr($conditions));
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function set(string $key, $val): Update {
|
||||
$this->values[$key] = $val;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTable(): string { return $this->table; }
|
||||
public function getConditions(): array { return $this->conditions; }
|
||||
public function getValues(): array { return $this->values; }
|
||||
|
||||
public function build(array &$params): ?string {
|
||||
$table = $this->sql->tableName($this->getTable());
|
||||
|
||||
$valueStr = array();
|
||||
foreach($this->getValues() as $key => $val) {
|
||||
$valueStr[] = $this->sql->columnName($key) . "=" . $this->sql->addValue($val, $params);
|
||||
}
|
||||
$valueStr = implode(",", $valueStr);
|
||||
|
||||
$where = $this->sql->getWhereClause($this->getConditions(), $params);
|
||||
return "UPDATE $table SET $valueStr$where";
|
||||
}
|
||||
}
|
||||
39
Core/Driver/SQL/RowIterator.class.php
Normal file
39
Core/Driver/SQL/RowIterator.class.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL;
|
||||
|
||||
abstract class RowIterator implements \Iterator {
|
||||
|
||||
protected $resultSet;
|
||||
protected int $rowIndex;
|
||||
protected array $fetchedRows;
|
||||
protected int $numRows;
|
||||
protected bool $useCache;
|
||||
|
||||
public function __construct($resultSet, bool $useCache = false) {
|
||||
$this->resultSet = $resultSet;
|
||||
$this->fetchedRows = [];
|
||||
$this->rowIndex = 0;
|
||||
$this->numRows = $this->getNumRows();
|
||||
$this->useCache = $useCache;
|
||||
}
|
||||
|
||||
protected abstract function getNumRows(): int;
|
||||
protected abstract function fetchRow(int $index): array;
|
||||
|
||||
public function current() {
|
||||
return $this->fetchRow($this->rowIndex);
|
||||
}
|
||||
|
||||
public function next() {
|
||||
$this->rowIndex++;
|
||||
}
|
||||
|
||||
public function key() {
|
||||
return $this->rowIndex;
|
||||
}
|
||||
|
||||
public function valid(): bool {
|
||||
return $this->rowIndex < $this->numRows;
|
||||
}
|
||||
}
|
||||
434
Core/Driver/SQL/SQL.class.php
Normal file
434
Core/Driver/SQL/SQL.class.php
Normal file
@@ -0,0 +1,434 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL;
|
||||
|
||||
use Core\Driver\Logger\Logger;
|
||||
use Core\Driver\SQL\Column\Column;
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Driver\SQL\Condition\CondAnd;
|
||||
use Core\Driver\SQL\Condition\CondBool;
|
||||
use Core\Driver\SQL\Condition\CondIn;
|
||||
use Core\Driver\SQL\Condition\Condition;
|
||||
use Core\Driver\SQL\Condition\CondKeyword;
|
||||
use Core\Driver\SQL\Condition\CondNot;
|
||||
use Core\Driver\Sql\Condition\CondNull;
|
||||
use Core\Driver\SQL\Condition\CondOr;
|
||||
use Core\Driver\SQL\Condition\Exists;
|
||||
use Core\Driver\SQL\Constraint\Constraint;
|
||||
use Core\Driver\SQL\Constraint\Unique;
|
||||
use Core\Driver\SQL\Constraint\PrimaryKey;
|
||||
use Core\Driver\SQL\Constraint\ForeignKey;
|
||||
use Core\Driver\SQL\Expression\CaseWhen;
|
||||
use Core\Driver\SQL\Expression\CurrentTimeStamp;
|
||||
use Core\Driver\SQL\Expression\Expression;
|
||||
use Core\Driver\SQL\Expression\Sum;
|
||||
use Core\Driver\SQL\Query\AlterTable;
|
||||
use Core\Driver\SQL\Query\CreateProcedure;
|
||||
use Core\Driver\SQL\Query\CreateTable;
|
||||
use Core\Driver\SQL\Query\CreateTrigger;
|
||||
use Core\Driver\SQL\Query\Delete;
|
||||
use Core\Driver\SQL\Query\Drop;
|
||||
use Core\Driver\SQL\Query\Insert;
|
||||
use Core\Driver\SQL\Query\Query;
|
||||
use Core\Driver\SQL\Query\Select;
|
||||
use Core\Driver\SQL\Query\Truncate;
|
||||
use Core\Driver\SQL\Query\Update;
|
||||
use Core\Driver\SQL\Strategy\CascadeStrategy;
|
||||
use Core\Driver\SQL\Strategy\SetDefaultStrategy;
|
||||
use Core\Driver\SQL\Strategy\SetNullStrategy;
|
||||
use Core\Driver\SQL\Strategy\Strategy;
|
||||
use Core\Objects\ConnectionData;
|
||||
|
||||
abstract class SQL {
|
||||
|
||||
const FETCH_NONE = 0;
|
||||
const FETCH_ONE = 1;
|
||||
const FETCH_ALL = 2;
|
||||
const FETCH_ITERATIVE = 3;
|
||||
|
||||
protected Logger $logger;
|
||||
protected string $lastError;
|
||||
protected $connection;
|
||||
protected ConnectionData $connectionData;
|
||||
protected int $lastInsertId;
|
||||
|
||||
protected bool $logQueries;
|
||||
|
||||
public function __construct($connectionData) {
|
||||
$this->connection = NULL;
|
||||
$this->lastError = 'Unknown Error';
|
||||
$this->connectionData = $connectionData;
|
||||
$this->lastInsertId = 0;
|
||||
$this->logger = new Logger(getClassName($this), $this);
|
||||
$this->logQueries = false;
|
||||
}
|
||||
|
||||
public function isConnected(): bool {
|
||||
return !is_null($this->connection) && !is_bool($this->connection);
|
||||
}
|
||||
|
||||
public function getLastError(): string {
|
||||
return trim($this->lastError);
|
||||
}
|
||||
|
||||
public function createTable($tableName): CreateTable {
|
||||
return new CreateTable($this, $tableName);
|
||||
}
|
||||
|
||||
public function insert($tableName, $columns=array()): Insert {
|
||||
return new Insert($this, $tableName, $columns);
|
||||
}
|
||||
|
||||
public function select(...$columNames): Select {
|
||||
return new Select($this, $columNames);
|
||||
}
|
||||
|
||||
public function truncate($table): Truncate {
|
||||
return new Truncate($this, $table);
|
||||
}
|
||||
|
||||
public function delete($table): Delete {
|
||||
return new Delete($this, $table);
|
||||
}
|
||||
|
||||
public function update($table): Update {
|
||||
return new Update($this, $table);
|
||||
}
|
||||
|
||||
public function drop(string $table): Drop {
|
||||
return new Drop($this, $table);
|
||||
}
|
||||
|
||||
public function alterTable($tableName): AlterTable {
|
||||
return new AlterTable($this, $tableName);
|
||||
}
|
||||
|
||||
public function createTrigger($triggerName): CreateTrigger {
|
||||
return new CreateTrigger($this, $triggerName);
|
||||
}
|
||||
|
||||
public function createProcedure(string $procName): CreateProcedure {
|
||||
return new CreateProcedure($this, $procName);
|
||||
}
|
||||
|
||||
|
||||
// ####################
|
||||
// ### ABSTRACT METHODS
|
||||
// ####################
|
||||
|
||||
// Misc
|
||||
public abstract function checkRequirements();
|
||||
public abstract function getDriverName();
|
||||
|
||||
// Connection Management
|
||||
public abstract function connect();
|
||||
public abstract function disconnect();
|
||||
|
||||
/**
|
||||
* @param Query $query
|
||||
* @param int $fetchType
|
||||
* @return mixed
|
||||
*/
|
||||
public function executeQuery(Query $query, int $fetchType = self::FETCH_NONE) {
|
||||
|
||||
$parameters = [];
|
||||
$queryStr = $query->build($parameters);
|
||||
|
||||
if ($query->dump) {
|
||||
var_dump($queryStr);
|
||||
var_dump($parameters);
|
||||
}
|
||||
|
||||
if ($queryStr === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$res = $this->execute($queryStr, $parameters, $fetchType);
|
||||
$success = ($res !== FALSE);
|
||||
|
||||
// fetch generated serial ids for Insert statements
|
||||
$generatedColumn = ($query instanceof Insert ? $query->getReturning() : null);
|
||||
if ($success && $generatedColumn) {
|
||||
$this->fetchReturning($res, $generatedColumn);
|
||||
}
|
||||
|
||||
if ($this->logQueries && (!($query instanceof Insert) || $query->getTableName() !== "SystemLog")) {
|
||||
|
||||
if ($success === false || $fetchType == self::FETCH_NONE) {
|
||||
$result = var_export($success, true);
|
||||
} else if ($fetchType === self::FETCH_ALL) {
|
||||
$result = count($res) . " rows";
|
||||
} else if ($fetchType === self::FETCH_ONE) {
|
||||
$result = ($res === null ? "(empty)" : "1 row");
|
||||
} else if ($fetchType === self::FETCH_ITERATIVE) {
|
||||
$result = $res->getNumRows() . " rows (iterative)";
|
||||
} else {
|
||||
$result = "Unknown";
|
||||
}
|
||||
|
||||
$message = sprintf("Query: %s, Parameters: %s, Result: %s",
|
||||
var_export($queryStr, true), var_export($parameters, true), $result
|
||||
);
|
||||
|
||||
if ($success === false) {
|
||||
$message .= "Error: " . var_export($this->lastError, true);
|
||||
}
|
||||
|
||||
$this->logger->debug($message);
|
||||
}
|
||||
|
||||
return $fetchType === self::FETCH_NONE ? $success : $res;
|
||||
}
|
||||
|
||||
public function getWhereClause($conditions, &$params): string {
|
||||
if (!$conditions) {
|
||||
return "";
|
||||
} else {
|
||||
return " WHERE " . $this->buildCondition($conditions, $params);
|
||||
}
|
||||
}
|
||||
|
||||
public function getConstraintDefinition(Constraint $constraint): ?string {
|
||||
$columnName = $this->columnName($constraint->getColumnNames());
|
||||
|
||||
if ($constraint instanceof PrimaryKey) {
|
||||
return "PRIMARY KEY ($columnName)";
|
||||
} else if ($constraint instanceof Unique) {
|
||||
return "UNIQUE ($columnName)";
|
||||
} else if ($constraint instanceof ForeignKey) {
|
||||
$refTable = $this->tableName($constraint->getReferencedTable());
|
||||
$refColumn = $this->columnName($constraint->getReferencedColumn());
|
||||
$strategy = $constraint->onDelete();
|
||||
$code = "FOREIGN KEY ($columnName) REFERENCES $refTable ($refColumn)";
|
||||
if ($strategy instanceof SetDefaultStrategy) {
|
||||
$code .= " ON DELETE SET DEFAULT";
|
||||
} else if($strategy instanceof SetNullStrategy) {
|
||||
$code .= " ON DELETE SET NULL";
|
||||
} else if($strategy instanceof CascadeStrategy) {
|
||||
$code .= " ON DELETE CASCADE";
|
||||
}
|
||||
|
||||
return $code;
|
||||
} else {
|
||||
$this->lastError = $this->logger->error("Unsupported constraint type: " . get_class($constraint));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract function fetchReturning($res, string $returningCol);
|
||||
public abstract function getColumnDefinition(Column $column): ?string;
|
||||
public abstract function getOnDuplicateStrategy(?Strategy $strategy, &$params): ?string;
|
||||
public abstract function createTriggerBody(CreateTrigger $trigger, array $params = []): ?string;
|
||||
public abstract function getProcedureHead(CreateProcedure $procedure): ?string;
|
||||
public abstract function getColumnType(Column $column): ?string;
|
||||
public function getProcedureTail(): string { return ""; }
|
||||
public function getReturning(?string $columns): string { return ""; }
|
||||
|
||||
public function getProcedureBody(CreateProcedure $procedure): string {
|
||||
$statements = "";
|
||||
foreach ($procedure->getStatements() as $statement) {
|
||||
$statements .= $this->buildUnsafe($statement) . ";";
|
||||
}
|
||||
return $statements;
|
||||
}
|
||||
|
||||
protected function getUnsafeValue($value): ?string {
|
||||
if (is_string($value)) {
|
||||
return "'" . addslashes("$value") . "'"; // unsafe operation here...
|
||||
} else if (is_numeric($value) || is_bool($value)) {
|
||||
return $value;
|
||||
} else if ($value instanceof Column) {
|
||||
return $this->columnName($value);
|
||||
} else if ($value === null) {
|
||||
return "NULL";
|
||||
} else {
|
||||
$this->lastError = $this->logger->error("Cannot create unsafe value of type: " . gettype($value));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract function getValueDefinition($val);
|
||||
public abstract function addValue($val, &$params = NULL, bool $unsafe = false);
|
||||
protected abstract function buildUnsafe(Query $statement): string;
|
||||
|
||||
public abstract function tableName($table): string;
|
||||
public abstract function columnName($col): string;
|
||||
|
||||
// Special Keywords and functions
|
||||
public function now(): CurrentTimeStamp { return new CurrentTimeStamp(); }
|
||||
public function currentTimestamp(): CurrentTimeStamp { return new CurrentTimeStamp(); }
|
||||
|
||||
public function count($col = NULL): Keyword {
|
||||
if (is_null($col)) {
|
||||
return new Keyword("COUNT(*) AS count");
|
||||
} else if($col instanceof Keyword) {
|
||||
return new Keyword("COUNT(" . $col->getValue() . ") AS count");
|
||||
} else {
|
||||
$countCol = strtolower(str_replace(".","_", $col)) . "_count";
|
||||
$col = $this->columnName($col);
|
||||
return new Keyword("COUNT($col) AS $countCol");
|
||||
}
|
||||
}
|
||||
|
||||
public function distinct($col): Keyword {
|
||||
$col = $this->columnName($col);
|
||||
return new Keyword("DISTINCT($col)");
|
||||
}
|
||||
|
||||
// Statements
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
protected abstract function execute($query, $values = NULL, int $fetchType = self::FETCH_NONE);
|
||||
|
||||
public function buildCondition($condition, &$params) {
|
||||
|
||||
if ($condition instanceof CondOr) {
|
||||
$conditions = array();
|
||||
foreach($condition->getConditions() as $cond) {
|
||||
$conditions[] = $this->buildCondition($cond, $params);
|
||||
}
|
||||
return "(" . implode(" OR ", $conditions) . ")";
|
||||
} else if ($condition instanceof CondAnd) {
|
||||
$conditions = array();
|
||||
foreach($condition->getConditions() as $cond) {
|
||||
$conditions[] = $this->buildCondition($cond, $params);
|
||||
}
|
||||
return "(" . implode(" AND ", $conditions) . ")";
|
||||
} else if ($condition instanceof Compare) {
|
||||
$column = $this->columnName($condition->getColumn());
|
||||
$value = $condition->getValue();
|
||||
$operator = $condition->getOperator();
|
||||
|
||||
if ($value === null) {
|
||||
if ($operator === "=") {
|
||||
return "$column IS NULL";
|
||||
} else if ($operator === "!=") {
|
||||
return "$column IS NOT NULL";
|
||||
}
|
||||
}
|
||||
|
||||
return $column . $operator . $this->addValue($value, $params);
|
||||
} else if ($condition instanceof CondBool) {
|
||||
return $this->columnName($condition->getValue());
|
||||
} else if (is_array($condition)) {
|
||||
if (count($condition) === 1) {
|
||||
return $this->buildCondition($condition[0], $params);
|
||||
} else {
|
||||
$conditions = array();
|
||||
foreach ($condition as $cond) {
|
||||
$conditions[] = $this->buildCondition($cond, $params);
|
||||
}
|
||||
return implode(" AND ", $conditions);
|
||||
}
|
||||
} else if($condition instanceof CondIn) {
|
||||
|
||||
$needle = $condition->getNeedle();
|
||||
$haystack = $condition->getHaystack();
|
||||
if (is_array($haystack)) {
|
||||
$values = array();
|
||||
foreach ($haystack as $value) {
|
||||
$values[] = $this->addValue($value, $params);
|
||||
}
|
||||
|
||||
$values = implode(",", $values);
|
||||
} else if($haystack instanceof Select) {
|
||||
$values = $haystack->build($params);
|
||||
} else {
|
||||
$this->lastError = $this->logger->error("Unsupported in-expression value: " . get_class($condition));
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($needle instanceof Column) {
|
||||
$lhs = $this->createExpression($needle, $params);
|
||||
} else {
|
||||
$lhs = $this->addValue($needle, $params);
|
||||
}
|
||||
|
||||
return "$lhs IN ($values)";
|
||||
} else if($condition instanceof CondKeyword) {
|
||||
$left = $condition->getLeftExp();
|
||||
$right = $condition->getRightExp();
|
||||
$keyword = $condition->getKeyword();
|
||||
$left = ($left instanceof Column) ? $this->columnName($left->getName()) : $this->addValue($left, $params);
|
||||
$right = ($right instanceof Column) ? $this->columnName($right->getName()) : $this->addValue($right, $params);
|
||||
return "$left $keyword $right ";
|
||||
} else if($condition instanceof CondNot) {
|
||||
$expression = $condition->getExpression();
|
||||
if ($expression instanceof Condition) {
|
||||
$expression = $this->buildCondition($expression, $params);
|
||||
} else {
|
||||
$expression = $this->columnName($expression);
|
||||
}
|
||||
|
||||
return "NOT $expression";
|
||||
} else if ($condition instanceof CondNull) {
|
||||
return $this->columnName($condition->getColumn()) . " IS NULL";
|
||||
} else if ($condition instanceof Exists) {
|
||||
return "EXISTS(" .$condition->getSubQuery()->build($params) . ")";
|
||||
} else {
|
||||
$this->lastError = $this->logger->error("Unsupported condition type: " . gettype($condition));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected function createExpression(Expression $exp, array &$params): ?string {
|
||||
if ($exp instanceof Column) {
|
||||
return $this->columnName($exp->getName());
|
||||
} else if ($exp instanceof Query) {
|
||||
return "(" . $exp->build($params) . ")";
|
||||
} else if ($exp instanceof CaseWhen) {
|
||||
$condition = $this->buildCondition($exp->getCondition(), $params);
|
||||
|
||||
// psql requires constant values here
|
||||
$trueCase = $this->addValue($exp->getTrueCase(), $params, true);
|
||||
$falseCase = $this->addValue($exp->getFalseCase(), $params, true);
|
||||
|
||||
return "CASE WHEN $condition THEN $trueCase ELSE $falseCase END";
|
||||
} else if ($exp instanceof Sum) {
|
||||
$value = $this->addValue($exp->getValue(), $params);
|
||||
$alias = $this->columnName($exp->getAlias());
|
||||
return "SUM($value) AS $alias";
|
||||
} else {
|
||||
$this->lastError = $this->logger->error("Unsupported expression type: " . get_class($exp));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function setLastError($str) {
|
||||
$this->lastError = $str;
|
||||
}
|
||||
|
||||
public function getLastInsertId(): int {
|
||||
return $this->lastInsertId;
|
||||
}
|
||||
|
||||
public function close() {
|
||||
$this->disconnect();
|
||||
$this->connection = NULL;
|
||||
}
|
||||
|
||||
public static function createConnection(ConnectionData $connectionData) {
|
||||
$type = $connectionData->getProperty("type");
|
||||
if ($type === "mysql") {
|
||||
$sql = new MySQL($connectionData);
|
||||
} else if ($type === "postgres") {
|
||||
$sql = new PostgreSQL($connectionData);
|
||||
} else {
|
||||
Logger::instance()->error("Unknown database type: $type");
|
||||
return "Unknown database type";
|
||||
}
|
||||
|
||||
if ($sql->checkRequirements()) {
|
||||
$sql->connect();
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
public abstract function getStatus();
|
||||
|
||||
public function parseBool($val) : bool {
|
||||
return in_array($val, array(true, 1, '1', 't', 'true', 'TRUE'), true);
|
||||
}
|
||||
}
|
||||
10
Core/Driver/SQL/Strategy/CascadeStrategy.class.php
Normal file
10
Core/Driver/SQL/Strategy/CascadeStrategy.class.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Strategy;
|
||||
|
||||
class CascadeStrategy extends Strategy {
|
||||
|
||||
public function __construct() {
|
||||
}
|
||||
|
||||
}
|
||||
10
Core/Driver/SQL/Strategy/SetDefaultStrategy.class.php
Normal file
10
Core/Driver/SQL/Strategy/SetDefaultStrategy.class.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Strategy;
|
||||
|
||||
class SetDefaultStrategy extends Strategy {
|
||||
|
||||
public function __construct() {
|
||||
}
|
||||
|
||||
}
|
||||
10
Core/Driver/SQL/Strategy/SetNullStrategy.class.php
Normal file
10
Core/Driver/SQL/Strategy/SetNullStrategy.class.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Strategy;
|
||||
|
||||
class SetNullStrategy extends Strategy {
|
||||
|
||||
public function __construct() {
|
||||
}
|
||||
|
||||
}
|
||||
7
Core/Driver/SQL/Strategy/Strategy.class.php
Normal file
7
Core/Driver/SQL/Strategy/Strategy.class.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Strategy;
|
||||
|
||||
abstract class Strategy {
|
||||
|
||||
}
|
||||
22
Core/Driver/SQL/Strategy/UpdateStrategy.class.php
Normal file
22
Core/Driver/SQL/Strategy/UpdateStrategy.class.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Strategy;
|
||||
|
||||
class UpdateStrategy extends Strategy {
|
||||
|
||||
private array $values;
|
||||
private array $conflictingColumns;
|
||||
|
||||
public function __construct(array $conflictingColumns, array $values) {
|
||||
$this->conflictingColumns = $conflictingColumns;
|
||||
$this->values = $values;
|
||||
}
|
||||
|
||||
public function getConflictingColumns(): array {
|
||||
return $this->conflictingColumns;
|
||||
}
|
||||
|
||||
public function getValues(): array {
|
||||
return $this->values;
|
||||
}
|
||||
}
|
||||
14
Core/Driver/SQL/Type/CurrentColumn.php
Normal file
14
Core/Driver/SQL/Type/CurrentColumn.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Core\Driver\SQL\Type;
|
||||
|
||||
|
||||
use Core\Driver\SQL\Column\Column;
|
||||
|
||||
class CurrentColumn extends Column {
|
||||
|
||||
public function __construct(string $string) {
|
||||
parent::__construct($string);
|
||||
}
|
||||
}
|
||||
11
Core/Driver/SQL/Type/CurrentTable.class.php
Normal file
11
Core/Driver/SQL/Type/CurrentTable.class.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Type;
|
||||
|
||||
use Core\Driver\SQL\Column\StringColumn;
|
||||
|
||||
class CurrentTable extends StringColumn {
|
||||
public function __construct() {
|
||||
parent::__construct("CURRENT_TABLE");
|
||||
}
|
||||
}
|
||||
11
Core/Driver/SQL/Type/Trigger.class.php
Normal file
11
Core/Driver/SQL/Type/Trigger.class.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Type;
|
||||
|
||||
use Core\Driver\SQL\Keyword;
|
||||
|
||||
class Trigger extends Keyword {
|
||||
public function __construct() {
|
||||
parent::__construct("TRIGGER");
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user