From adc1c38d54aed9ad3531183c712a2a20e82f5c4b Mon Sep 17 00:00:00 2001 From: Mal <=> Date: Mon, 17 Aug 2020 23:46:58 +0200 Subject: [PATCH] Init --- .gitignore | 5 + Makefile | 6 + api/v1/index.php | 15 + data/classes/api/ApiBadRequestResponse.php | 13 + data/classes/api/ApiJsonResponse.php | 27 ++ data/classes/api/ApiResponse.php | 77 +++++ data/classes/api/ApiSuccessResponse.php | 12 + data/classes/api/ApiSvgResponse.php | 26 ++ data/classes/api/ApiUnauthorizedResponse.php | 12 + .../controller/FingerprintGetController.php | 32 ++ .../controller/FingerprintPostController.php | 63 ++++ .../controller/QrCodeGetController.php | 35 +++ data/classes/controller/UserGetController.php | 25 ++ .../controller/UserLoginPutController.php | 43 +++ .../controller/UserLogoutPutController.php | 28 ++ data/classes/core/AbstractController.php | 83 +++++ data/classes/core/Autoloader.php | 245 +++++++++++++++ data/classes/core/DatabaseInterface.php | 60 ++++ data/classes/core/MimeType.php | 10 + data/classes/core/MySqlDatabase.php | 262 ++++++++++++++++ data/classes/core/MySqlTable.php | 18 ++ data/classes/core/Password.php | 16 + data/classes/core/QrCode.php | 67 +++++ data/classes/core/Router.php | 57 ++++ data/classes/core/ServerStatus.php | 13 + data/classes/core/Session.php | 108 +++++++ data/classes/core/Table.php | 284 ++++++++++++++++++ data/classes/database/Fingerprint.php | 53 ++++ data/classes/database/Sharing.php | 43 +++ data/classes/database/User.php | 133 ++++++++ data/classes/exception/DatabaseException.php | 7 + data/classes/exception/QrCodeException.php | 7 + data/classes/exception/UserException.php | 7 + data/scripts/generate.php | 5 + docs/api/v1.yaml | 0 index.php | 5 + 36 files changed, 1902 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 api/v1/index.php create mode 100644 data/classes/api/ApiBadRequestResponse.php create mode 100644 data/classes/api/ApiJsonResponse.php create mode 100644 data/classes/api/ApiResponse.php create mode 100644 data/classes/api/ApiSuccessResponse.php create mode 100644 data/classes/api/ApiSvgResponse.php create mode 100644 data/classes/api/ApiUnauthorizedResponse.php create mode 100644 data/classes/controller/FingerprintGetController.php create mode 100644 data/classes/controller/FingerprintPostController.php create mode 100644 data/classes/controller/QrCodeGetController.php create mode 100644 data/classes/controller/UserGetController.php create mode 100644 data/classes/controller/UserLoginPutController.php create mode 100644 data/classes/controller/UserLogoutPutController.php create mode 100644 data/classes/core/AbstractController.php create mode 100644 data/classes/core/Autoloader.php create mode 100644 data/classes/core/DatabaseInterface.php create mode 100644 data/classes/core/MimeType.php create mode 100644 data/classes/core/MySqlDatabase.php create mode 100644 data/classes/core/MySqlTable.php create mode 100644 data/classes/core/Password.php create mode 100644 data/classes/core/QrCode.php create mode 100644 data/classes/core/Router.php create mode 100644 data/classes/core/ServerStatus.php create mode 100644 data/classes/core/Session.php create mode 100644 data/classes/core/Table.php create mode 100644 data/classes/database/Fingerprint.php create mode 100644 data/classes/database/Sharing.php create mode 100644 data/classes/database/User.php create mode 100644 data/classes/exception/DatabaseException.php create mode 100644 data/classes/exception/QrCodeException.php create mode 100644 data/classes/exception/UserException.php create mode 100644 data/scripts/generate.php create mode 100644 docs/api/v1.yaml create mode 100644 index.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..617d403 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +data/classes/Setting.php +data/cache +data/tmp +data/qr +.idea diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..99d1947 --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +build: + php data/scripts/generate.php + +clean: + rm -rf data/cache/* data/tmp/* + diff --git a/api/v1/index.php b/api/v1/index.php new file mode 100644 index 0000000..730624f --- /dev/null +++ b/api/v1/index.php @@ -0,0 +1,15 @@ +setRequestBody($_SERVER['HTTP_CONTENT_TYPE'], file_get_contents('php://input')); +} + +$router->route(); diff --git a/data/classes/api/ApiBadRequestResponse.php b/data/classes/api/ApiBadRequestResponse.php new file mode 100644 index 0000000..5dd4c05 --- /dev/null +++ b/data/classes/api/ApiBadRequestResponse.php @@ -0,0 +1,13 @@ +setParameter('success', false); + $this->setStatus(self::STATUS_BAD_REQUEST); + } +} \ No newline at end of file diff --git a/data/classes/api/ApiJsonResponse.php b/data/classes/api/ApiJsonResponse.php new file mode 100644 index 0000000..9965eb6 --- /dev/null +++ b/data/classes/api/ApiJsonResponse.php @@ -0,0 +1,27 @@ +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); + } +} \ No newline at end of file diff --git a/data/classes/api/ApiResponse.php b/data/classes/api/ApiResponse.php new file mode 100644 index 0000000..827c85b --- /dev/null +++ b/data/classes/api/ApiResponse.php @@ -0,0 +1,77 @@ +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); + } +} diff --git a/data/classes/api/ApiSuccessResponse.php b/data/classes/api/ApiSuccessResponse.php new file mode 100644 index 0000000..7d766b3 --- /dev/null +++ b/data/classes/api/ApiSuccessResponse.php @@ -0,0 +1,12 @@ +setParameter('success', $success); + } +} diff --git a/data/classes/api/ApiSvgResponse.php b/data/classes/api/ApiSvgResponse.php new file mode 100644 index 0000000..c306c96 --- /dev/null +++ b/data/classes/api/ApiSvgResponse.php @@ -0,0 +1,26 @@ +content = $content; + } + + public function respond(): void + { + parent::respond(); + header('Content-Length: ' . strlen($this->content)); + echo $this->content; + } +} diff --git a/data/classes/api/ApiUnauthorizedResponse.php b/data/classes/api/ApiUnauthorizedResponse.php new file mode 100644 index 0000000..b3d1fef --- /dev/null +++ b/data/classes/api/ApiUnauthorizedResponse.php @@ -0,0 +1,12 @@ +setStatus(self::STATUS_UNAUTHORIZED); + } +} diff --git a/data/classes/controller/FingerprintGetController.php b/data/classes/controller/FingerprintGetController.php new file mode 100644 index 0000000..8233261 --- /dev/null +++ b/data/classes/controller/FingerprintGetController.php @@ -0,0 +1,32 @@ +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); + } + } +} \ No newline at end of file diff --git a/data/classes/controller/FingerprintPostController.php b/data/classes/controller/FingerprintPostController.php new file mode 100644 index 0000000..77dbc00 --- /dev/null +++ b/data/classes/controller/FingerprintPostController.php @@ -0,0 +1,63 @@ +response = new ApiJsonResponse(); + } + + public function handle(): void + { + parent::handle(); + + $json = json_decode($this->requestBody); + $fingerprint = new Fingerprint(); + + try { + $fingerprint->setFingerprint($json->fingerprint); + $fingerprint->setUserId($json->userId); + $fingerprint->Save(); + + $qrCode = new QrCode($fingerprint->getFingerprintId(), $fingerprint->getFingerprint()); + $qrCode->generate(); + $qrCode->save(); + + $this->response->setParameter('fingerprintId', $fingerprint->getFingerprintId()); + } catch (QrCodeException $e) { + $fingerprint->Delete(); + + $this->response->setParameter('success', false); + $this->response->setStatus(ServerStatus::INTERNAL_ERROR); + $this->response->setMessage('An error occured during qr code creation!'); + } catch (Throwable $e) { + $this->catchDatabaseException($e->getMessage(), $json); + } + } + + private function catchDatabaseException(string $message, object $json): void + { + $this->response->setParameter('success', false); + + if (substr_count($message, 'foreign key constraint fails') > 0) { + $this->response->setMessage(sprintf('User with id %d doesn\'t exist!', $json->userId)); + $this->response->setStatus(ServerStatus::NOT_FOUND); + } elseif (substr_count($message, 'Duplicate entry') > 0) { + $this->response->setMessage(sprintf('Fingerprint %s already exists!', $json->fingerprint)); + $this->response->setStatus(ServerStatus::BAD_REQUEST); + } else { + $this->response->setMessage($message); + $this->response->setStatus(ServerStatus::INTERNAL_ERROR); + } + } +} \ No newline at end of file diff --git a/data/classes/controller/QrCodeGetController.php b/data/classes/controller/QrCodeGetController.php new file mode 100644 index 0000000..5a05289 --- /dev/null +++ b/data/classes/controller/QrCodeGetController.php @@ -0,0 +1,35 @@ +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); + } +} \ No newline at end of file diff --git a/data/classes/controller/UserGetController.php b/data/classes/controller/UserGetController.php new file mode 100644 index 0000000..2ecf056 --- /dev/null +++ b/data/classes/controller/UserGetController.php @@ -0,0 +1,25 @@ +userId = (int)$this->getUrlParamInt('userId'); + } + + public function handle(): void + { + $user = new User($this->userId); + + $this->response = new ApiJsonResponse(); + $this->response->setResult($user); + } +} \ No newline at end of file diff --git a/data/classes/controller/UserLoginPutController.php b/data/classes/controller/UserLoginPutController.php new file mode 100644 index 0000000..ae30bb5 --- /dev/null +++ b/data/classes/controller/UserLoginPutController.php @@ -0,0 +1,43 @@ +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(); + } +} \ No newline at end of file diff --git a/data/classes/controller/UserLogoutPutController.php b/data/classes/controller/UserLogoutPutController.php new file mode 100644 index 0000000..b7636c3 --- /dev/null +++ b/data/classes/controller/UserLogoutPutController.php @@ -0,0 +1,28 @@ +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); + } +} \ No newline at end of file diff --git a/data/classes/core/AbstractController.php b/data/classes/core/AbstractController.php new file mode 100644 index 0000000..ea1acef --- /dev/null +++ b/data/classes/core/AbstractController.php @@ -0,0 +1,83 @@ +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; + } + } +} diff --git a/data/classes/core/Autoloader.php b/data/classes/core/Autoloader.php new file mode 100644 index 0000000..6fa7e6f --- /dev/null +++ b/data/classes/core/Autoloader.php @@ -0,0 +1,245 @@ +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( + "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); + } +} diff --git a/data/classes/core/DatabaseInterface.php b/data/classes/core/DatabaseInterface.php new file mode 100644 index 0000000..795ab51 --- /dev/null +++ b/data/classes/core/DatabaseInterface.php @@ -0,0 +1,60 @@ +connection = new PDO("mysql:host=$hostname;dbname=$database", $user, $password); + } + + public function __destruct() + { + $this->connection = null; + } + + /** + * {@inheritDoc} + */ + public function Query(string $query, array $params = []): void + { + $this->cursor = $this->connection->prepare($query); + + if (!$this->cursor) { + throw new Exception('Initialization of database cursor failed'); + } + + foreach ($params as $key => $param) { + if (is_bool($param)) { + $param = (int)$param; + } + + $this->cursor->bindValue(':' . $key, $param); + } + + if (!$this->cursor->execute()) { + throw new Exception($this->cursor->errorInfo()[2]); + } + } + + /** + * {@inheritDoc} + */ + public function getResult(): array + { + $result = []; + + while ($fetch = $this->cursor->fetchObject()) { + $row = []; + + foreach (get_object_vars($fetch) as $key => $value) { + $row[$key] = $value; + } + + $result[] = $row; + } + + return $result; + } + + /** + * Selects data from a table. + */ + public function Select( + string $tableName, + array $fields = [], + array $conditions = [], + int $limit = 0, + array $orderBy = [], + bool $asc = true, + int $offset = 0 + ): array { + if (!self::isValidTableName($tableName)) { + []; + } + + if (count($fields) === 0) { + $fieldsExpression = '*'; + } else { + $fieldsExpression = implode(',', $fields); + } + + $conditionsExpression = ''; + $conditionPairs = []; + + foreach ($conditions as $field => $value) { + $conditionPairs[] = sprintf('%s = :%s ', $field, $field); + } + + if (count($conditions) > 0) { + $conditionsExpression = 'WHERE '; + $conditionsExpression .= implode(' AND ', $conditionPairs); + } + + $orderStatement = ''; + + if (count($orderBy) > 0) { + $orderStatement = 'ORDER BY ' . implode(',', $orderBy); + + if (!$asc) { + $orderStatement .= ' DESC'; + } + } + + $limitStatement = ''; + + if ($limit > 0) { + $limitStatement = 'LIMIT ' . $limit; + } + + $offsetStatement = ''; + + if ($offset > 0) { + $offsetStatement = 'OFFSET ' . $offset; + } + + $query = sprintf( + 'SELECT %s FROM %s %s %s %s %s', + $fieldsExpression, + $tableName, + $conditionsExpression, + $orderStatement, + $limitStatement, + $offsetStatement + ); + + try { + $this->Query($query, $conditions); + } catch (Throwable $e) { + return []; + } + + return $this->getResult(); + } + + /** + * Deletes rows from a table. + */ + public function Delete(string $table, array $conditions): void + { + if (count($conditions) === 0) { + $conditionsStatement = '1'; + } else { + $conditionPairs = []; + + foreach ($conditions as $field => $value) { + $conditionPairs[] = sprintf('%s=:Condition%s', $field, $field); + $conditions['Condition' . $field] = $value; + unset($conditions[$field]); + } + + $conditionsStatement = implode(' AND ', $conditionPairs); + } + + $query = sprintf('DELETE FROM %s WHERE %s', $table, $conditionsStatement); + + $this->Query($query, $conditions); + } + + /** + * {@inheritDoc} + */ + public function Insert(string $table, array $fields): ?int + { + if (count($fields) === 0) { + throw new Exception('Row to insert is empty!'); + } + + $fieldNames = implode(',', array_keys($fields)); + $fieldPlaceholder = []; + + foreach ($fields as $name => $value) { + $fieldPlaceholder[] = ':' . $name; + } + + $query = sprintf( + 'INSERT INTO %s (%s) VALUES (%s)', $table, $fieldNames, implode(',', $fieldPlaceholder) + ); + + $this->Query($query, $fields); + + $lastInsertedId = $this->GetLastInsertedId(); + + if ((int)$lastInsertedId === 0) { + return null; + } + + return $lastInsertedId; + } + + /** + * {@inheritDoc} + */ + public function Update(string $table, array $fields, array $conditions): void + { + $conditionPairs = []; + + foreach ($conditions as $field => $value) { + $conditionPairs[] = sprintf('%s=:Condition%s', $field, $field); + $conditions['Condition' . $field] = $value; + unset($conditions[$field]); + } + + $conditionsStatement = implode(' AND ', $conditionPairs); + + $fieldPairs = []; + + foreach ($fields as $field => $value) { + $fieldPairs[] = sprintf('%s=:%s', $field, $field); + } + + $fieldsStatement = implode(',', $fieldPairs); + + $query = sprintf('UPDATE %s SET %s WHERE %s', $table, $fieldsStatement, $conditionsStatement); + + + $this->Query($query, array_merge($fields, $conditions)); + } + + /** + * {@inheritDoc} + */ + public function Count(string $table, array $conditions = []): int + { + $result = $this->Select($table, ['count(*)'], $conditions); + + return (int)$result[0]['count(*)']; + } + + /** + * {@inheritDoc} + */ + public function GetLastInsertedId(): int + { + $this->Query('SELECT LAST_INSERT_ID() as ID'); + + return (int)$this->getResult()[0]['ID']; + } + + /** + * Does a check if the given table name contains forbidden chars. + */ + private static function isValidTableName(string $tableName): bool + { + foreach (str_split($tableName) as $char) { + if (substr_count(self::CHARS_ALLOWED_IN_TABLE_NAMES, $char) === 0) { + return false; + } + } + + return true; + } +} diff --git a/data/classes/core/MySqlTable.php b/data/classes/core/MySqlTable.php new file mode 100644 index 0000000..2e176af --- /dev/null +++ b/data/classes/core/MySqlTable.php @@ -0,0 +1,18 @@ +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); + } +} \ No newline at end of file diff --git a/data/classes/core/Router.php b/data/classes/core/Router.php new file mode 100644 index 0000000..de16905 --- /dev/null +++ b/data/classes/core/Router.php @@ -0,0 +1,57 @@ +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) . '/'; + } +} \ No newline at end of file diff --git a/data/classes/core/ServerStatus.php b/data/classes/core/ServerStatus.php new file mode 100644 index 0000000..2cff70f --- /dev/null +++ b/data/classes/core/ServerStatus.php @@ -0,0 +1,13 @@ +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); + } +} \ No newline at end of file diff --git a/data/classes/core/Table.php b/data/classes/core/Table.php new file mode 100644 index 0000000..1464659 --- /dev/null +++ b/data/classes/core/Table.php @@ -0,0 +1,284 @@ +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)); + } + } +} diff --git a/data/classes/database/Fingerprint.php b/data/classes/database/Fingerprint.php new file mode 100644 index 0000000..aa2d6c4 --- /dev/null +++ b/data/classes/database/Fingerprint.php @@ -0,0 +1,53 @@ +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() + ]; + } +} diff --git a/data/classes/database/Sharing.php b/data/classes/database/Sharing.php new file mode 100644 index 0000000..d9b4999 --- /dev/null +++ b/data/classes/database/Sharing.php @@ -0,0 +1,43 @@ +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); + } +} diff --git a/data/classes/database/User.php b/data/classes/database/User.php new file mode 100644 index 0000000..f3e4f61 --- /dev/null +++ b/data/classes/database/User.php @@ -0,0 +1,133 @@ +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() + ]; + } +} diff --git a/data/classes/exception/DatabaseException.php b/data/classes/exception/DatabaseException.php new file mode 100644 index 0000000..c33d8cd --- /dev/null +++ b/data/classes/exception/DatabaseException.php @@ -0,0 +1,7 @@ +