Init
This commit is contained in:
commit
adc1c38d54
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
data/classes/Setting.php
|
||||
data/cache
|
||||
data/tmp
|
||||
data/qr
|
||||
.idea
|
6
Makefile
Normal file
6
Makefile
Normal file
@ -0,0 +1,6 @@
|
||||
build:
|
||||
php data/scripts/generate.php
|
||||
|
||||
clean:
|
||||
rm -rf data/cache/* data/tmp/*
|
||||
|
15
api/v1/index.php
Normal file
15
api/v1/index.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
require '../../data/classes/core/Autoloader.php';
|
||||
|
||||
$autoloader = new Autoloader('../../data/cache/');
|
||||
|
||||
$session = new Session();
|
||||
|
||||
$router = new Router($_SERVER['REQUEST_URI'], $_SERVER['REQUEST_METHOD']);
|
||||
|
||||
if (isset($_SERVER['HTTP_CONTENT_TYPE'])) {
|
||||
$router->setRequestBody($_SERVER['HTTP_CONTENT_TYPE'], file_get_contents('php://input'));
|
||||
}
|
||||
|
||||
$router->route();
|
13
data/classes/api/ApiBadRequestResponse.php
Normal file
13
data/classes/api/ApiBadRequestResponse.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class ApiBadRequestResponse extends ApiResponse
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->setParameter('success', false);
|
||||
$this->setStatus(self::STATUS_BAD_REQUEST);
|
||||
}
|
||||
}
|
27
data/classes/api/ApiJsonResponse.php
Normal file
27
data/classes/api/ApiJsonResponse.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
final class ApiJsonResponse extends ApiResponse
|
||||
{
|
||||
protected string $mimeType = MimeType::JSON;
|
||||
|
||||
public function __construct(int $status = ServerStatus::OK)
|
||||
{
|
||||
parent::__construct($status);
|
||||
|
||||
$this->setParameter('success', true);
|
||||
}
|
||||
|
||||
public function setResult(JsonSerializable $result): void
|
||||
{
|
||||
$this->setParameter('result', $result->jsonSerialize());
|
||||
}
|
||||
|
||||
public function respond(): void
|
||||
{
|
||||
parent::respond();
|
||||
|
||||
echo json_encode($this->parameters);
|
||||
}
|
||||
}
|
77
data/classes/api/ApiResponse.php
Normal file
77
data/classes/api/ApiResponse.php
Normal file
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class ApiResponse implements JsonSerializable
|
||||
{
|
||||
public const STATUS_OK = 200;
|
||||
public const STATUS_FORBIDDEN = 403;
|
||||
public const STATUS_UNAUTHORIZED = 401;
|
||||
public const STATUS_BAD_REQUEST = 400;
|
||||
public const STATUS_NOT_FOUND = 404;
|
||||
public const STATUS_SERVER_ERROR = 500;
|
||||
|
||||
public const MIME_TYPE_PLAINTEXT = 'text/plain';
|
||||
public const MIME_TYPE_JSON = 'application/json';
|
||||
public const MIME_TYPE_SVG = 'image/svg+xml';
|
||||
|
||||
protected int $status = ServerStatus::OK;
|
||||
protected string $mimeType = MimeType::PLAINTEXT;
|
||||
protected array $parameters = [];
|
||||
|
||||
public function __construct(int $status = ServerStatus::OK)
|
||||
{
|
||||
$this->setStatus($status);
|
||||
}
|
||||
|
||||
public function setParameter(string $key, $value): void
|
||||
{
|
||||
$this->parameters[$key] = $value;
|
||||
}
|
||||
|
||||
public function setStatus(int $status): void
|
||||
{
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
public function setMessage(string $message): void
|
||||
{
|
||||
$this->setParameter('message', $message);
|
||||
}
|
||||
|
||||
public function setMimeType(string $mimeType): void
|
||||
{
|
||||
$this->mimeType = $mimeType;
|
||||
}
|
||||
|
||||
public function setBody(JsonSerializable $data): void
|
||||
{
|
||||
$this->parameters = $data->jsonSerialize();
|
||||
}
|
||||
|
||||
public function SetMessageIdNotFound(string $instanceName): void
|
||||
{
|
||||
$this->setMessage(sprintf('Die für %s angeforderte ID existiert nicht!', $instanceName));
|
||||
}
|
||||
|
||||
public function getStatus(): int
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function getMimeType(): string
|
||||
{
|
||||
return $this->mimeType;
|
||||
}
|
||||
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return $this->parameters;
|
||||
}
|
||||
|
||||
public function respond(): void
|
||||
{
|
||||
http_response_code($this->status);
|
||||
header('Content-Type: ' . $this->mimeType);
|
||||
}
|
||||
}
|
12
data/classes/api/ApiSuccessResponse.php
Normal file
12
data/classes/api/ApiSuccessResponse.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class ApiSuccessResponse extends ApiResponse
|
||||
{
|
||||
public function __construct(bool $success = true)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->setParameter('success', $success);
|
||||
}
|
||||
}
|
26
data/classes/api/ApiSvgResponse.php
Normal file
26
data/classes/api/ApiSvgResponse.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
final class ApiSvgResponse extends ApiResponse
|
||||
{
|
||||
private string $content;
|
||||
protected string $mimeType = MimeType::SVG;
|
||||
|
||||
public function __construct(int $status = ServerStatus::OK)
|
||||
{
|
||||
parent::__construct($status);
|
||||
}
|
||||
|
||||
public function setContent(string $content): void
|
||||
{
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
public function respond(): void
|
||||
{
|
||||
parent::respond();
|
||||
header('Content-Length: ' . strlen($this->content));
|
||||
echo $this->content;
|
||||
}
|
||||
}
|
12
data/classes/api/ApiUnauthorizedResponse.php
Normal file
12
data/classes/api/ApiUnauthorizedResponse.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class ApiUnauthorizedResponse extends ApiResponse
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->setStatus(self::STATUS_UNAUTHORIZED);
|
||||
}
|
||||
}
|
32
data/classes/controller/FingerprintGetController.php
Normal file
32
data/classes/controller/FingerprintGetController.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
final class FingerprintGetController extends AbstractController
|
||||
{
|
||||
protected string $route = '/api/v1/fingerprint/{fingerprintId}';
|
||||
|
||||
private int $fingerprintId;
|
||||
|
||||
public function __construct(string $url)
|
||||
{
|
||||
parent::__construct($url);
|
||||
|
||||
$this->fingerprintId = (int)$this->getUrlParamInt('fingerprintId');
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
$fingerprint = new Fingerprint($this->fingerprintId);
|
||||
|
||||
$this->response = new ApiJsonResponse();
|
||||
$this->response->setResult($fingerprint->jsonSerialize());
|
||||
} catch (Throwable $e) {
|
||||
$this->response = new ApiJsonResponse(ApiResponse::STATUS_NOT_FOUND);
|
||||
$this->response->setParameter('success', false);
|
||||
$this->response->setMessage(sprintf('No fingerprint with id %d found!', $this->fingerprintId));
|
||||
$this->response->setMimeType(ApiResponse::MIME_TYPE_JSON);
|
||||
}
|
||||
}
|
||||
}
|
63
data/classes/controller/FingerprintPostController.php
Normal file
63
data/classes/controller/FingerprintPostController.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
final class FingerprintPostController extends AbstractController
|
||||
{
|
||||
protected string $route = '/api/v1/fingerprint';
|
||||
protected array $mandatoryAttributes = [
|
||||
'fingerprint',
|
||||
'userId',
|
||||
];
|
||||
|
||||
public function __construct(string $url)
|
||||
{
|
||||
parent::__construct($url);
|
||||
|
||||
$this->response = new ApiJsonResponse();
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
parent::handle();
|
||||
|
||||
$json = json_decode($this->requestBody);
|
||||
$fingerprint = new Fingerprint();
|
||||
|
||||
try {
|
||||
$fingerprint->setFingerprint($json->fingerprint);
|
||||
$fingerprint->setUserId($json->userId);
|
||||
$fingerprint->Save();
|
||||
|
||||
$qrCode = new QrCode($fingerprint->getFingerprintId(), $fingerprint->getFingerprint());
|
||||
$qrCode->generate();
|
||||
$qrCode->save();
|
||||
|
||||
$this->response->setParameter('fingerprintId', $fingerprint->getFingerprintId());
|
||||
} catch (QrCodeException $e) {
|
||||
$fingerprint->Delete();
|
||||
|
||||
$this->response->setParameter('success', false);
|
||||
$this->response->setStatus(ServerStatus::INTERNAL_ERROR);
|
||||
$this->response->setMessage('An error occured during qr code creation!');
|
||||
} catch (Throwable $e) {
|
||||
$this->catchDatabaseException($e->getMessage(), $json);
|
||||
}
|
||||
}
|
||||
|
||||
private function catchDatabaseException(string $message, object $json): void
|
||||
{
|
||||
$this->response->setParameter('success', false);
|
||||
|
||||
if (substr_count($message, 'foreign key constraint fails') > 0) {
|
||||
$this->response->setMessage(sprintf('User with id %d doesn\'t exist!', $json->userId));
|
||||
$this->response->setStatus(ServerStatus::NOT_FOUND);
|
||||
} elseif (substr_count($message, 'Duplicate entry') > 0) {
|
||||
$this->response->setMessage(sprintf('Fingerprint %s already exists!', $json->fingerprint));
|
||||
$this->response->setStatus(ServerStatus::BAD_REQUEST);
|
||||
} else {
|
||||
$this->response->setMessage($message);
|
||||
$this->response->setStatus(ServerStatus::INTERNAL_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
35
data/classes/controller/QrCodeGetController.php
Normal file
35
data/classes/controller/QrCodeGetController.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
final class QrCodeGetController extends AbstractController
|
||||
{
|
||||
protected string $route = '/api/v1/fingerprint/{fingerprintId}/qr';
|
||||
|
||||
private int $fingerprintId;
|
||||
|
||||
public function __construct(string $url)
|
||||
{
|
||||
parent::__construct($url);
|
||||
|
||||
$this->fingerprintId = (int)$this->getUrlParamInt('fingerprintId');
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$filename = Setting::PATH_QR_CODES . (string)$this->fingerprintId . '.svg';
|
||||
|
||||
if (!is_file($filename)) {
|
||||
$this->response = new ApiJsonResponse();
|
||||
$this->response->setParameter('success', false);
|
||||
$this->response->setMessage('No QR code for fingerprint id %d found!');
|
||||
$this->response->setMimeType(ApiResponse::MIME_TYPE_JSON);
|
||||
}
|
||||
|
||||
$this->response = new ApiSvgResponse();
|
||||
|
||||
$file = fopen($filename, 'r');
|
||||
$this->response->setContent(fread($file, filesize($filename)));
|
||||
fclose($file);
|
||||
}
|
||||
}
|
25
data/classes/controller/UserGetController.php
Normal file
25
data/classes/controller/UserGetController.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
final class UserGetController extends AbstractController
|
||||
{
|
||||
protected string $route = '/api/v1/user/{userId}';
|
||||
|
||||
private int $userId;
|
||||
|
||||
public function __construct(string $url)
|
||||
{
|
||||
parent::__construct($url);
|
||||
|
||||
$this->userId = (int)$this->getUrlParamInt('userId');
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$user = new User($this->userId);
|
||||
|
||||
$this->response = new ApiJsonResponse();
|
||||
$this->response->setResult($user);
|
||||
}
|
||||
}
|
43
data/classes/controller/UserLoginPutController.php
Normal file
43
data/classes/controller/UserLoginPutController.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
final class UserLoginPutController extends AbstractController
|
||||
{
|
||||
protected string $route = '/api/v1/user/login';
|
||||
protected array $mandatoryAttributes = [
|
||||
'username',
|
||||
'password',
|
||||
];
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
parent::handle();
|
||||
|
||||
if ($this->response->getStatus() !== ServerStatus::OK) {
|
||||
return;
|
||||
}
|
||||
|
||||
$json = json_decode($this->requestBody);
|
||||
|
||||
$session = new Session();
|
||||
|
||||
if ($session->IsLoggedIn()) {
|
||||
$this->response = new ApiJsonResponse(ServerStatus::BAD_REQUEST);
|
||||
$this->response->setParameter('success', false);
|
||||
$this->response->setMessage('You are already logged in!');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$session->Login($json->username, $json->password)) {
|
||||
$this->response = new ApiJsonResponse(ServerStatus::UNAUTHORIZED);
|
||||
$this->response->setParameter('success', false);
|
||||
$this->response->setMessage('Login failed!');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->response = new ApiJsonResponse();
|
||||
}
|
||||
}
|
28
data/classes/controller/UserLogoutPutController.php
Normal file
28
data/classes/controller/UserLogoutPutController.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
final class UserLogoutPutController extends AbstractController
|
||||
{
|
||||
protected string $route = '/api/v1/user/logout';
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
parent::handle();
|
||||
|
||||
$session = new Session();
|
||||
|
||||
if (!$session->IsLoggedIn()) {
|
||||
$this->response = new ApiJsonResponse(ServerStatus::BAD_REQUEST);
|
||||
$this->response->setParameter('success', false);
|
||||
$this->response->setMessage('You were not logged in!');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$session->Destroy();
|
||||
|
||||
$this->response = new ApiJsonResponse();
|
||||
$this->response->setParameter('success', true);
|
||||
}
|
||||
}
|
83
data/classes/core/AbstractController.php
Normal file
83
data/classes/core/AbstractController.php
Normal file
@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
abstract class AbstractController
|
||||
{
|
||||
protected string $route;
|
||||
protected ApiResponse $response;
|
||||
|
||||
protected string $requestUrl;
|
||||
protected ?string $requestBody = null;
|
||||
protected ?string $contentType = null;
|
||||
protected array $mandatoryAttributes = [];
|
||||
|
||||
public function __construct(string $url)
|
||||
{
|
||||
$this->requestUrl = $url;
|
||||
$this->response = new ApiResponse();
|
||||
}
|
||||
|
||||
public function setRequestBody(string $contentType, string $content): void
|
||||
{
|
||||
$this->requestBody = $content;
|
||||
$this->contentType = $contentType;
|
||||
}
|
||||
|
||||
public function getResponse(): ApiResponse
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
if (!$this->validateJsonBody()) {
|
||||
$this->response = new ApiJsonResponse(ServerStatus::BAD_REQUEST);
|
||||
$this->response->setParameter('success', false);
|
||||
$this->response->setMessage('The request body has not the required json attributes!');
|
||||
}
|
||||
}
|
||||
|
||||
protected function getUrlParam(string $name): ?string
|
||||
{
|
||||
foreach (explode('/', $this->route) as $index => $fragment) {
|
||||
if ($fragment === '{' . $name . '}') {
|
||||
return explode('/', $this->requestUrl)[$index];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function getUrlParamInt(string $name): ?int
|
||||
{
|
||||
$param = $this->getUrlParam($name);
|
||||
|
||||
return $param !== null ? (int)$param : null;
|
||||
}
|
||||
|
||||
protected function validateJsonBody(): bool
|
||||
{
|
||||
if (count($this->mandatoryAttributes) === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->contentType === MimeType::JSON && $this->requestBody === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$json = json_decode($this->requestBody);
|
||||
|
||||
foreach ($this->mandatoryAttributes as $attribute) {
|
||||
if (!isset($json->{$attribute})) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
245
data/classes/core/Autoloader.php
Normal file
245
data/classes/core/Autoloader.php
Normal file
@ -0,0 +1,245 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class Autoloader
|
||||
{
|
||||
private const PATH_CLASSES = 'data/classes';
|
||||
private const PATH_CONTROLLERS = self::PATH_CLASSES . '/controller';
|
||||
private const PATH_CACHE = 'data/cache/';
|
||||
|
||||
public function __construct(string $cachePath = self::PATH_CACHE)
|
||||
{
|
||||
if ($cachePath !== self::PATH_CACHE) {
|
||||
$cachePath = substr($cachePath, -1) === '/' ? $cachePath : $cachePath . '/';
|
||||
}
|
||||
|
||||
$routesFound = @include($cachePath . 'routes.php');
|
||||
$classesFound = @include($cachePath . 'classes.php');
|
||||
|
||||
if (!$routesFound || !$classesFound) {
|
||||
throw new Exception(
|
||||
sprintf(
|
||||
'Autoloader cache not found! Please generate it with %s::BuildCache() at first!',
|
||||
self::class
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
spl_autoload_register(
|
||||
function (string $className) {
|
||||
if (!$this->loadClass($className)) {
|
||||
throw new Exception(sprintf('Class %s couldn\'t be loaded!', $className));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public static function BuildCache(): void
|
||||
{
|
||||
self::BuildClassCache();
|
||||
self::BuildRouteCache();
|
||||
}
|
||||
|
||||
public static function BuildClassCache(): void
|
||||
{
|
||||
$classesResult = self::scanForClasses();
|
||||
|
||||
$cacheContent = '';
|
||||
|
||||
foreach ($classesResult as $className => $path) {
|
||||
$cacheContent .= sprintf("\t\t'%s' => '%s',\n", $className, $path);
|
||||
}
|
||||
|
||||
$cacheContent .= "\t]\n);";
|
||||
|
||||
self::buildCacheFile($cacheContent, 'classes');
|
||||
}
|
||||
|
||||
private function loadClass(string $className): bool
|
||||
{
|
||||
if (!isset(CLASSES[$className]) || !@include(CLASSES[$className])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function BuildRouteCache(): void
|
||||
{
|
||||
$controllersResult = self::scanForControllers();
|
||||
$controllerMethods = [
|
||||
'GET' => [],
|
||||
'POST' => [],
|
||||
'PUT' => [],
|
||||
];
|
||||
|
||||
foreach ($controllersResult as $className => $path) {
|
||||
$file = fopen($path, 'r');
|
||||
$content = fread($file, filesize($path));
|
||||
fclose($file);
|
||||
|
||||
preg_match_all('/(?<=private )\w+ \$\w+(?=;)/', $content, $matches);
|
||||
|
||||
$params = [];
|
||||
|
||||
foreach ($matches[0] as $match) {
|
||||
$parts = explode(' ', $match);
|
||||
$params[] = [
|
||||
'type' => $parts[0],
|
||||
'name' => $parts[1],
|
||||
];
|
||||
}
|
||||
|
||||
preg_match('/(?<=protected string \$route = \').*(?=\';)/', $content, $matches);
|
||||
$route = $matches[0];
|
||||
|
||||
preg_match('/[A-Z][a-z]+(?=Controller)/', $className, $matches);
|
||||
$method = strtoupper($matches[0]);
|
||||
|
||||
$controllerMethods[$method][$route] = [
|
||||
'name' => $className,
|
||||
'params' => $params,
|
||||
];
|
||||
}
|
||||
|
||||
$cacheContent = '';
|
||||
|
||||
foreach ($controllerMethods as $method => $controllers) {
|
||||
$cacheContent .= self::createRoutesForMethod($method, $controllers);
|
||||
}
|
||||
|
||||
$cacheContent .= "\t]\n);";
|
||||
|
||||
self::buildCacheFile($cacheContent, 'routes');
|
||||
}
|
||||
|
||||
private static function createRoutesForMethod(string $method, array $routes): string
|
||||
{
|
||||
krsort($routes);
|
||||
$stringRoutes = '';
|
||||
|
||||
foreach ($routes as $route => $params) {
|
||||
$stringRoutes .= sprintf(
|
||||
"'%s' => [
|
||||
'controller' => %s::class,
|
||||
'params' => [
|
||||
%s
|
||||
],
|
||||
],
|
||||
",
|
||||
$route,
|
||||
$params['name'],
|
||||
self::createRouteParams($params['params'])
|
||||
);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
"
|
||||
'%s' => [
|
||||
%s
|
||||
],
|
||||
",
|
||||
$method,
|
||||
$stringRoutes,
|
||||
);
|
||||
}
|
||||
|
||||
private static function createRouteParams(array $params): string
|
||||
{
|
||||
$string = '';
|
||||
|
||||
foreach ($params as $param) {
|
||||
$string .= sprintf(
|
||||
"
|
||||
'%s' => [
|
||||
'type' => '%s',
|
||||
],
|
||||
",
|
||||
str_replace('$', '', $param['name']),
|
||||
$param['type']
|
||||
);
|
||||
}
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
private static function reformatCacheFileContent(string $content): string
|
||||
{
|
||||
$depth = 0;
|
||||
$reformatted = '';
|
||||
$replace = '';
|
||||
|
||||
// Removing indents
|
||||
foreach (explode("\n", $content) as $line) {
|
||||
$trim = trim($line);
|
||||
|
||||
if ($trim !== '') {
|
||||
$replace .= $trim . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
for ($i = 0; $i < strlen($replace); $i++) {
|
||||
if (in_array($replace[$i], [')', ']'])) {
|
||||
$depth--;
|
||||
}
|
||||
|
||||
if ($replace[$i - 1] === "\n") {
|
||||
$reformatted .= str_repeat("\t", $depth);
|
||||
}
|
||||
|
||||
$reformatted .= $replace[$i];
|
||||
|
||||
if (in_array($replace[$i], ['(', '['])) {
|
||||
$depth++;
|
||||
}
|
||||
}
|
||||
|
||||
return $reformatted;
|
||||
}
|
||||
|
||||
private static function buildCacheFile(string $content, string $cacheName): void
|
||||
{
|
||||
$cacheContent = sprintf(
|
||||
"<?php\n\n/*\n * This file was auto generated on %s\n */\n\ndefine(\n\t'%s',\n\t[\n",
|
||||
(new DateTime())->format('Y-m-d H:i:s'),
|
||||
strtoupper($cacheName)
|
||||
);
|
||||
|
||||
$cacheContent .= $content;
|
||||
|
||||
$file = fopen(getcwd() . '/' . self::PATH_CACHE . $cacheName . '.php', 'w');
|
||||
fwrite($file, self::reformatCacheFileContent($cacheContent));
|
||||
fclose($file);
|
||||
}
|
||||
|
||||
private static function scanForFiles(string $folder): array
|
||||
{
|
||||
$folder = substr($folder, -1) === '/' ? substr($folder, 0, -1) : $folder;
|
||||
$files = [];
|
||||
$handler = opendir($folder);
|
||||
|
||||
while ($file = readdir($handler)) {
|
||||
$path = $folder . '/' . $file;
|
||||
|
||||
if (is_dir($path) && $file !== '.' && $file !== '..') {
|
||||
$files = array_merge($files, self::scanForFiles($path));
|
||||
} elseif (is_file($path) && substr($path, -4) === '.php') {
|
||||
$className = substr($file, 0, -4);
|
||||
$files[$className] = $path;
|
||||
}
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
private static function scanForClasses(): array
|
||||
{
|
||||
return self::scanForFiles(getcwd() . '/' . self::PATH_CLASSES);
|
||||
}
|
||||
|
||||
private static function scanForControllers(): array
|
||||
{
|
||||
return self::scanForFiles(getcwd() . '/' . self::PATH_CONTROLLERS);
|
||||
}
|
||||
}
|
60
data/classes/core/DatabaseInterface.php
Normal file
60
data/classes/core/DatabaseInterface.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
interface DatabaseInterface
|
||||
{
|
||||
public const ORDER_ASC = true;
|
||||
public const ORDER_DESC = false;
|
||||
|
||||
/**
|
||||
* Has to close the connection.
|
||||
*/
|
||||
public function __destruct();
|
||||
|
||||
/**
|
||||
* Sends an sql query to the database.
|
||||
*/
|
||||
public function Query(string $query, array $params = []): void;
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getResult(): array;
|
||||
|
||||
/**
|
||||
* Selects data from a table.
|
||||
*/
|
||||
public function Select(
|
||||
string $tableName,
|
||||
array $fields = [],
|
||||
array $conditions = [],
|
||||
int $limit = 0,
|
||||
array $orderBy = [],
|
||||
bool $asc = true,
|
||||
int $offset = 0
|
||||
): array;
|
||||
|
||||
/**
|
||||
* Deletes rows from a table.
|
||||
*/
|
||||
public function Delete(string $table, array $conditions): void;
|
||||
|
||||
/**
|
||||
* Inserts a new row into the table.
|
||||
*/
|
||||
public function Insert(string $table, array $fields): ?int;
|
||||
|
||||
/**
|
||||
* Edits data inside a table.
|
||||
*/
|
||||
public function Update(string $table, array $fields, array $conditions): void;
|
||||
|
||||
/**
|
||||
* Returns the number of entries found.
|
||||
*/
|
||||
public function Count(string $table, array $conditions = []): int;
|
||||
|
||||
/**
|
||||
* Returns the primary key from the last inserted row.
|
||||
*/
|
||||
public function GetLastInsertedId(): int;
|
||||
}
|
10
data/classes/core/MimeType.php
Normal file
10
data/classes/core/MimeType.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
final class MimeType
|
||||
{
|
||||
public const PLAINTEXT = 'text/plain';
|
||||
public const JSON = 'application/json';
|
||||
public const SVG = 'image/svg+xml';
|
||||
}
|
262
data/classes/core/MySqlDatabase.php
Normal file
262
data/classes/core/MySqlDatabase.php
Normal file
@ -0,0 +1,262 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class MySqlDatabase implements DatabaseInterface
|
||||
{
|
||||
private const CHARS_ALLOWED_IN_TABLE_NAMES = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890_-';
|
||||
|
||||
private ?PDO $connection;
|
||||
private ?PDOStatement $cursor;
|
||||
|
||||
public function __construct(
|
||||
string $hostname = Setting::MYSQL_HOST,
|
||||
string $user = Setting::MYSQL_USER,
|
||||
string $password = Setting::MYSQL_PASSWORD,
|
||||
string $database = Setting::MYSQL_DATABASE
|
||||
) {
|
||||
$this->connection = new PDO("mysql:host=$hostname;dbname=$database", $user, $password);
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
$this->connection = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function Query(string $query, array $params = []): void
|
||||
{
|
||||
$this->cursor = $this->connection->prepare($query);
|
||||
|
||||
if (!$this->cursor) {
|
||||
throw new Exception('Initialization of database cursor failed');
|
||||
}
|
||||
|
||||
foreach ($params as $key => $param) {
|
||||
if (is_bool($param)) {
|
||||
$param = (int)$param;
|
||||
}
|
||||
|
||||
$this->cursor->bindValue(':' . $key, $param);
|
||||
}
|
||||
|
||||
if (!$this->cursor->execute()) {
|
||||
throw new Exception($this->cursor->errorInfo()[2]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getResult(): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
while ($fetch = $this->cursor->fetchObject()) {
|
||||
$row = [];
|
||||
|
||||
foreach (get_object_vars($fetch) as $key => $value) {
|
||||
$row[$key] = $value;
|
||||
}
|
||||
|
||||
$result[] = $row;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects data from a table.
|
||||
*/
|
||||
public function Select(
|
||||
string $tableName,
|
||||
array $fields = [],
|
||||
array $conditions = [],
|
||||
int $limit = 0,
|
||||
array $orderBy = [],
|
||||
bool $asc = true,
|
||||
int $offset = 0
|
||||
): array {
|
||||
if (!self::isValidTableName($tableName)) {
|
||||
[];
|
||||
}
|
||||
|
||||
if (count($fields) === 0) {
|
||||
$fieldsExpression = '*';
|
||||
} else {
|
||||
$fieldsExpression = implode(',', $fields);
|
||||
}
|
||||
|
||||
$conditionsExpression = '';
|
||||
$conditionPairs = [];
|
||||
|
||||
foreach ($conditions as $field => $value) {
|
||||
$conditionPairs[] = sprintf('%s = :%s ', $field, $field);
|
||||
}
|
||||
|
||||
if (count($conditions) > 0) {
|
||||
$conditionsExpression = 'WHERE ';
|
||||
$conditionsExpression .= implode(' AND ', $conditionPairs);
|
||||
}
|
||||
|
||||
$orderStatement = '';
|
||||
|
||||
if (count($orderBy) > 0) {
|
||||
$orderStatement = 'ORDER BY ' . implode(',', $orderBy);
|
||||
|
||||
if (!$asc) {
|
||||
$orderStatement .= ' DESC';
|
||||
}
|
||||
}
|
||||
|
||||
$limitStatement = '';
|
||||
|
||||
if ($limit > 0) {
|
||||
$limitStatement = 'LIMIT ' . $limit;
|
||||
}
|
||||
|
||||
$offsetStatement = '';
|
||||
|
||||
if ($offset > 0) {
|
||||
$offsetStatement = 'OFFSET ' . $offset;
|
||||
}
|
||||
|
||||
$query = sprintf(
|
||||
'SELECT %s FROM %s %s %s %s %s',
|
||||
$fieldsExpression,
|
||||
$tableName,
|
||||
$conditionsExpression,
|
||||
$orderStatement,
|
||||
$limitStatement,
|
||||
$offsetStatement
|
||||
);
|
||||
|
||||
try {
|
||||
$this->Query($query, $conditions);
|
||||
} catch (Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes rows from a table.
|
||||
*/
|
||||
public function Delete(string $table, array $conditions): void
|
||||
{
|
||||
if (count($conditions) === 0) {
|
||||
$conditionsStatement = '1';
|
||||
} else {
|
||||
$conditionPairs = [];
|
||||
|
||||
foreach ($conditions as $field => $value) {
|
||||
$conditionPairs[] = sprintf('%s=:Condition%s', $field, $field);
|
||||
$conditions['Condition' . $field] = $value;
|
||||
unset($conditions[$field]);
|
||||
}
|
||||
|
||||
$conditionsStatement = implode(' AND ', $conditionPairs);
|
||||
}
|
||||
|
||||
$query = sprintf('DELETE FROM %s WHERE %s', $table, $conditionsStatement);
|
||||
|
||||
$this->Query($query, $conditions);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function Insert(string $table, array $fields): ?int
|
||||
{
|
||||
if (count($fields) === 0) {
|
||||
throw new Exception('Row to insert is empty!');
|
||||
}
|
||||
|
||||
$fieldNames = implode(',', array_keys($fields));
|
||||
$fieldPlaceholder = [];
|
||||
|
||||
foreach ($fields as $name => $value) {
|
||||
$fieldPlaceholder[] = ':' . $name;
|
||||
}
|
||||
|
||||
$query = sprintf(
|
||||
'INSERT INTO %s (%s) VALUES (%s)', $table, $fieldNames, implode(',', $fieldPlaceholder)
|
||||
);
|
||||
|
||||
$this->Query($query, $fields);
|
||||
|
||||
$lastInsertedId = $this->GetLastInsertedId();
|
||||
|
||||
if ((int)$lastInsertedId === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $lastInsertedId;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function Update(string $table, array $fields, array $conditions): void
|
||||
{
|
||||
$conditionPairs = [];
|
||||
|
||||
foreach ($conditions as $field => $value) {
|
||||
$conditionPairs[] = sprintf('%s=:Condition%s', $field, $field);
|
||||
$conditions['Condition' . $field] = $value;
|
||||
unset($conditions[$field]);
|
||||
}
|
||||
|
||||
$conditionsStatement = implode(' AND ', $conditionPairs);
|
||||
|
||||
$fieldPairs = [];
|
||||
|
||||
foreach ($fields as $field => $value) {
|
||||
$fieldPairs[] = sprintf('%s=:%s', $field, $field);
|
||||
}
|
||||
|
||||
$fieldsStatement = implode(',', $fieldPairs);
|
||||
|
||||
$query = sprintf('UPDATE %s SET %s WHERE %s', $table, $fieldsStatement, $conditionsStatement);
|
||||
|
||||
|
||||
$this->Query($query, array_merge($fields, $conditions));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function Count(string $table, array $conditions = []): int
|
||||
{
|
||||
$result = $this->Select($table, ['count(*)'], $conditions);
|
||||
|
||||
return (int)$result[0]['count(*)'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function GetLastInsertedId(): int
|
||||
{
|
||||
$this->Query('SELECT LAST_INSERT_ID() as ID');
|
||||
|
||||
return (int)$this->getResult()[0]['ID'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Does a check if the given table name contains forbidden chars.
|
||||
*/
|
||||
private static function isValidTableName(string $tableName): bool
|
||||
{
|
||||
foreach (str_split($tableName) as $char) {
|
||||
if (substr_count(self::CHARS_ALLOWED_IN_TABLE_NAMES, $char) === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
18
data/classes/core/MySqlTable.php
Normal file
18
data/classes/core/MySqlTable.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
class MySqlTable extends Table
|
||||
{
|
||||
public function __construct(string $tableName, $id = null, DatabaseInterface &$database = null)
|
||||
{
|
||||
self::EnsureConnection($database);
|
||||
|
||||
parent::__construct($tableName, $id, $database);
|
||||
}
|
||||
|
||||
public static function EnsureConnection(?DatabaseInterface & $database): void
|
||||
{
|
||||
if (!($database instanceof MySqlDatabase)) {
|
||||
$database = new MySqlDatabase();
|
||||
}
|
||||
}
|
||||
}
|
16
data/classes/core/Password.php
Normal file
16
data/classes/core/Password.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class Password
|
||||
{
|
||||
public static function IsValid(string $password, string $hash): bool
|
||||
{
|
||||
return password_verify($password, $hash);
|
||||
}
|
||||
|
||||
public static function GetHash(string $password): string
|
||||
{
|
||||
return password_hash($password, PASSWORD_BCRYPT);
|
||||
}
|
||||
}
|
67
data/classes/core/QrCode.php
Normal file
67
data/classes/core/QrCode.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
final class QrCode
|
||||
{
|
||||
private int $fingerprintId;
|
||||
private string $fingerprint;
|
||||
private string $temporaryFilename;
|
||||
|
||||
public function __construct(int $fingerprintId, string $fingerprint)
|
||||
{
|
||||
$this->fingerprintId = $fingerprintId;
|
||||
$this->fingerprint = $fingerprint;
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
if (!is_file($this->temporaryFilename)) {
|
||||
throw new QrCodeException(
|
||||
sprintf('Temporary QR file %s couldn\'t be found!', $this->temporaryFilename)
|
||||
);
|
||||
}
|
||||
|
||||
$returnCode = 0;
|
||||
|
||||
$path = substr(Setting::PATH_QR_CODES, -1) === '/'
|
||||
? Setting::PATH_QR_CODES
|
||||
: Setting::PATH_QR_CODES . '/';
|
||||
|
||||
$filename = $path . $this->fingerprintId . '.svg';
|
||||
|
||||
passthru(
|
||||
sprintf('mv %s %s', $this->temporaryFilename, $filename),
|
||||
$returnCode
|
||||
);
|
||||
|
||||
if ($returnCode !== 0 || !is_file($filename)) {
|
||||
throw new QrCodeException(
|
||||
sprintf('QR code for fingerprint %d couldn\'t be created!', $this->fingerprintId)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function generate(): bool
|
||||
{
|
||||
$returnCode = 0;
|
||||
|
||||
$path = substr(Setting::PATH_TMP, -1) === '/' ? Setting::PATH_TMP : Setting::PATH_TMP . '/';
|
||||
|
||||
$this->temporaryFilename = $path . $this->generateTemporaryFilename() . '.svg';
|
||||
|
||||
passthru(
|
||||
sprintf('qrencode -o %s -t SVG "%s"', $this->temporaryFilename, $this->fingerprint),
|
||||
$returnCode
|
||||
);
|
||||
|
||||
return !(bool)$returnCode;
|
||||
}
|
||||
|
||||
private function generateTemporaryFilename(): string
|
||||
{
|
||||
$hash = hash('md5', (new DateTime())->format('U') . $this->fingerprint);
|
||||
|
||||
return sprintf('%s.svg', $hash);
|
||||
}
|
||||
}
|
57
data/classes/core/Router.php
Normal file
57
data/classes/core/Router.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
final class Router
|
||||
{
|
||||
private string $route;
|
||||
private string $method;
|
||||
private ?string $requestBody = null;
|
||||
private ?string $contentType = null;
|
||||
|
||||
public function __construct(string $route, string $method)
|
||||
{
|
||||
$this->route = $route;
|
||||
$this->method = $method;
|
||||
}
|
||||
|
||||
public function setRequestBody(string $contentType, string $content): void
|
||||
{
|
||||
$this->contentType = $contentType;
|
||||
$this->requestBody = $content;
|
||||
}
|
||||
|
||||
public function route(): void
|
||||
{
|
||||
foreach (ROUTES[$this->method] as $route => $params) {
|
||||
preg_match_all($this->createRegex($route, $params['params']), $this->route, $matches);
|
||||
|
||||
if (count($matches[0]) > 0) {
|
||||
$class = new ReflectionClass($params['controller']);
|
||||
|
||||
$controller = $class->newInstance($matches[0][0]);
|
||||
|
||||
if ($this->requestBody !== null && $this->contentType !== null) {
|
||||
$controller->setRequestBody($this->contentType, $this->requestBody);
|
||||
}
|
||||
|
||||
$controller->handle();
|
||||
$controller->getResponse()->respond();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function createRegex(string $route, array $params): string
|
||||
{
|
||||
foreach ($params as $param => $values) {
|
||||
switch ($values['type']) {
|
||||
case 'int':
|
||||
$route = str_replace('{' . $param . '}', '[0-9]+', $route);
|
||||
}
|
||||
}
|
||||
|
||||
return '/' . str_replace('/', '\\/', $route) . '/';
|
||||
}
|
||||
}
|
13
data/classes/core/ServerStatus.php
Normal file
13
data/classes/core/ServerStatus.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class ServerStatus
|
||||
{
|
||||
public const OK = 200;
|
||||
public const FORBIDDEN = 403;
|
||||
public const UNAUTHORIZED = 401;
|
||||
public const BAD_REQUEST = 400;
|
||||
public const NOT_FOUND = 404;
|
||||
public const INTERNAL_ERROR = 500;
|
||||
}
|
108
data/classes/core/Session.php
Normal file
108
data/classes/core/Session.php
Normal file
@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
final class Session
|
||||
{
|
||||
public const TYPE_INT = 1;
|
||||
public const TYPE_STRING = 2;
|
||||
public const TYPE_BOOL = 3;
|
||||
|
||||
private const IS_LOGGED_IN = 'is_logged_in';
|
||||
private const USER_ID = 'account_id';
|
||||
private const USERNAME = 'username';
|
||||
private const PERMISSION = 'permission';
|
||||
private const EMAIL = 'email';
|
||||
private const JABBER_ADDRESS = 'jabber';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@session_start();
|
||||
|
||||
if (!$this->HasValue(self::IS_LOGGED_IN)) {
|
||||
$this->SetBool(self::IS_LOGGED_IN, false);
|
||||
}
|
||||
}
|
||||
|
||||
public function Destroy(): void
|
||||
{
|
||||
session_unset();
|
||||
session_destroy();
|
||||
}
|
||||
|
||||
public function Login(string $usernameOrEmail, string $password): bool
|
||||
{
|
||||
try {
|
||||
$user = User::getFromUsername($usernameOrEmail);
|
||||
} catch (Throwable $e) {
|
||||
$user = User::getFromEmail($usernameOrEmail);
|
||||
}
|
||||
|
||||
if ($user === null || !Password::IsValid($password, $user->getPassword())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->SetBool(self::IS_LOGGED_IN, true);
|
||||
$this->SetInt(self::USER_ID, $user->getPrimaryKey());
|
||||
$this->SetString(self::USERNAME, $user->getUsername());
|
||||
$this->SetString(self::EMAIL, $user->getEmail());
|
||||
$this->SetString(self::JABBER_ADDRESS, $user->getJabberAddress());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function HasValue(string $key): bool
|
||||
{
|
||||
return self::HasSession() && isset($_SESSION[$key]);
|
||||
}
|
||||
|
||||
public function SetBool(string $key, bool $value): void
|
||||
{
|
||||
$_SESSION[$key] = $value;
|
||||
}
|
||||
|
||||
public function SetString(string $key, string $value): void
|
||||
{
|
||||
$_SESSION[$key] = $value;
|
||||
}
|
||||
|
||||
public function SetInt(string $key, int $value): void
|
||||
{
|
||||
$_SESSION[$key] = $value;
|
||||
}
|
||||
|
||||
public function IsLoggedIn(): bool
|
||||
{
|
||||
return self::HasSession() && $this->GetBool(self::IS_LOGGED_IN);
|
||||
}
|
||||
|
||||
public function GetInt(string $key): ?int
|
||||
{
|
||||
return $this->HasValue($key) ? (int)$_SESSION[$key] : null;
|
||||
}
|
||||
|
||||
public function GetString(string $key): ?string
|
||||
{
|
||||
return $this->HasValue($key) ? (string)$_SESSION[$key] : null;
|
||||
}
|
||||
|
||||
public function GetBool(string $key): ?bool
|
||||
{
|
||||
return $this->HasValue($key) ? (bool)$_SESSION[$key] : null;
|
||||
}
|
||||
|
||||
public function GetAccountId(): ?int
|
||||
{
|
||||
return $this->GetInt(self::USER_ID);
|
||||
}
|
||||
|
||||
public function GetPermission(): ?int
|
||||
{
|
||||
return $this->GetInt(self::PERMISSION);
|
||||
}
|
||||
|
||||
public static function HasSession(): bool
|
||||
{
|
||||
return isset($_SESSION);
|
||||
}
|
||||
}
|
284
data/classes/core/Table.php
Normal file
284
data/classes/core/Table.php
Normal file
@ -0,0 +1,284 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Basic object to load a mysql table inside an object.
|
||||
*/
|
||||
abstract class Table
|
||||
{
|
||||
public const TYPE_STRING = 1;
|
||||
public const TYPE_INTEGER = 2;
|
||||
public const TYPE_FLOAT = 3;
|
||||
public const TYPE_DATETIME = 4;
|
||||
public const TYPE_BOOL = 5;
|
||||
|
||||
protected const VALUE = 'value';
|
||||
protected const TYPE = 'type';
|
||||
|
||||
protected ?DatabaseInterface $database;
|
||||
protected string $tableName;
|
||||
protected array $fields;
|
||||
protected string $primaryKey;
|
||||
protected bool $isPrimKeyManual = false;
|
||||
|
||||
public function __construct(string $tableName, $id, ?DatabaseInterface & $database)
|
||||
{
|
||||
$this->tableName = $tableName;
|
||||
$this->fields = [];
|
||||
|
||||
$this->database = $database;
|
||||
|
||||
$this->database->Query(sprintf('DESCRIBE %s', $tableName));
|
||||
|
||||
$result = $this->database->getResult();
|
||||
|
||||
foreach ($result as $field) {
|
||||
$sqlType = substr_count(
|
||||
$field['Type'], '(') === 0 ? $field['Type'] : strstr($field['Type'],
|
||||
'(',
|
||||
true
|
||||
);
|
||||
|
||||
switch ($sqlType) {
|
||||
case 'varchar':
|
||||
case 'char':
|
||||
case 'text':
|
||||
case 'longtext':
|
||||
case 'mediumtext':
|
||||
case 'tinytext':
|
||||
$type = self::TYPE_STRING;
|
||||
break;
|
||||
case 'int':
|
||||
case 'smallint':
|
||||
case 'mediumint':
|
||||
case 'bigint':
|
||||
$type = self::TYPE_INTEGER;
|
||||
break;
|
||||
case 'float':
|
||||
case 'decimal':
|
||||
case 'double':
|
||||
case 'real':
|
||||
$type = self::TYPE_FLOAT;
|
||||
break;
|
||||
case 'datetime':
|
||||
case 'date':
|
||||
$type = self::TYPE_DATETIME;
|
||||
break;
|
||||
case 'tinyint':
|
||||
$type = self::TYPE_BOOL;
|
||||
break;
|
||||
default:
|
||||
throw new Exception(sprintf('Type %s of field %s couldn\'t be handled', $sqlType, $field['Field']));
|
||||
}
|
||||
|
||||
$this->addField($field['Field'], $type);
|
||||
|
||||
if ($field['Key'] === 'PRI') {
|
||||
$this->primaryKey = $field['Field'];
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->isPrimKeyManual && $id !== null) {
|
||||
$this->loadById($id);
|
||||
}
|
||||
}
|
||||
|
||||
public function getPrimaryKey()
|
||||
{
|
||||
if ($this->primaryKey === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->getField($this->primaryKey);
|
||||
}
|
||||
|
||||
protected function addField(string $name, int $type): void
|
||||
{
|
||||
if (!self::IsValidType($type)) {
|
||||
throw new Exception(
|
||||
sprintf('Field %s has invalid type of %s!', $name, $type)
|
||||
);
|
||||
}
|
||||
|
||||
$this->fields[$name] = [self::VALUE => null, self::TYPE => $type];
|
||||
}
|
||||
|
||||
protected function loadById($id): void
|
||||
{
|
||||
try {
|
||||
$this->database->Query(
|
||||
sprintf('SELECT * FROM %s WHERE %s = :id', $this->tableName, $this->primaryKey),
|
||||
['id' => $id]
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
throw new Exception();
|
||||
}
|
||||
|
||||
$result = $this->database->getResult();
|
||||
|
||||
if (count($result) === 0) {
|
||||
throw new Exception('No table entry with id ' . $id . ' found!');
|
||||
}
|
||||
|
||||
foreach ($result[0] as $field => $value) {
|
||||
$this->setField($field, $value);
|
||||
}
|
||||
}
|
||||
|
||||
public function Flush(): void
|
||||
{
|
||||
$this->database->Delete($this->tableName, []);
|
||||
}
|
||||
|
||||
public function Delete(): void
|
||||
{
|
||||
try {
|
||||
$this->database->Delete($this->tableName, [$this->primaryKey => $this->getPrimaryKey()]);
|
||||
} catch (Throwable $e) {
|
||||
throw new Exception();
|
||||
}
|
||||
|
||||
foreach ($this->GetAllFieldNames() as $field) {
|
||||
$this->fields[$field][self::VALUE] = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected function getField(string $name)
|
||||
{
|
||||
if (!array_key_exists($name, $this->fields)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->fields[$name][self::VALUE];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value for the given field inside the database.
|
||||
*/
|
||||
protected function setField(string $name, $value): void
|
||||
{
|
||||
if (!$this->HasField($name)) {
|
||||
throw new Exception(sprintf('Field %s doesn\'t exist!', $name));
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
$this->fields[$name][self::VALUE] = null;
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($this->fields[$name][self::TYPE]) {
|
||||
case self::TYPE_STRING:
|
||||
$this->fields[$name][self::VALUE] = (string)$value;
|
||||
return;
|
||||
case self::TYPE_INTEGER:
|
||||
$this->fields[$name][self::VALUE] = (int)$value;
|
||||
return;
|
||||
case self::TYPE_FLOAT:
|
||||
$this->fields[$name][self::VALUE] = (float)$value;
|
||||
return;
|
||||
case self::TYPE_DATETIME:
|
||||
try {
|
||||
$this->fields[$name][self::VALUE] = new DateTime((string)$value);
|
||||
} catch (Exception $e) {
|
||||
throw new Exception();
|
||||
}
|
||||
return;
|
||||
case self::TYPE_BOOL:
|
||||
$this->fields[$name][self::VALUE] = (bool)$value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the table has the given column.
|
||||
*/
|
||||
public function HasField(string $name): bool
|
||||
{
|
||||
return array_key_exists($name, $this->fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the whole object into the database.
|
||||
*/
|
||||
public function Save(): void
|
||||
{
|
||||
$fields = [];
|
||||
|
||||
foreach ($this->GetAllFieldNames() as $fieldName) {
|
||||
$field = $this->getField($fieldName);
|
||||
|
||||
if ($field instanceof DateTime) {
|
||||
$fields[$fieldName] = $field->format('Y-m-d H:i:s');
|
||||
} else if (is_bool($field)) {
|
||||
$fields[$fieldName] = (int)$field;
|
||||
} else {
|
||||
$fields[$fieldName] = $field;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->isPrimKeyManual) {
|
||||
$this->saveWithManualId($fields);
|
||||
} else {
|
||||
$this->saveWithPrimaryKey($fields);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function GetAllFieldNames(): array
|
||||
{
|
||||
$fieldNames = [];
|
||||
|
||||
foreach ($this->fields as $name => $field) {
|
||||
$fieldNames[] = $name;
|
||||
}
|
||||
|
||||
return $fieldNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the index is a valid data type.
|
||||
*/
|
||||
public static function IsValidType(int $type): bool
|
||||
{
|
||||
$validTypes = [
|
||||
self::TYPE_STRING,
|
||||
self::TYPE_INTEGER,
|
||||
self::TYPE_FLOAT,
|
||||
self::TYPE_DATETIME,
|
||||
self::TYPE_BOOL,
|
||||
];
|
||||
|
||||
return in_array($type, $validTypes);
|
||||
}
|
||||
|
||||
protected function saveWithManualId(array $fields): void
|
||||
{
|
||||
if ($this->getField($this->primaryKey) === null) {
|
||||
throw new Exception('Manual primary key must not be null!');
|
||||
}
|
||||
|
||||
$hasKey = (bool)$this->database->Count(
|
||||
$this->tableName,
|
||||
[$this->primaryKey => $this->getField($this->primaryKey)]
|
||||
);
|
||||
|
||||
if ($hasKey) {
|
||||
$this->database->Update(
|
||||
$this->tableName, $fields, [$this->primaryKey => $this->getField($this->primaryKey)]
|
||||
);
|
||||
} else {
|
||||
$this->database->Insert($this->tableName, $fields);
|
||||
}
|
||||
}
|
||||
|
||||
protected function saveWithPrimaryKey(array $fields): void
|
||||
{
|
||||
if ($this->getField($this->primaryKey) !== null) {
|
||||
$this->database->Update(
|
||||
$this->tableName, $fields, [$this->primaryKey => $this->getField($this->primaryKey)]
|
||||
);
|
||||
} else {
|
||||
$this->setField($this->primaryKey, $this->database->Insert($this->tableName, $fields));
|
||||
}
|
||||
}
|
||||
}
|
53
data/classes/database/Fingerprint.php
Normal file
53
data/classes/database/Fingerprint.php
Normal file
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
final class Fingerprint extends MySqlTable implements JsonSerializable
|
||||
{
|
||||
public const FIELD_ID = 'FingerprintId';
|
||||
public const FIELD_FINGERPRINT = 'Fingerprint';
|
||||
public const FIELD_USER = 'UserId';
|
||||
|
||||
public function __construct($id = null, DatabaseInterface &$database = null)
|
||||
{
|
||||
parent::__construct(self::class, $id, $database);
|
||||
}
|
||||
|
||||
public function getFingerprintId(): ?int
|
||||
{
|
||||
if ($this->getPrimaryKey() === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int)$this->getPrimaryKey();
|
||||
}
|
||||
|
||||
public function getFingerprint(): string
|
||||
{
|
||||
return $this->getField(self::FIELD_FINGERPRINT);
|
||||
}
|
||||
|
||||
public function getUserId(): int
|
||||
{
|
||||
return $this->getField(self::FIELD_USER);
|
||||
}
|
||||
|
||||
public function setFingerprint(string $fingerprint): void
|
||||
{
|
||||
$this->setField(self::FIELD_FINGERPRINT, $fingerprint);
|
||||
}
|
||||
|
||||
public function setUserId(int $userId): void
|
||||
{
|
||||
$this->setField(self::FIELD_USER, $userId);
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'fingerprintId' => $this->getFingerprintId(),
|
||||
'fingerprint' => $this->getFingerprint(),
|
||||
'userId' => $this->getUserId()
|
||||
];
|
||||
}
|
||||
}
|
43
data/classes/database/Sharing.php
Normal file
43
data/classes/database/Sharing.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
final class Sharing extends MySqlTable
|
||||
{
|
||||
public const FIELD_USER = 'User';
|
||||
public const FIELD_USER_SHARED = 'UserShared';
|
||||
|
||||
public function __construct($id = null, DatabaseInterface &$database = null)
|
||||
{
|
||||
parent::__construct(self::class, $id, $database);
|
||||
}
|
||||
|
||||
public function getSharingId(): ?int
|
||||
{
|
||||
if ($this->getPrimaryKey() === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int)$this->getPrimaryKey();
|
||||
}
|
||||
|
||||
public function getUserId(): int
|
||||
{
|
||||
return $this->getField(self::FIELD_USER);
|
||||
}
|
||||
|
||||
public function getUserShared(): int
|
||||
{
|
||||
return $this->getField(self::FIELD_USER_SHARED);
|
||||
}
|
||||
|
||||
public function setUserId(int $userId): void
|
||||
{
|
||||
$this->setField(self::FIELD_USER, $userId);
|
||||
}
|
||||
|
||||
public function setUserShared(int $userShared): void
|
||||
{
|
||||
$this->setField(self::FIELD_USER_SHARED, $userShared);
|
||||
}
|
||||
}
|
133
data/classes/database/User.php
Normal file
133
data/classes/database/User.php
Normal file
@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
final class User extends MySqlTable implements JsonSerializable
|
||||
{
|
||||
public const FIELD_ID = 'UserId';
|
||||
public const FIELD_USERNAME = 'Username';
|
||||
public const FIELD_PASSWORD = 'Password';
|
||||
public const FIELD_EMAIL = 'Email';
|
||||
public const FIELD_JABBER_ADDRESS = 'JabberAddress';
|
||||
|
||||
public function __construct($id = null, DatabaseInterface &$database = null)
|
||||
{
|
||||
parent::__construct(self::class, $id, $database);
|
||||
}
|
||||
|
||||
public function getUserId(): ?int
|
||||
{
|
||||
if ($this->getPrimaryKey() === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int)$this->getPrimaryKey();
|
||||
}
|
||||
|
||||
public function getUsername(): string
|
||||
{
|
||||
return $this->getField(self::FIELD_USERNAME);
|
||||
}
|
||||
|
||||
public function getPassword(): string
|
||||
{
|
||||
return $this->getField(self::FIELD_PASSWORD);
|
||||
}
|
||||
|
||||
public function getEmail(): string
|
||||
{
|
||||
return $this->getField(self::FIELD_EMAIL);
|
||||
}
|
||||
|
||||
public function getJabberAddress(): string
|
||||
{
|
||||
return $this->getField(self::FIELD_JABBER_ADDRESS);
|
||||
}
|
||||
|
||||
public function setUsername(string $username): void
|
||||
{
|
||||
$this->setField(self::FIELD_USERNAME, $username);
|
||||
}
|
||||
|
||||
public function setPassword(string $password): void
|
||||
{
|
||||
$this->setField(self::FIELD_PASSWORD, $password);
|
||||
}
|
||||
|
||||
public function setEmail(string $email): void
|
||||
{
|
||||
$this->setField(self::FIELD_EMAIL, $email);
|
||||
}
|
||||
|
||||
public function setJabberAddress(string $jabberAddress): void
|
||||
{
|
||||
$this->setField(self::FIELD_JABBER_ADDRESS, $jabberAddress);
|
||||
}
|
||||
|
||||
public static function getFromUsername(string $username, DatabaseInterface &$database = null): self
|
||||
{
|
||||
$databaseGiven = true;
|
||||
|
||||
if ($database === null) {
|
||||
$database = new MySqlDatabase();
|
||||
$databaseGiven = false;
|
||||
}
|
||||
|
||||
if ($database->Count(self::class) === 0) {
|
||||
throw new UserException(sprintf('No user with name %s found!', $username));
|
||||
}
|
||||
|
||||
$id = $database->Select(self::class, [self::FIELD_ID], [self::FIELD_USERNAME => $username]);
|
||||
|
||||
$user = $databaseGiven ? new User((int)$id, $database) : new User((int)$id);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public static function getFromEmail(string $email, DatabaseInterface &$database = null): self
|
||||
{
|
||||
$databaseGiven = true;
|
||||
|
||||
if ($database === null) {
|
||||
$database = new MySqlDatabase();
|
||||
$databaseGiven = false;
|
||||
}
|
||||
|
||||
if ($database->Count(self::class) === 0) {
|
||||
throw new UserException(sprintf('No user with email %s found!', $email));
|
||||
}
|
||||
|
||||
$id = $database->Select(self::class, [self::FIELD_ID], [self::FIELD_EMAIL => $email])[0][self::FIELD_ID];
|
||||
|
||||
$user = $databaseGiven ? new User((int)$id, $database) : new User((int)$id);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function getFingerprintIds(): array
|
||||
{
|
||||
$result = $this->database->Select(
|
||||
Fingerprint::class,
|
||||
[Fingerprint::FIELD_ID],
|
||||
[Fingerprint::FIELD_USER => $this->getUserId()]
|
||||
);
|
||||
|
||||
$ids = [];
|
||||
|
||||
foreach ($result as $record) {
|
||||
$ids[] = (int)$record[Fingerprint::FIELD_ID];
|
||||
}
|
||||
|
||||
return $ids;
|
||||
}
|
||||
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return [
|
||||
'userId' => $this->getUserId(),
|
||||
'username' => $this->getUsername(),
|
||||
'jabberAddress' => $this->getJabberAddress(),
|
||||
'fingerprintIds' => $this->getFingerprintIds()
|
||||
];
|
||||
}
|
||||
}
|
7
data/classes/exception/DatabaseException.php
Normal file
7
data/classes/exception/DatabaseException.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class DatabaseException extends Exception
|
||||
{
|
||||
}
|
7
data/classes/exception/QrCodeException.php
Normal file
7
data/classes/exception/QrCodeException.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class QrCodeException extends Exception
|
||||
{
|
||||
}
|
7
data/classes/exception/UserException.php
Normal file
7
data/classes/exception/UserException.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
final class UserException extends Exception
|
||||
{
|
||||
}
|
5
data/scripts/generate.php
Normal file
5
data/scripts/generate.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
require 'data/classes/core/Autoloader.php';
|
||||
|
||||
Autoloader::BuildCache();
|
0
docs/api/v1.yaml
Normal file
0
docs/api/v1.yaml
Normal file
Loading…
Reference in New Issue
Block a user