initial commit for soko-web
soko-web/pipeline/head This commit looks good

This commit is contained in:
bence committed 2023-04-07 19:32:15 +02:00
commit a17a88e9d4
71 files changed
+7241

No files matched your search

+23
View File
@@ -0,0 +1,23 @@
<?php
define('SCRIPT_STARTED', hrtime(true));
require 'vendor/autoload.php';
const ROOT = __DIR__;
const VERSION = '';
const REVISION = '';
const REVISION_DATE = '';
$dotenv = Dotenv\Dotenv::createImmutable(ROOT);
$dotenv->load();
class Container
{
static SokoWeb\Interfaces\Database\IConnection $dbConnection;
static SokoWeb\Routing\RouteCollection $routeCollection;
static SokoWeb\Interfaces\Session\ISessionHandler $sessionHandler;
static SokoWeb\Interfaces\Request\IRequest $request;
}
Container::$dbConnection = new SokoWeb\Database\Mysql\Connection($_ENV['DB_HOST'], $_ENV['DB_USER'], $_ENV['DB_PASSWORD'], $_ENV['DB_NAME']);
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env php
<?php
require 'main.php';
$app = new Symfony\Component\Console\Application('{app} Console', '');
$app->add(new {app}\Cli\MigrateDatabaseCommand());
$app->add(new {app}\Cli\AddUserCommand());
$app->add(new {app}\Cli\LinkViewCommand());
$app->run();
+15
View File
@@ -0,0 +1,15 @@
SET NAMES utf8mb4;
SET foreign_key_checks = 0;
CREATE TABLE `users` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`email` varchar(100) NOT NULL,
`password` varchar(60) DEFAULT NULL,
`type` enum('user','admin') NOT NULL,
`active` tinyint(1) NOT NULL DEFAULT 0,
`google_sub` varchar(255) CHARACTER SET ascii COLLATE ascii_bin DEFAULT NULL,
`created` timestamp NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `email` (`email`),
UNIQUE KEY `google_sub` (`google_sub`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+51
View File
@@ -0,0 +1,51 @@
<?php namespace {app}\Cli;
use SokoWeb\PersistentData\PersistentDataManager;
use {app}\PersistentData\Model\User;
use DateTime;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class AddUserCommand extends Command
{
public function configure(): void
{
$this->setName('user:add')
->setDescription('Adding of user.')
->addArgument('email', InputArgument::REQUIRED, 'Email of user')
->addArgument('password', InputArgument::REQUIRED, 'Password of user')
->addArgument('type', InputArgument::OPTIONAL, 'Type of user');
}
public function execute(InputInterface $input, OutputInterface $output): int
{
$user = new User();
$user->setEmail($input->getArgument('email'));
$user->setPlainPassword($input->getArgument('password'));
$user->setActive(true);
$user->setCreatedDate(new DateTime());
if ($input->hasArgument('type') && $input->getArgument('type') !== null) {
$user->setType($input->getArgument('type'));
}
try {
$pdm = new PersistentDataManager();
$pdm->saveToDb($user);
} catch (\Exception $e) {
$output->writeln('<error>Adding user failed!</error>');
$output->writeln('');
$output->writeln((string) $e);
$output->writeln('');
return 1;
}
$output->writeln('<info>User was successfully added!</info>');
return 0;
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php namespace {app}\Cli;
use SokoWeb\View\Linker;
use FilesystemIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class LinkViewCommand extends Command
{
public function configure(): void
{
$this->setName('view:link')
->setDescription('Linking of views.')
->addArgument('view', InputArgument::OPTIONAL, 'View file to be linked.');
}
public function execute(InputInterface $input, OutputInterface $output): int
{
$views = [];
$view = $input->getArgument('view');
if ($view !== null) {
$views[] = $view;
} else {
$folder = ROOT . '/views';
$folderLength = strlen($folder) + 1;
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($folder, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST);
foreach ($iterator as $file) {
if ($file->isDir() || $file->getExtension() !== 'php') {
continue;
}
$view = substr($file->getPath(), $folderLength) . '/' . $file->getBasename('.php');
if (strpos($view, 'templates') === 0 || strpos($view, 'tests') === 0) {
continue;
}
$views[] = $view;
}
}
try {
foreach ($views as $view) {
$generator = new Linker($view);
$generator->generate();
}
} catch (\Exception $e) {
$output->writeln('<error>Linking view(s) failed!</error>');
$output->writeln('');
$output->writeln((string) $e);
$output->writeln('');
return 1;
}
$output->writeln('<info>View(s) successfully linked!</info>');
return 0;
}
}
@@ -0,0 +1,122 @@
<?php namespace {app}\Cli;
use SokoWeb\Database\Query\Modify;
use SokoWeb\Database\Query\Select;
use SokoWeb\Interfaces\Database\IResultSet;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class MigrateDatabaseCommand extends Command
{
public function configure(): void
{
$this->setName('db:migrate')
->setDescription('Migration of database changes.');
}
public function execute(InputInterface $input, OutputInterface $output): int
{
$db = \Container::$dbConnection;
$db->startTransaction();
$success = [];
try {
foreach ($this->readDir('structure') as $file) {
$db->multiQuery(file_get_contents($file));
$success[] = $this->saveToDB($file, 'structure');
}
foreach ($this->readDir('data') as $file) {
require $file;
$success[] = $this->saveToDB($file, 'data');
}
} catch (\Exception $e) {
$db->rollback();
$output->writeln('<error>Migration failed!</error>');
$output->writeln('');
$output->writeln((string) $e);
$output->writeln('');
return 1;
}
$db->commit();
$output->writeln('<info>Migration was successful!</info>');
$output->writeln('');
if (count($success) > 0) {
foreach ($success as $migration) {
$output->writeln($migration);
}
$output->writeln('');
}
return 0;
}
private function readDir(string $type): array
{
$done = [];
$migrationTableExists = \Container::$dbConnection->query('SELECT count(*)
FROM information_schema.tables
WHERE table_schema = \'' . $_ENV['DB_NAME'] . '\'
AND table_name = \'migrations\';')
->fetch(IResultSet::FETCH_NUM)[0];
if ($migrationTableExists != 0) {
$select = new Select(\Container::$dbConnection, 'migrations');
$select->columns(['migration']);
$select->where('type', '=', $type);
$select->orderBy('migration');
$result = $select->execute();
while ($migration = $result->fetch(IResultSet::FETCH_ASSOC)) {
$done[] = $migration['migration'];
}
}
$path = ROOT . '/database/migrations/' . $type;
$dir = opendir($path);
if ($dir === false) {
throw new \Exception('Cannot open dir: ' . $path);
}
$files = [];
while ($file = readdir($dir)) {
$filePath = $path . '/' . $file;
if (!is_file($filePath) || in_array(pathinfo($file, PATHINFO_FILENAME), $done)) {
continue;
}
$files[] = $filePath;
}
natsort($files);
return $files;
}
private function saveToDB(string $file, string $type): string
{
$baseName = pathinfo($file, PATHINFO_FILENAME);
$modify = new Modify(\Container::$dbConnection, 'migrations');
$modify->set('migration', $baseName);
$modify->set('type', $type);
$modify->save();
return $baseName . ' (' . $type . ')';
}
}
@@ -0,0 +1,20 @@
<?php namespace {app}\Controller;
use SokoWeb\Interfaces\Request\IRequest;
use SokoWeb\Interfaces\Response\IContent;
use SokoWeb\Response\HtmlContent;
class HomeController
{
private IRequest $request;
public function __construct(IRequest $request)
{
$this->request = $request;
}
public function getIndex(): IContent
{
return new HtmlContent('index');
}
}
@@ -0,0 +1,133 @@
<?php namespace {app}\PersistentData\Model;
use DateTime;
use SokoWeb\Interfaces\Authentication\IUser;
class User extends Model implements IUser
{
protected static string $table = 'users';
protected static array $fields = ['email', 'password', 'type', 'active', 'google_sub', 'created'];
private static array $types = ['user', 'admin'];
private string $email = '';
private ?string $password = null;
private string $type = 'user';
private bool $active = false;
private ?string $googleSub = null;
private DateTime $created;
public function setEmail(string $email): void
{
$this->email = $email;
}
public function setPassword(?string $hashedPassword): void
{
$this->password = $hashedPassword;
}
public function setPlainPassword(string $plainPassword): void
{
$this->password = password_hash($plainPassword, PASSWORD_BCRYPT);
}
public function setType(string $type): void
{
if (in_array($type, self::$types)) {
$this->type = $type;
}
}
public function setActive(bool $active): void
{
$this->active = $active;
}
public function setGoogleSub(?string $googleSub): void
{
$this->googleSub = $googleSub;
}
public function setCreatedDate(DateTime $created): void
{
$this->created = $created;
}
public function setCreated(string $created): void
{
$this->created = new DateTime($created);
}
public function getEmail(): string
{
return $this->email;
}
public function getPassword(): ?string
{
return $this->password;
}
public function getType(): string
{
return $this->type;
}
public function getActive(): bool
{
return $this->active;
}
public function getGoogleSub(): ?string
{
return $this->googleSub;
}
public function getCreatedDate(): DateTime
{
return $this->created;
}
public function getCreated(): string
{
return $this->created->format('Y-m-d H:i:s');
}
public function hasPermission(int $permission): bool
{
switch ($permission) {
case IUser::PERMISSION_NORMAL:
return true;
case IUser::PERMISSION_ADMIN:
return $this->type === 'admin';
default:
throw new \Exception('Permission does not exist: ' . $permission);
}
}
public function getUniqueId()
{
return $this->id;
}
public function getDisplayName(): string
{
return $this->email;
}
public function checkPassword(string $password): bool
{
if ($this->password === null) {
return false;
}
return password_verify($password, $this->password);
}
}
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{app}</title>
</head>
<body>
<p>Hello World! This is {app}.</p>
</body>
</html>
+56
View File
@@ -0,0 +1,56 @@
<?php
require 'main.php';
if (!empty($_ENV['DEV'])) {
error_reporting(E_ALL);
ini_set('display_errors', '1');
} else {
ini_set('display_errors', '0');
}
Container::$routeCollection = new SokoWeb\Routing\RouteCollection();
Container::$routeCollection->get('index', '', [{app}\Controller\HomeController::class, 'getIndex']);
if (isset($_COOKIE['COOKIES_CONSENT'])) {
Container::$sessionHandler = new SokoWeb\Session\DatabaseSessionHandler();
session_set_save_handler(Container::$sessionHandler, true);
session_start([
'gc_probability' => 0, // old sessions are deleted by MaintainDatabaseCommand
'cookie_lifetime' => 604800,
'cookie_path' => '/',
'cookie_httponly' => true,
'cookie_samesite' => 'Lax'
]);
if (isset($_COOKIE[session_name()])) {
// extend session cookie lifetime is cookie already exists
setcookie(session_name(), session_id(), [
'expires' => time() + 604800,
'path' => '/',
'httponly' => true,
'samesite' => 'Lax'
]);
}
// this is needed to handle old type of session IDs
if (!Container::$sessionHandler->validateId(session_id())) {
session_regenerate_id(true);
}
} else {
$_SESSION = [];
}
Container::$request = new MapGuesser\Request\Request(
$_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'],
$_GET,
$_POST,
$_SESSION,
new MapGuesser\Repository\UserRepository()
);
if (!Container::$request->session()->has('anti_csrf_token')) {
Container::$request->session()->set('anti_csrf_token', bin2hex(random_bytes(16)));
}