This commit is contained in:
Mal 2020-08-17 23:46:58 +02:00
commit adc1c38d54
36 changed files with 1902 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
data/classes/Setting.php
data/cache
data/tmp
data/qr
.idea

6
Makefile Normal file
View 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
View 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();

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

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

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
class ApiUnauthorizedResponse extends ApiResponse
{
public function __construct()
{
parent::__construct();
$this->setStatus(self::STATUS_UNAUTHORIZED);
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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);
}
}

View 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);
}
}

View 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();
}
}

View 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);
}
}

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

View 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);
}
}

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

View 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';
}

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

View 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();
}
}
}

View 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);
}
}

View 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);
}
}

View 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) . '/';
}
}

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

View 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
View 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));
}
}
}

View 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()
];
}
}

View 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);
}
}

View 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()
];
}
}

View File

@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
class DatabaseException extends Exception
{
}

View File

@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
class QrCodeException extends Exception
{
}

View File

@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
final class UserException extends Exception
{
}

View File

@ -0,0 +1,5 @@
<?php
require 'data/classes/core/Autoloader.php';
Autoloader::BuildCache();

0
docs/api/v1.yaml Normal file
View File

5
index.php Normal file
View File

@ -0,0 +1,5 @@
<?php
require 'data/classes/core/Autoloader.php';
$autoloader = new Autoloader('data/cache');