Compare commits

...

24 Commits

Author SHA1 Message Date
Mal 46fc2c978f Readme updated 2020-08-24 22:46:24 +02:00
Mal ef7f7c4b4a Endpoints added and refactored - permissions added 2020-08-24 22:19:46 +02:00
Mal 6b2abc1f89 Unification of indents 2020-08-23 12:37:39 +02:00
Mal 37f220b561 Permissions for fingerprint endpoints created 2020-08-23 11:44:12 +02:00
Mal 39d14451b2 Permission architecture implemented 2020-08-22 23:08:05 +02:00
Mal e1a62442d9 API docs ready for swagger-ui 2020-08-22 21:10:49 +02:00
Mal 44aaff929c OpenAPI documentation added 2020-08-21 23:47:13 +02:00
Mal a42f728ccf Fixes and improvements for session handling 2020-08-21 23:16:24 +02:00
Mal f4fdf50288 Fixes for controllers 2020-08-21 22:47:49 +02:00
Mal 3a99e99130 Error handling simplified 2020-08-21 22:46:19 +02:00
Mal 3cb02a12f1 Fix for FingerprintPostController 2020-08-20 23:08:20 +02:00
Mal b0f2991346 Endpoint for editing fingerprints added 2020-08-20 23:07:55 +02:00
Mal f6d1376dbb On delete cascades added 2020-08-20 22:28:22 +02:00
Mal 86c12890c2 Endpoint for editing user data implemented 2020-08-20 22:27:31 +02:00
Mal ec485fb03d Refactoring of FingerprintPostController 2020-08-20 18:36:55 +02:00
Mal 459be4a999 Endpoints for sharing implemented 2020-08-20 18:36:30 +02:00
Mal 0334c7022d Debugging of fingerprint deletion and QR code repair script added 2020-08-20 16:53:54 +02:00
Mal f2957ab4ad Fixing error handling in QrCodeGetController 2020-08-20 16:23:49 +02:00
Mal 515dadb319 Endpoint for fingerprint deletion added 2020-08-20 15:58:00 +02:00
Mal d5aa47ff54 Endpoint for user deletion added 2020-08-20 15:45:59 +02:00
Mal 0a8c71b735 Endpoint for user creation added 2020-08-19 23:57:00 +02:00
Mal 3a1a026e2f Setup script created 2020-08-19 23:45:39 +02:00
Mal 5df0a1f8c2 Moved to folder backend 2020-08-19 21:07:03 +02:00
Mal a7d9c0bf51 Moved to folder backend 2020-08-19 21:06:45 +02:00
85 changed files with 3568 additions and 1938 deletions

9
.gitignore vendored
View File

@ -1,5 +1,6 @@
data/classes/Setting.php
data/cache
data/tmp
data/qr
backend/classes/Setting.php
backend/cache
backend/tmp
backend/qr
api/docs
.idea

View File

@ -1,6 +1,11 @@
build:
php data/scripts/generate.php
repair:
php backend/scripts/repair.php
setup:
php backend/scripts/setup.php
cache:
php backend/scripts/generate.php
clean:
rm -rf data/cache/* data/tmp/*
rm -rf backend/cache/* backend/tmp/*

View File

@ -1,19 +1,27 @@
# Ringfinger
A restfull keyring API with web UI to share your jabber fingerprints with the people you want.
Create an account, store your jabber fingerprints and share it with your friends. There is no need to type in your 64 chars long fingerprint for each of your friends. Just enter it once and decide which people you want to share your keyring with.
# Ringfinger
A restfull keyring API with web UI to share your OMEMO fingerprints for jabber with the people you like.
Create an account, store your OMEMO fingerprints and share it with your friends. There is no need to type in your 64 chars long fingerprint for each of your friends. Just enter it once and decide which people you want to share your keyring with.
# Requirements
* A webserver (Nginx, Apache, etc.)
* PHP 7.4 (with php-fpm configured)
* MySQL/MariaDB
* qrencode
# Setup
Clone or copy the ringfinger folder to the location folder of your webserver. Move into the ringfinger folder an generate the cache:
> make build
## Webserver configuration
Despite your basic setup with PHP and MySQL/MariaDB your webserver has to to rewrite all requests that access the path `/ringfinger/api/v1/...` to `/ringfinger/api/v1/index.php` to make the API working.
### NGINX
Add the following line to your nginx.conf or to a separate file that will be included by the nginx.conf:
> rewrite /ringfinger/api/v1/.* /ringfinger/api/v1/index.php;
**Also make sure you deny the access to /ringfinger/data for all!**
Before you start setting up make sure you have an empty MySQL database created. You also need a database user that has full access to the newly created database!
Then clone or copy the ringfinger folder to the location folder of your webserver. Move into the ringfinger folder and start the setup process
> make setup
## Webserver configuration
Despite your basic setup with PHP and MySQL/MariaDB your webserver has to to rewrite all requests that access the path `/ringfinger/api/v1/...` to `/ringfinger/api/v1/index.php` to make the API working.
### NGINX
Add the following line to your nginx.conf or to a separate file that will be included by the nginx.conf:
> rewrite /ringfinger/api/v1/.* /ringfinger/api/v1/index.php;
**Also make sure you deny the access to /ringfinger/backend for all!**

610
api/openapi.yaml Normal file
View File

@ -0,0 +1,610 @@
openapi: 3.0.0
info:
title: Ringfinger API
version: 1.0.0
description: The restfull API of the jabber fingerprint keyring.
tags:
- name: User
description: A user that can login, store fingerprints and share them with other users.
- name: Fingerprint
description: The jabber fingerprint owned by a user.
- name: Sharing
description: The sharing of all fingerprints of a user with another user.
paths:
'/api/v1/user/{userId}':
get:
tags:
- User
summary: Returns the user attributes
parameters:
- $ref: '#/components/parameters/UserId'
responses:
200:
description: Returns the user data for the given id.
content:
application/json:
schema:
type: object
properties:
success:
$ref: '#/components/schemas/Success'
result:
type: object
description: Contains the user data.
properties:
userId:
$ref: '#/components/schemas/UserId'
usernamme:
$ref: '#/components/schemas/Username'
jabberAddress:
$ref: '#/components/schemas/JabberAddress'
isAdmin:
type: boolean
description: Returns if the user has admin permissions.
example: true
fingerprintIds:
type: array
items:
type: integer
description: A list of all fingerprint ids of the user.
example: [25, 42, 88]
default: []
default:
$ref: '#/components/responses/Error'
put:
tags:
- User
summary: Changes user attributes
parameters:
- $ref: '#/components/parameters/UserId'
requestBody:
required: true
description: Contains the json with the user attributes that have to be changed.
content:
application/json:
schema:
properties:
username:
$ref: '#/components/schemas/Username'
password:
$ref: '#/components/schemas/Password'
email:
$ref: '#/components/schemas/EmailAddress'
jabberAddress:
$ref: '#/components/schemas/JabberAddress'
responses:
200:
$ref: '#/components/responses/Success'
default:
$ref: '#/components/responses/Error'
delete:
tags:
- User
summary: Deletes a user
parameters:
- $ref: '#/components/parameters/UserId'
responses:
200:
$ref: '#/components/responses/Success'
default:
$ref: '#/components/responses/Error'
'/api/v1/user/{userId}/fingerprints':
get:
tags:
- User
summary: A list containing all fingerprints of the user
responses:
200:
description: Returns the success state and a list with all fingerprints of the user.
content:
application/json:
schema:
properties:
success:
$ref: '#/components/schemas/Success'
fingerprints:
type: array
items:
type: object
example:
- fingerprintId: 8
fingerprint: '5BDF1668E59F2184582591699F55D9158DEF400A48772887A8F61531ED36B2A'
userId: 25
- fingerprintId: 42
fingerprint: '6FF8842B6D17F5C2098A3DD8AB55D9158DEF400A48772887A8F61531ED36B2A'
userId: 25
'/api/v1/user/{userId}/email':
get:
tags:
- User
parameters:
- $ref: '#/components/parameters/UserId'
summary: Returns the user's email address
responses:
200:
description: Contains a JSON object with the success state and the email address.
content:
application/json:
schema:
type: object
properties:
success:
$ref: '#/components/schemas/Success'
email:
$ref: '#/components/schemas/EmailAddress'
default:
$ref: '#/components/responses/Error'
put:
tags:
- User
summary: Changes the user's email address
parameters:
- $ref: '#/components/parameters/UserId'
requestBody:
content:
application/json:
schema:
type: object
properties:
email:
$ref: '#/components/schemas/EmailAddress'
allOf:
- required:
- email
responses:
200:
description: Returns the success state for the change request.
content:
application/json:
schema:
type: object
properties:
success:
$ref: '#/components/schemas/Success'
default:
$ref: '#/components/responses/Error'
'/api/v1/user/{userId}/password':
put:
tags:
- User
summary: Changes the password of the user
parameters:
- $ref: '#/components/parameters/UserId'
requestBody:
description: The password inside a json object.
content:
application/json:
schema:
type: object
properties:
password:
type: string
description: The new password.
example: '12345'
allOf:
- required:
- password
responses:
200:
$ref: '#/components/responses/Success'
default:
$ref: '#/components/responses/Error'
'/api/v1/user/{userId}/admin':
post:
tags:
- User
summary: Gives the user admin permissions
parameters:
- $ref: '#/components/parameters/UserId'
responses:
200:
$ref: '#/components/responses/Success'
default:
$ref: '#/components/responses/Error'
delete:
tags:
- User
summary: Removes the user's admin permissions
parameters:
- $ref: '#/components/parameters/UserId'
responses:
200:
$ref: '#/components/responses/Success'
default:
$ref: '#/components/responses/Error'
'/api/v1/user':
post:
tags:
- User
summary: Creates a new user
requestBody:
required: true
description: 'The attributes for the user to be created.'
content:
application/json:
schema:
properties:
username:
$ref: '#/components/schemas/Username'
password:
$ref: '#/components/schemas/Password'
email:
$ref: '#/components/schemas/EmailAddress'
jabberAddress:
$ref: '#/components/schemas/JabberAddress'
allOf:
- required:
- username
- password
- email
- jabberAddress
responses:
200:
$ref: '#/components/responses/Success'
default:
$ref: '#/components/responses/Error'
'/api/v1/user/session':
post:
tags:
- User
summary: Creates a user session (login)
requestBody:
required: true
content:
application/json:
schema:
properties:
username:
$ref: '#/components/schemas/Username'
password:
$ref: '#/components/schemas/Password'
allOf:
- required:
- username
- password
responses:
200:
description: Returns the success state and the user id for the created session.
content:
application/json:
schema:
properties:
success:
$ref: '#/components/schemas/Success'
userId:
$ref: '#/components/schemas/UserId'
default:
$ref: '#/components/responses/Error'
delete:
tags:
- User
summary: Deletes a user session (logout)
responses:
200:
$ref: '#/components/responses/Success'
default:
$ref: '#/components/responses/Error'
'/api/v1/fingerprint/{fingerprintId}/qr':
get:
tags:
- Fingerprint
summary: Returns the fingerprint's QR code
parameters:
- $ref: '#/components/parameters/FingerprintId'
responses:
200:
description: The QR code as SVG image
content:
application/svg+xml:
schema:
type: string
description: The xml of the SVG image.
default:
$ref: '#/components/responses/Error'
'/api/v1/fingerprint/{fingerprintId}':
get:
tags:
- Fingerprint
summary: Returns the attributes of the fingerprint
parameters:
- $ref: '#/components/parameters/FingerprintId'
responses:
200:
description: Returns the fingerprint for the given fingerprint id.
content:
application/json:
schema:
$ref: '#/components/schemas/FingerprintData'
default:
$ref: '#/components/responses/Error'
put:
tags:
- Fingerprint
summary: Changes the fingerprint
parameters:
- $ref: '#/components/parameters/FingerprintId'
requestBody:
content:
application/json:
schema:
properties:
fingerprint:
$ref: '#/components/schemas/Fingerprint'
required:
- fingerprint
responses:
200:
description: Returns the success state.
content:
application/json:
schema:
properties:
success:
type: boolean
description: 'Returns the success state of the fingerprint change.'
example: true
message:
type: string
description: 'Returns if no changes have been found inside the request body.'
example: 'Fingerprint did not differ from the stored. Nothing changed.'
required:
- success
default:
$ref: '#/components/responses/Error'
delete:
tags:
- Fingerprint
summary: Removes the fingerprint
parameters:
- $ref: '#/components/parameters/FingerprintId'
responses:
200:
$ref: '#/components/responses/Success'
default:
$ref: '#/components/responses/Error'
'/api/v1/fingerprint':
post:
tags:
- Fingerprint
summary: Creates a new fingerprint
requestBody:
content:
application/json:
schema:
description: The parameters for the new fingerprint to be created.
properties:
fingerprint:
$ref: '#/components/schemas/Fingerprint'
userId:
$ref: '#/components/schemas/UserId'
required:
- fingerprint
- userId
responses:
200:
description: Returns the confirmation and the id for the newly created fingerprint.
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
description: Returns the success status.
example: true
fingerprintId:
type: integer
description: The id for the newly created fingerprint.
example: 42
default:
$ref: '#/components/responses/Error'
'/api/v1/sharing/{sharingId}':
get:
tags:
- Sharing
summary: Returns the sharing attributes
parameters:
- $ref: '#/components/parameters/SharingId'
responses:
200:
description: The JSON result for the sharing.
content:
application/json:
schema:
properties:
success:
$ref: '#/components/schemas/Success'
result:
type: object
properties:
sharingId:
$ref: '#/components/schemas/SharingId'
userId:
type: integer
description: The id of the user that shares his fingerprints.
example: 42
userSharedId:
$ref: '#/components/schemas/SharingUserId'
default:
$ref: '#/components/responses/Error'
delete:
tags:
- Sharing
summary: Removes the specified sharing
parameters:
- $ref: '#/components/parameters/SharingId'
responses:
200:
$ref: '#/components/responses/Success'
default:
$ref: '#/components/responses/Error'
'/api/v1/sharing':
post:
tags:
- Sharing
summary: Creates a new sharing
requestBody:
description: Contains the attributes for the sharing to be created.
content:
application/json:
schema:
type: object
properties:
userId:
type: integer
description: The id of the user that shares his fingerprints.
example: 42
userSharedId:
$ref: '#/components/schemas/SharingUserId'
allOf:
- required:
- userId
- userSharedId
responses:
200:
description: Returns the success flag and the id for the newly created sharing.
content:
application/json:
schema:
type: object
properties:
success:
$ref: '#/components/schemas/Success'
sharingId:
$ref: '#/components/schemas/SharingId'
default:
$ref: '#/components/responses/Error'
components:
parameters:
FingerprintId:
name: fingerprintId
in: path
required: true
schema:
$ref: '#/components/schemas/FingerprintId'
SharingId:
name: sharingId
in: path
required: true
schema:
$ref: '#/components/schemas/SharingId'
UserId:
name: userId
in: path
required: true
schema:
$ref: '#/components/schemas/UserId'
responses:
Success:
description: Returns that the request could be handled successfully.
content:
application/json:
schema:
properties:
success:
$ref: '#/components/schemas/Success'
Error:
description: Returns a JSON object that describes the error.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
schemas:
Error:
type: object
properties:
success:
type: boolean
description: Returns that the request couldn't be handled.
example: false
message:
type: string
description: Description of the error that occured.
example: Something extremely terrible happened!
Success:
type: boolean
description: Returns if the request could be handled.
example: true
UserId:
type: integer
description: The id of the user in question.
example: 42
Username:
type: string
description: The login name for the given user.
example: Kevin42
Password:
type: string
description: The password for the user login.
example: '12345'
JabberAddress:
type: string
description: The address of the jabber account.
example: donalt@jabber.de
EmailAddress:
type: string
description: The registered email address for the user.
example: kevin42@gmail.com
FingerprintId:
type: integer
description: The id that identifies the fingerprint.
example: 42
readOnly: true
Fingerprint:
type: string
description: The jabber fingerprint
example: '5BDF1668E59F2184582591699F55D9158DEF400A48772887A8F61531ED36B2A'
FingerprintData:
type: object
properties:
fingerprintId:
$ref: '#/components/schemas/FingerprintId'
fingerprint:
$ref: '#/components/schemas/Fingerprint'
userId:
type: integer
description: The id of the user that owns this fingerprint.
example: 25
SharingId:
type: integer
description: The id for the sharing.
example: 42
readOnly: true
SharingUserId:
type: integer
description: The id of the user who is allowed to display the fingerprints.
example: 25

View File

@ -1,15 +1,15 @@
<?php
require '../../data/classes/core/Autoloader.php';
require '../../backend/classes/core/Autoloader.php';
$autoloader = new Autoloader('../../data/cache/');
$autoloader = new Autoloader('../../backend/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->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,32 @@
<?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 setSuccess(bool $success): void
{
$this->setParameter('success', $success);
}
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,57 @@
<?php
declare(strict_types=1);
final class FingerprintDeleteController 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
{
if (!$this->isUserLoggedIn()) {
return;
}
parent::handle();
if ($this->response->getStatus() !== ServerStatus::OK) {
return;
}
try {
$db = new MySqlDatabase();
$this->response = new ApiJsonResponse();
$fingerprint = new Fingerprint($this->fingerprintId, $db);
if (!$this->hasUserPermission($fingerprint->getUserId())) {
return;
}
$db->startTransaction();
if (!$this->hasUserPermission($fingerprint->getUserId())) {
return;
}
$qrCode = new QrCode($fingerprint->getFingerprintId(), $fingerprint->getFingerprint());
$fingerprint->Delete();
$qrCode->delete();
$db->commit();
} catch (Throwable $e) {
$this->response = new ApiJsonResponse(ServerStatus::BAD_REQUEST);
$this->response->setParameter('success', false);
$this->response->setMessage($e->getMessage());
}
}
}

View File

@ -0,0 +1,42 @@
<?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 {
if (!$this->isUserLoggedIn()) {
return;
}
$fingerprint = new Fingerprint($this->fingerprintId);
if (!$this->hasUserPermission($fingerprint->getUserId())) {
if (!$fingerprint->isSharedWith($this->session->getUserId())) {
return;
}
}
$this->response = new ApiJsonResponse();
$this->response->setResult($fingerprint);
} 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,71 @@
<?php
declare(strict_types=1);
final class FingerprintPostController extends AbstractController
{
protected string $route = '/api/v1/fingerprint';
protected array $mandatoryAttributes = [
'fingerprint',
'userId',
];
public function handle(): void
{
parent::handle();
if (!$this->isUserLoggedIn() || $this->response->getStatus() !== ServerStatus::OK) {
return;
}
$db = new MySqlDatabase();
$fingerprint = new Fingerprint(null, $db);
$this->response = new ApiJsonResponse();
try {
if (!$this->hasUserPermission((int)$this->jsonBody->userId)) {
return;
}
$fingerprint->setFingerprint((string)$this->jsonBody->fingerprint);
$fingerprint->setUserId((int)$this->jsonBody->userId);
if (!$db->hasTransaction()) {
$db->startTransaction();
}
$fingerprint->Save();
$qrCode = new QrCode($fingerprint->getFingerprintId(), $fingerprint->getFingerprint());
$qrCode->generate();
$qrCode->save();
$db->commit();
$this->response->setParameter('fingerprintId', $fingerprint->getFingerprintId());
} catch (QrCodeException $e) {
$this->response->setStatus(ServerStatus::INTERNAL_ERROR);
$this->response->setParameter('success', false);
$this->response->setMessage('An error occured during QR code creation!');
} catch (Throwable $e) {
$this->catchDatabaseException($e->getMessage(), $this->jsonBody);
}
}
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,57 @@
<?php
declare(strict_types=1);
final class FingerprintPutController extends AbstractController
{
protected string $route = '/api/v1/fingerprint/{fingerprintId}';
protected array $mandatoryAttributes = [
'fingerprint',
];
private int $fingerprintId;
public function __construct(string $url)
{
parent::__construct($url);
$this->fingerprintId = (int)$this->getUrlParamInt('fingerprintId');
}
public function handle(): void
{
if (!$this->isUserLoggedIn()) {
return;
}
parent::handle();
$this->response = new ApiJsonResponse();
try {
$fingerprint = new Fingerprint($this->fingerprintId);
if ($this->hasUserPermission($fingerprint->getUserId()) || $this->handleFingerprint($fingerprint)) {
return;
}
$this->response->setMessage('Fingerprint did not differ from the stored. Nothing changed.');
} catch (Throwable $e) {
$this->response->setStatus(ServerStatus::BAD_REQUEST);
$this->response->setParameter('success', false);
$this->response->setMessage($e->getMessage());
}
}
public function handleFingerprint(Fingerprint $fingerprint): bool
{
if ($fingerprint->getFingerprint() !== $this->jsonBody->fingerprint) {
$fingerprint->setFingerprint($this->jsonBody->fingerprint);
$fingerprint->Save();
return true;
}
return false;
}
}

View File

@ -0,0 +1,48 @@
<?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
{
try {
$fingerprint = new Fingerprint($this->fingerprintId);
if (!$this->isUserLoggedIn() || !$this->hasUserPermission($fingerprint->getUserId())) {
return;
}
$filename = Setting::PATH_QR_CODES . (string)$this->fingerprintId . '.svg';
if (!is_file($filename)) {
$this->response = new ApiJsonResponse(ServerStatus::BAD_REQUEST);
$this->response->setParameter('success', false);
$this->response->setMessage(sprintf('No QR code for fingerprint id %d found!', $this->fingerprintId));
return;
}
$this->response = new ApiSvgResponse();
$file = @fopen($filename, 'r');
$this->response->setContent(@fread($file, @filesize($filename)));
@fclose($file);
} catch (Throwable $e) {
$this->response = new ApiJsonResponse(ServerStatus::INTERNAL_ERROR);
$this->response->setParameter('success', false);
$this->response->setMessage($e->getMessage());
}
}
}

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
final class SharingDeleteController extends AbstractController
{
protected string $route = '/api/v1/sharing/{sharingId}';
private int $sharingId;
public function __construct(string $url)
{
parent::__construct($url);
$this->sharingId = (int)$this->getUrlParamInt('sharingId');
}
public function handle(): void
{
parent::handle();
if ($this->response->getStatus() !== ServerStatus::OK) {
return;
}
try {
$sharing = new Sharing($this->sharingId);
if (!$this->isUserLoggedIn() || !$this->hasUserPermission($sharing->getUserId())) {
return;
}
$sharing->Delete();
$this->response = new ApiJsonResponse();
$this->response->setParameter('success', true);
} catch (Throwable $e) {
$this->response = new ApiJsonResponse(ServerStatus::BAD_REQUEST);
$this->response->setParameter('success', false);
$this->response->setMessage($e->getMessage());
}
}
}

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
final class SharingGetController extends AbstractController
{
protected string $route = '/api/v1/sharing/{sharingId}';
private int $sharingId;
public function __construct(string $url)
{
parent::__construct($url);
$this->sharingId = (int)$this->getUrlParamInt('sharingId');
}
public function handle(): void
{
parent::handle();
if ($this->response->getStatus() !== ServerStatus::OK) {
return;
}
try {
$sharing = new Sharing($this->sharingId);
if (!$this->isUserLoggedIn() || !$this->hasUserPermission($sharing->getUserId())) {
return;
}
$this->response = new ApiJsonResponse();
$this->response->setParameter('success', true);
$this->response->setResult($sharing);
} catch (Throwable $e) {
$this->response = new ApiJsonResponse(ServerStatus::BAD_REQUEST);
$this->response->setParameter('success', false);
$this->response->setMessage($e->getMessage());
}
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
final class SharingPostController extends AbstractController
{
protected string $route = '/api/v1/sharing';
/** @var string[] */
protected array $mandatoryAttributes = [
'userId',
'userSharedId',
];
public function handle(): void
{
parent::handle();
if ($this->response->getStatus() !== ServerStatus::OK) {
return;
}
if (!$this->isUserLoggedIn() || !$this->hasUserPermission($this->jsonBody->userId)) {
return;
}
try {
$sharing = new Sharing();
$sharing->setUserId($this->jsonBody->userId);
$sharing->setUserShared($this->jsonBody->userSharedId);
$sharing->Save();
$this->response = new ApiJsonResponse();
$this->response->setParameter('sharingId', $sharing->getSharingId());
} catch (Throwable $e) {
$this->response = new ApiJsonResponse(ServerStatus::BAD_REQUEST);
$this->response->setMessage($e->getMessage());
}
}
}

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
final class UserAdminDeleteController extends AbstractController
{
protected string $route = '/api/v1/user/{userId}/admin';
private int $userId;
public function __construct(string $url)
{
parent::__construct($url);
$this->userId = (int)$this->getUrlParamInt('userId');
}
public function handle(): void
{
$this->response = new ApiJsonResponse();
if (!$this->isUserLoggedIn()) {
return;
}
if (!$this->session->isAdmin()) {
$this->response->setStatus(ServerStatus::UNAUTHORIZED);
$this->response->setSuccess(false);
$this->response->setMessage('You have no permission!');
return;
}
try {
$user = new User($this->userId);
$user->setAdmin(false);
$user->Save();
} catch (Throwable $e) {
$this->response->setSuccess(false);
$this->response->setStatus($e->getCode() !== 0 ? $e->getCode() : ServerStatus::BAD_REQUEST);
$this->response->setMessage($e->getMessage());
}
}
}

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
final class UserAdminPostController extends AbstractController
{
protected string $route = '/api/v1/user/{userId}/admin';
private int $userId;
public function __construct(string $url)
{
parent::__construct($url);
$this->userId = (int)$this->getUrlParamInt('userId');
}
public function handle(): void
{
$this->response = new ApiJsonResponse();
if (!$this->isUserLoggedIn()) {
return;
}
if (!$this->session->isAdmin()) {
$this->response->setStatus(ServerStatus::UNAUTHORIZED);
$this->response->setSuccess(false);
$this->response->setMessage('You have no permission!');
return;
}
try {
$user = new User($this->userId);
$user->setAdmin(true);
$user->Save();
} catch (Throwable $e) {
$this->response->setSuccess(false);
$this->response->setStatus($e->getCode() !== 0 ? $e->getCode() : ServerStatus::BAD_REQUEST);
$this->response->setMessage($e->getMessage());
}
}
}

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
final class UserDeleteController 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
{
if (!$this->isUserLoggedIn()) {
return;
}
parent::handle();
if ($this->response->getStatus() !== ServerStatus::OK || !$this->hasUserPermission($this->userId)) {
return;
}
try {
$user = new User($this->userId);
$user->Delete();
$this->response = new ApiJsonResponse();
$this->response->setParameter('success', true);
} catch (Throwable $e) {
$this->response = new ApiJsonResponse(ServerStatus::BAD_REQUEST);
$this->response->setParameter('success', false);
$this->response->setMessage($e->getMessage());
}
}
}

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
final class UserEmailGetController extends AbstractController
{
protected string $route = '/api/v1/user/{userId}/email';
private int $userId;
public function __construct(string $url)
{
parent::__construct($url);
$this->userId = (int)$this->getUrlParamInt('userId');
}
public function handle(): void
{
if (!$this->isUserLoggedIn()) {
return;
}
parent::handle();
if (!$this->hasUserPermission($this->userId)) {
return;
}
try {
$user = new User($this->userId);
$this->response->setParameter('email', $user->getEmail());
} catch (Throwable $e) {
$this->response->setParameter('success', false);
$this->response->setMessage($e->getMessage());
$this->response->setStatus($e->getCode() !== 0 ? $e->getCode() : ServerStatus::BAD_REQUEST);
}
}
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
final class UserEmailPutController extends AbstractController
{
protected string $route = '/api/v1/user/{userId}/email';
protected array $mandatoryAttributes = [
'email',
];
private int $userId;
public function __construct(string $url)
{
parent::__construct($url);
$this->userId = (int)$this->getUrlParamInt('userId');
}
public function handle(): void
{
if (!$this->isUserLoggedIn() || !$this->hasUserPermission($this->userId)) {
return;
}
parent::handle();
$this->response = new ApiJsonResponse();
try {
$json = json_decode($this->requestBody);
$user = new User($this->userId);
if ($user->getEmail() !== $json->email) {
$user->setEmail($json->email);
$user->Save();
}
} catch (Throwable $e) {
$this->response->setStatus($e->getCode() !== 0 ? $e->getCode() : ServerStatus::BAD_REQUEST);
$this->response->setParameter('success', false);
$this->response->setMessage($e->getMessage());
}
}
}

View File

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
final class UserFingerprintsGetController extends AbstractController
{
protected string $route = '/api/v1/user/{userId}/fingerprints';
private int $userId;
public function __construct(string $url)
{
parent::__construct($url);
$this->userId = (int)$this->getUrlParamInt('userId');
}
public function handle(): void
{
if (!$this->isUserLoggedIn() ) {
return;
}
try {
$this->response = new ApiJsonResponse();
$user = new User($this->userId);
if (!$this->hasUserPermission($this->userId)) {
if (!$user->isSharingWith($this->session->getUserId())) {
return;
}
}
$fingerprints = new FingerprintCollection();
foreach ($user->getFingerprintIds() as $fingerprintId) {
$fingerprints->add(new Fingerprint($fingerprintId));
}
$this->response->setParameter('fingerprints', $fingerprints->jsonSerialize());
} catch (Throwable $e) {
$this->response->setParameter('success', false);
$this->response->setMessage($e->getMessage());
$this->response->setStatus($e->getCode() !== 0 ? $e->getCode() : ServerStatus::BAD_REQUEST);
}
}
}

View File

@ -0,0 +1,35 @@
<?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
{
if (!$this->isUserLoggedIn()) {
return;
}
$this->response = new ApiJsonResponse();
try {
$user = new User($this->userId);
$this->response->setResult($user);
} catch (Throwable $e) {
$this->response->setParameter('success', false);
$this->response->setStatus($e->getCode() !== 0 ? $e->getCode() : ServerStatus::INTERNAL_ERROR);
$this->response->setMessage($e->getMessage());
}
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
final class UserPasswordPutController extends AbstractController
{
protected string $route = '/api/v1/user/{userId}/password';
/** @var string[] */
protected array $mandatoryAttributes= [
'password',
];
private int $userId;
public function __construct(string $url)
{
parent::__construct($url);
$this->userId = (int)$this->getUrlParamInt('userId');
}
public function handle(): void
{
if (!$this->isUserLoggedIn() || !$this->hasUserPermission($this->userId)) {
return;
}
parent::handle();
if ($this->response->getStatus() !== ServerStatus::OK) {
return;
}
$this->response = new ApiJsonResponse();
try {
$user = new User($this->userId);
$user->setPassword(Password::GetHash($this->jsonBody->password));
$user->Save();
} catch (Throwable $e) {
$this->response->setSuccess(false);
$this->response->setStatus($e->getCode() !== 0 ? $e->getCode() : ServerStatus::BAD_REQUEST);
$this->response->setMessage($e->getMessage());
}
}
}

View File

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
final class UserPostController extends AbstractController
{
protected string $route = '/api/v1/user';
protected array $mandatoryAttributes = [
'username',
'password',
'email',
'jabberAddress',
];
public function handle(): void
{
if (!$this->isUserLoggedIn()) {
return;
}
if (!$this->session->isAdmin()) {
$this->response = new ApiJsonResponse(ServerStatus::UNAUTHORIZED);
$this->response->setSuccess(false);
$this->response->setMessage('You have no permission!');
}
parent::handle();
if ($this->response->getStatus() !== ServerStatus::OK) {
return;
}
try {
$user = new User();
$user->setUsername($this->jsonBody->username);
$user->setPassword(Password::GetHash($this->jsonBody->password));
$user->setEmail($this->jsonBody->email);
$user->setJabberAddress($this->jsonBody->jabberAddress);
$user->Save();
$this->response = new ApiJsonResponse();
$this->response->setParameter('userId', $user->getUserId());
} catch (DatabaseException $e) {
$this->response = new ApiJsonResponse(ServerStatus::INTERNAL_ERROR);
$this->response->setParameter('success', false);
$this->response->setMessage($e->getMessage());
} catch (Throwable $e) {
$this->response = new ApiJsonResponse(ServerStatus::BAD_REQUEST);
$this->response->setParameter('success', false);
$this->response->setMessage($e->getMessage());
}
}
}

View File

@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
final class UserPutController 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
{
if (!$this->isUserLoggedIn()) {
return;
}
parent::handle();
if ($this->response->getStatus() !== ServerStatus::OK || !$this->hasUserPermission($this->userId)) {
return;
}
try {
$user = new User($this->userId);
$hasChanged = $this->handleUserData($user);
$this->response = new ApiJsonResponse();
if ($hasChanged) {
$user->Save();
return;
}
$this->response->setMessage('No differing attributes found. Nothing changed.');
} catch (Throwable $e) {
$this->response = new ApiJsonResponse(ServerStatus::BAD_REQUEST);
$this->response->setParameter('success', false);
$this->response->setMessage($e->getMessage());
}
}
private function setUsername(User $user): bool
{
$hasChanged = $user->getUsername() !== $this->jsonBody->username;
$user->setUsername($this->jsonBody->username);
return $hasChanged;
}
private function setPassword(User $user): bool
{
$hasChanged = !Password::IsValid($this->jsonBody->password, $user->getPassword());
$user->setPassword(Password::GetHash($this->jsonBody->password));
return $hasChanged;
}
private function setEmail(User $user): bool
{
$hasChanged = $user->getEmail() !== $this->jsonBody->email;
$user->setEmail($this->jsonBody->email);
return $hasChanged;
}
private function setJabberAddress(User $user): bool
{
$hasChanged = $user->getJabberAddress() !== $this->jsonBody->jabberAddress;
$user->setJabberAddress($this->jsonBody->jabberAddress);
return $hasChanged;
}
private function handleUserData(User $user): bool
{
$hasChanged = $this->setUsername($user) || false;
$hasChanged = $this->setPassword($user) || $hasChanged;
$hasChanged = $this->setEmail($user) || $hasChanged;
$hasChanged = $this->setJabberAddress($user) || $hasChanged;
return $hasChanged;
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
final class UserSessionDeleteController extends AbstractController
{
protected string $route = '/api/v1/user/session';
public function handle(): void
{
parent::handle();
if (!$this->session->IsLoggedIn()) {
$this->response = new ApiJsonResponse(ServerStatus::BAD_REQUEST);
$this->response->setParameter('success', false);
$this->response->setMessage('No session to delete!');
return;
}
$this->session->Destroy();
$this->response = new ApiJsonResponse();
$this->response->setParameter('success', true);
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
final class UserSessionPostController extends AbstractController
{
protected string $route = '/api/v1/user/session';
protected array $mandatoryAttributes = [
'username',
'password',
];
public function handle(): void
{
parent::handle();
if ($this->response->getStatus() !== ServerStatus::OK) {
return;
}
if ($this->session->IsLoggedIn()) {
$this->response = new ApiJsonResponse(ServerStatus::BAD_REQUEST);
$this->response->setParameter('success', false);
$this->response->setMessage('You are already logged in!');
return;
}
if (!$this->session->Login($this->jsonBody->username, $this->jsonBody->password)) {
$this->response = new ApiJsonResponse(ServerStatus::UNAUTHORIZED);
$this->response->setParameter('success', false);
$this->response->setMessage('Login failed!');
return;
}
$this->response = new ApiJsonResponse();
$this->response->setParameter('userId', $this->session->getUserId());
}
}

View File

@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
abstract class AbstractController
{
protected string $route;
protected ApiResponse $response;
protected Session $session;
protected string $requestUrl;
protected ?string $requestBody = null;
protected ?object $jsonBody = null;
protected ?string $contentType = null;
/** @var string[] */
protected array $mandatoryAttributes = [];
public function __construct(string $url)
{
$this->requestUrl = $url;
$this->response = new ApiResponse();
$this->session = new Session();
}
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->setSuccess(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;
}
public function isUserLoggedIn(): bool
{
if (!$this->session->IsLoggedIn()) {
$this->response = new ApiJsonResponse(ServerStatus::UNAUTHORIZED);
$this->response->setParameter('success', false);
$this->response->setMessage('You are not logged in!');
return false;
}
return true;
}
public function hasUserPermission(int $userId): bool
{
$this->response = new ApiJsonResponse();
$hasPermission = $this->session->isAdmin() || $this->session->getUserId() === $userId;
if (!$hasPermission) {
$this->response->setSuccess(false);
$this->response->setMessage('You don\'t have the permission!');
$this->response->setStatus(ServerStatus::UNAUTHORIZED);
}
return $hasPermission;
}
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;
}
}
$this->jsonBody = $json;
return true;
} catch (Throwable $e) {
return false;
}
}
}

View File

@ -0,0 +1,246 @@
<?php
declare(strict_types=1);
class Autoloader
{
private const PATH_CLASSES = 'backend/classes';
private const PATH_CONTROLLERS = self::PATH_CLASSES . '/controller';
private const PATH_CACHE = 'backend/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' => [],
'DELETE' => [],
];
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,80 @@
<?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 backend 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 backend 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 if there's an open transaction.
*/
public function hasTransaction(): bool;
/*
* Starts a transaction that can later be committed or rolled back.
*/
public function startTransaction(): void;
/*
* Quits a current transaction without saving.
*/
public function rollback(): void;
/*
* Saves and exits a current transaction.
*/
public function commit(): void;
/**
* 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,281 @@
<?php
declare(strict_types=1);
final class MySqlDatabase implements DatabaseInterface
{
private const CHARS_ALLOWED_IN_TABLE_NAMES = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890_-';
private ?PDO $connection;
private ?PDOStatement $cursor;
private bool $isTransactionOpen = false;
public function __construct(
string $hostname = Setting::MYSQL_HOST,
string $user = Setting::MYSQL_USER,
string $password = Setting::MYSQL_PASSWORD,
string $database = Setting::MYSQL_DATABASE
) {
try {
$this->connection = new PDO("mysql:host=$hostname;dbname=$database", $user, $password);
} catch (Throwable $e) {
throw new DatabaseException(
'Couldn\'t connect to the database!',
ServerStatus::INTERNAL_ERROR
);
}
}
public function __destruct()
{
$this->connection = null;
}
public function Query(string $query, array $params = []): void
{
$this->cursor = $this->connection->prepare($query);
if (!$this->cursor) {
throw new DatabaseException(
'Initialization of database cursor failed',
DatabaseException::CONNECTION_FAILED
);
}
foreach ($params as $key => $param) {
if (is_bool($param)) {
$param = (int)$param;
}
$this->cursor->bindValue(':' . $key, $param);
}
if (!$this->cursor->execute()) {
throw new DatabaseException($this->cursor->errorInfo()[2], $this->cursor->errorInfo()[1]);
}
}
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 backend 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) {
throw new DatabaseException(
$e->getMessage(),
ServerStatus::INTERNAL_ERROR
);
}
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);
}
public function Insert(string $table, array $fields): ?int
{
if (count($fields) === 0) {
throw new DatabaseException('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;
}
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));
}
public function Count(string $table, array $conditions = []): int
{
$result = $this->Select($table, ['count(*)'], $conditions);
return (int)$result[0]['count(*)'];
}
public function hasTransaction(): bool
{
return $this->isTransactionOpen;
}
public function startTransaction(): void
{
$this->connection->beginTransaction();
$this->isTransactionOpen = true;
}
public function rollback(): void
{
$this->connection->rollBack();
$this->isTransactionOpen = false;
}
public function commit(): void
{
$this->connection->commit();
$this->isTransactionOpen = false;
}
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,90 @@
<?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;
$filename = $this->getFilePath();
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 delete(): void
{
$filepath = $this->getFilePath();
if (!is_file($filepath)) {
throw new QrCodeException(sprintf('Qr code file %s not found!', $filepath));
}
if (!unlink($filepath)) {
throw new QrCodeException('Couldn\'t delete %s!', $filepath);
}
}
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;
}
public function hasFile(): bool
{
return is_file($this->getFilePath());
}
private function generateTemporaryFilename(): string
{
$hash = hash('md5', (new DateTime())->format('U') . $this->fingerprint);
return sprintf('%s.svg', $hash);
}
private function getFilePath(): string
{
$path = substr(Setting::PATH_QR_CODES, -1) === '/'
? Setting::PATH_QR_CODES
: Setting::PATH_QR_CODES . '/';
return $path . $this->fingerprintId . '.svg';
}
}

View File

@ -0,0 +1,58 @@
<?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']);
/** @var AbstractController $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 IS_ADMIN = 'admin';
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(): bool
{
return 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->getUserId());
$this->SetString(self::USERNAME, $user->getUsername());
$this->SetString(self::EMAIL, $user->getEmail());
$this->SetString(self::JABBER_ADDRESS, $user->getJabberAddress());
$this->SetBool(self::IS_ADMIN, $user->isAdmin());
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 getUserId(): ?int
{
return $this->GetInt(self::USER_ID);
}
public function isAdmin(): bool
{
return $this->GetBool(self::IS_ADMIN);
}
public static function HasSession(): bool
{
return isset($_SESSION);
}
}

View File

@ -0,0 +1,292 @@
<?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 DatabaseException(
sprintf('Type %s of field %s couldn\'t be handled', $sqlType, $field['Field']),
ServerStatus::INTERNAL_ERROR
);
}
$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 DatabaseException(
sprintf('Field %s has invalid type of %s!', $name, $type),
ServerStatus::INTERNAL_ERROR
);
}
$this->fields[$name] = [self::VALUE => null, self::TYPE => $type];
}
protected function loadById($id): void
{
$this->database->Query(
sprintf('SELECT * FROM %s WHERE %s = :id', $this->tableName, $this->primaryKey),
['id' => $id]
);
$result = $this->database->getResult();
if (count($result) === 0) {
throw new DatabaseException(
sprintf('No %s with id %d found!', $this->tableName, $id),
ServerStatus::BAD_REQUEST
);
}
foreach ($result[0] as $field => $value) {
$this->setField($field, $value);
}
}
public function Flush(): void
{
$this->database->Delete($this->tableName, []);
}
public function Delete(): void
{
$this->database->Delete($this->tableName, [$this->primaryKey => $this->getPrimaryKey()]);
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 DatabaseException(
sprintf('Field %s doesn\'t exist!', $name),
ServerStatus::INTERNAL_ERROR
);
}
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 (Throwable $e) {
throw new DatabaseException(
$e->getMessage(),
ServerStatus::INTERNAL_ERROR
);
}
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 backend 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 DatabaseException(
'Manual primary key must not be null!',
ServerStatus::INTERNAL_ERROR
);
}
$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,61 @@
<?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 isSharedWith(int $userId): bool
{
return (bool)$this->database->Count(
Sharing::class,
[Sharing::FIELD_USER => $this->getUserId(), Sharing::FIELD_USER_SHARED => $userId]
);
}
public function jsonSerialize(): array
{
return [
'fingerprintId' => $this->getFingerprintId(),
'fingerprint' => $this->getFingerprint(),
'userId' => $this->getUserId()
];
}
}

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
final class FingerprintCollection implements Iterator, JsonSerializable
{
private int $position = 0;
/** @var Fingerprint[] */
private array $fingerprints = [];
public function add(Fingerprint $fingerprint): void
{
$this->fingerprints[] = $fingerprint;
}
public function current(): Fingerprint
{
return $this->fingerprints[$this->position];
}
public function next(): void
{
$this->position++;
}
public function key(): int
{
return $this->position;
}
public function valid(): bool
{
return isset($this->fingerprints[$this->position]);
}
public function rewind(): void
{
$this->position = 0;
}
public function jsonSerialize(): array
{
$fingerprints = [];
foreach ($this->fingerprints as $fingerprint) {
$fingerprints[] = $fingerprint->jsonSerialize();
}
return $fingerprints;
}
}

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
final class Sharing extends MySqlTable implements JsonSerializable
{
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);
}
public function jsonSerialize()
{
return [
'sharingId' => $this->getSharingId(),
'userId' => $this->getUserId(),
'userSharedId' => $this->getUserShared(),
];
}
}

View File

@ -0,0 +1,153 @@
<?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 const FIELD_ADMIN = 'IsAdmin';
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 isAdmin(): bool
{
return $this->getField(self::FIELD_ADMIN);
}
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 function setAdmin(bool $isAdmin): void
{
$this->setField(self::FIELD_ADMIN, $isAdmin);
}
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, [self::FIELD_USERNAME => $username]) === 0) {
throw new UserException(sprintf('No user with name %s found!', $username));
}
$id = $database->Select(self::class, [self::FIELD_ID], [self::FIELD_USERNAME => $username])[0][self::FIELD_ID];
$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 isSharingWith(int $userId): bool
{
return (bool)$this->database->Count(
Sharing::class,
[Sharing::FIELD_USER => $this->getUserId(), Sharing::FIELD_USER_SHARED => $userId]
);
}
public function jsonSerialize()
{
return [
'userId' => $this->getUserId(),
'username' => $this->getUsername(),
'jabberAddress' => $this->getJabberAddress(),
'isAdmin' => $this->isAdmin(),
'fingerprintIds' => $this->getFingerprintIds()
];
}
}

View File

@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
final class DatabaseException extends Exception
{
public const CONNECTION_FAILED = 1;
}

View File

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

View File

@ -0,0 +1,31 @@
<?php
require 'backend/classes/core/Autoloader.php';
$autoloader = new Autoloader();
echo 'Checking for missing qr codes...' . PHP_EOL;
$db = new MySqlDatabase();
$countForMissing = 0;
foreach ($db->Select(Fingerprint::class, [Fingerprint::FIELD_ID]) as $record) {
$fingerprint = new Fingerprint((int)$record[Fingerprint::FIELD_ID], $db);
$qrCode = new QrCode($fingerprint->getFingerprintId(), $fingerprint->getFingerprint());
if (!$qrCode->hasFile()) {
$countForMissing++;
$qrCode->generate();
$qrCode->save();
echo sprintf("\t=> Missing QR code for fingerprint %d generated.\n", $fingerprint->getFingerprintId());
}
}
echo $countForMissing === 0
? 'No missing QR codes found.' . PHP_EOL
: sprintf('%d missing QR code(s) fixed.', $countForMissing) . PHP_EOL;

59
backend/scripts/setup.php Normal file
View File

@ -0,0 +1,59 @@
<?php
function getUserInput(string $label): string
{
echo $label;
return str_replace("\n", '', fgets(STDIN));
}
const TEMPLATE_SETTINGS = '<?php
/*
* This file was auto generated on :DATETIME
*/
declare(strict_types=1);
final class Setting
{
public const MYSQL_HOST = \':HOST\';
public const MYSQL_USER = \':USER\';
public const MYSQL_PASSWORD = \':PASSWORD\';
public const MYSQL_DATABASE = \':DATABASE\';
public const PATH_ROOT = \':ROOT_PATH/\';
public const PATH_QR_CODES = self::PATH_ROOT . \'backend/qr/\';
public const PATH_TMP = self::PATH_ROOT . \'backend/tmp/\';
}
';
$hostname = getUserInput('MySQL host address (default "localhost"): ');
$username = getUserInput('MySQL user: ');
$password = getUserInput('MySQL password (shown!): ');
$database = getUserInput('MySQL database: ');
$settings = str_replace(
[':HOST', ':USER', ':PASSWORD', ':DATABASE', ':ROOT_PATH', ':DATETIME'],
[$hostname, $username, $password, $database, getcwd(), (new DateTime())->format('Y-m-d H:i:s')],
TEMPLATE_SETTINGS
);
$file = fopen(getcwd() . '/backend/classes/Setting.php', 'w');
fwrite($file, $settings);
fclose($file);
require 'backend/classes/core/Autoloader.php';
Autoloader::BuildCache();
$autoloader = new Autoloader();
$file = fopen('backend/scripts/setup.sql', 'r');
$setupSql = fread($file, filesize('backend/scripts/setup.sql'));
fclose($file);
$db = new MySqlDatabase($hostname, $username, $password, $database);
$db->Query($setupSql);
echo 'Ringfinger setup has successfully finished.' . PHP_EOL;

39
backend/scripts/setup.sql Normal file
View File

@ -0,0 +1,39 @@
START TRANSACTION;
USE ringfinger;
CREATE TABLE User (
UserId int(10) unsigned NOT NULL AUTO_INCREMENT,
Username varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL,
Password varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL,
Email varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
JabberAddress varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
IsAdmin BOOL NOT NULL DEFAULT 0,
PRIMARY KEY (UserId),
UNIQUE KEY Username (Username),
UNIQUE KEY Email (Email),
UNIQUE KEY JabberAddress (JabberAddress)
);
CREATE TABLE Fingerprint (
FingerprintId int(10) unsigned NOT NULL AUTO_INCREMENT,
Fingerprint varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
UserId int(10) unsigned NOT NULL,
PRIMARY KEY (FingerprintId),
UNIQUE KEY Fingerprint (Fingerprint),
KEY User (UserId),
CONSTRAINT User FOREIGN KEY (UserId) REFERENCES User (UserId) ON DELETE CASCADE
);
CREATE TABLE Sharing (
SharingId int(10) unsigned NOT NULL AUTO_INCREMENT,
User int(10) unsigned NOT NULL,
UserShared int(10) unsigned NOT NULL,
PRIMARY KEY (SharingId),
UNIQUE KEY User (User, UserShared),
KEY UserSharedId (UserShared),
CONSTRAINT UserId FOREIGN KEY (User) REFERENCES User (UserId) ON DELETE CASCADE,
CONSTRAINT UserSharedId FOREIGN KEY (UserShared) REFERENCES User (UserId) ON DELETE CASCADE
);
COMMIT;

View File

@ -1,13 +0,0 @@
<?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

@ -1,27 +0,0 @@
<?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

@ -1,77 +0,0 @@
<?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

@ -1,12 +0,0 @@
<?php
declare(strict_types=1);
class ApiSuccessResponse extends ApiResponse
{
public function __construct(bool $success = true)
{
parent::__construct();
$this->setParameter('success', $success);
}
}

View File

@ -1,26 +0,0 @@
<?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

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

View File

@ -1,32 +0,0 @@
<?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

@ -1,72 +0,0 @@
<?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();
$db = new MySqlDatabase();
$json = json_decode($this->requestBody);
$fingerprint = new Fingerprint(null, $db);
try {
$fingerprint->setFingerprint($json->fingerprint);
$fingerprint->setUserId($json->userId);
if (!$db->hasTransaction()) {
$db->startTransaction();
}
$fingerprint->Save();
$qrCode = new QrCode($fingerprint->getFingerprintId(), $fingerprint->getFingerprint());
$qrCode->generate();
$qrCode->save();
$db->commit();
$this->response->setParameter('fingerprintId', $fingerprint->getFingerprintId());
} catch (QrCodeException $e) {
$db->rollback();
$this->response->setParameter('success', false);
$this->response->setStatus(ServerStatus::INTERNAL_ERROR);
$this->response->setMessage('An error occured during qr code creation!');
} catch (Throwable $e) {
$db->rollback();
$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

@ -1,35 +0,0 @@
<?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

@ -1,25 +0,0 @@
<?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

@ -1,43 +0,0 @@
<?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

@ -1,28 +0,0 @@
<?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

@ -1,83 +0,0 @@
<?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

@ -1,245 +0,0 @@
<?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

@ -1,80 +0,0 @@
<?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 if there's an open transaction.
*/
public function hasTransaction(): bool;
/*
* Starts a transaction that can later be committed or rolled back.
*/
public function startTransaction(): void;
/*
* Quits a current transaction without saving.
*/
public function rollback(): void;
/*
* Saves and exits a current transaction.
*/
public function commit(): void;
/**
* Returns the primary key from the last inserted row.
*/
public function GetLastInsertedId(): int;
}

View File

@ -1,10 +0,0 @@
<?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

@ -1,286 +0,0 @@
<?php
declare(strict_types=1);
final class MySqlDatabase implements DatabaseInterface
{
private const CHARS_ALLOWED_IN_TABLE_NAMES = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890_-';
private ?PDO $connection;
private ?PDOStatement $cursor;
private bool $isTransactionOpen = false;
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(*)'];
}
public function hasTransaction(): bool
{
return $this->isTransactionOpen;
}
public function startTransaction(): void
{
$this->connection->beginTransaction();
$this->isTransactionOpen = true;
}
public function rollback(): void
{
$this->connection->rollBack();
$this->isTransactionOpen = false;
}
public function commit(): void
{
$this->connection->commit();
$this->isTransactionOpen = false;
}
/**
* {@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

@ -1,18 +0,0 @@
<?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

@ -1,16 +0,0 @@
<?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

@ -1,67 +0,0 @@
<?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

@ -1,57 +0,0 @@
<?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

@ -1,13 +0,0 @@
<?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

@ -1,108 +0,0 @@
<?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);
}
}

View File

@ -1,284 +0,0 @@
<?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

@ -1,53 +0,0 @@
<?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

@ -1,43 +0,0 @@
<?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

@ -1,133 +0,0 @@
<?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

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

View File

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

View File

View File

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