Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e67afc401b
|
||
|
|
e59d627080
|
||
|
|
3acca19d49
|
||
|
|
2226b88a88
|
||
|
|
8e08b09ae8
|
||
|
|
a3bce1f2aa
|
||
|
|
a84d3a3976
|
||
|
|
7210b24aa3
|
||
|
|
2d48f20aed
|
||
|
|
ebe1fa2aa6
|
||
|
|
bccee89c13
|
||
|
|
8bf495c89b
|
||
|
|
74cb576a2e
|
||
|
|
4283bc9bb1
|
||
|
|
fc4c3234a7
|
No files matched your search
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM php:7.4.7-cli-buster
|
FROM php:8.1-cli-bookworm
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y unzip
|
RUN apt-get update && apt-get install -y unzip
|
||||||
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
|
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
|
||||||
+3
-3
@@ -5,12 +5,12 @@
|
|||||||
"license": "GNU GPL 3.0",
|
"license": "GNU GPL 3.0",
|
||||||
"require": {
|
"require": {
|
||||||
"vlucas/phpdotenv": "^5.5",
|
"vlucas/phpdotenv": "^5.5",
|
||||||
"symfony/console": "^5.4",
|
"symfony/console": "^6.3",
|
||||||
"phpmailer/phpmailer": "^6.8",
|
"phpmailer/phpmailer": "^6.8",
|
||||||
"cocur/slugify": "^4.3"
|
"cocur/slugify": "^4.5"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": "^9.6",
|
"phpunit/phpunit": "^10.3",
|
||||||
"phpstan/phpstan": "^1.10"
|
"phpstan/phpstan": "^1.10"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
|
|||||||
Generated
+366
-568
File diff suppressed because it is too large.
Load diff
@@ -7,48 +7,52 @@ use mysqli;
|
|||||||
|
|
||||||
class Connection implements IConnection
|
class Connection implements IConnection
|
||||||
{
|
{
|
||||||
private mysqli $connection;
|
private string $host;
|
||||||
|
private string $user;
|
||||||
|
private string $password;
|
||||||
|
private string $db;
|
||||||
|
private int $port;
|
||||||
|
private string $socket;
|
||||||
|
private ?mysqli $connection = null;
|
||||||
|
|
||||||
public function __construct(string $host, string $user, string $password, string $db, int $port = -1, string $socket = null)
|
public function __construct(string $host, string $user, string $password, string $db, int $port = -1, string $socket = null)
|
||||||
{
|
{
|
||||||
if ($port < 0) {
|
$this->host = $host;
|
||||||
$port = (int) ini_get('mysqli.default_port');
|
$this->user = $user;
|
||||||
}
|
$this->password = $password;
|
||||||
|
$this->db = $db;
|
||||||
if ($socket === null) {
|
$this->port = $port < 0 ? (int) ini_get('mysqli.default_port') : $port;
|
||||||
$socket = (string) ini_get('mysqli.default_socket');
|
$this->socket = $socket === null ? (string) ini_get('mysqli.default_socket') : $socket;
|
||||||
}
|
|
||||||
|
|
||||||
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
|
|
||||||
$this->connection = new mysqli($host, $user, $password, $db, $port, $socket);
|
|
||||||
$this->connection->set_charset('utf8mb4');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __destruct()
|
public function __destruct()
|
||||||
{
|
{
|
||||||
|
if ($this->connection === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
$this->connection->close();
|
$this->connection->close();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function startTransaction(): void
|
public function startTransaction(): void
|
||||||
{
|
{
|
||||||
$this->connection->autocommit(false);
|
$this->getConnection()->autocommit(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function commit(): void
|
public function commit(): void
|
||||||
{
|
{
|
||||||
$this->connection->commit();
|
$this->getConnection()->commit();
|
||||||
$this->connection->autocommit(true);
|
$this->getConnection()->autocommit(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rollback(): void
|
public function rollback(): void
|
||||||
{
|
{
|
||||||
$this->connection->rollback();
|
$this->getConnection()->rollback();
|
||||||
$this->connection->autocommit(true);
|
$this->getConnection()->autocommit(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function query(string $query): ?IResultSet
|
public function query(string $query): ?IResultSet
|
||||||
{
|
{
|
||||||
$result = $this->connection->query($query);
|
$result = $this->getConnection()->query($query);
|
||||||
|
|
||||||
if ($result !== true) {
|
if ($result !== true) {
|
||||||
return new ResultSet($result);
|
return new ResultSet($result);
|
||||||
@@ -59,36 +63,51 @@ class Connection implements IConnection
|
|||||||
|
|
||||||
public function multiQuery(string $query): array
|
public function multiQuery(string $query): array
|
||||||
{
|
{
|
||||||
$this->connection->multi_query($query);
|
$this->getConnection()->multi_query($query);
|
||||||
|
|
||||||
$ret = [];
|
$ret = [];
|
||||||
do {
|
do {
|
||||||
if ($result = $this->connection->store_result()) {
|
if ($result = $this->getConnection()->store_result()) {
|
||||||
$ret[] = new ResultSet($result);
|
$ret[] = new ResultSet($result);
|
||||||
} else {
|
} else {
|
||||||
$ret[] = null;
|
$ret[] = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->connection->more_results();
|
$this->getConnection()->more_results();
|
||||||
} while ($this->connection->next_result());
|
} while ($this->getConnection()->next_result());
|
||||||
|
|
||||||
return $ret;
|
return $ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function prepare(string $query): IStatement
|
public function prepare(string $query): IStatement
|
||||||
{
|
{
|
||||||
$stmt = $this->connection->prepare($query);
|
$stmt = $this->getConnection()->prepare($query);
|
||||||
|
|
||||||
return new Statement($stmt);
|
return new Statement($stmt);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function lastId(): int
|
public function lastId(): int
|
||||||
{
|
{
|
||||||
return $this->connection->insert_id;
|
return $this->getConnection()->insert_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAffectedRows(): int
|
public function getAffectedRows(): int
|
||||||
{
|
{
|
||||||
return $this->connection->affected_rows;
|
return $this->getConnection()->affected_rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getConnection(): mysqli
|
||||||
|
{
|
||||||
|
if ($this->connection === null) {
|
||||||
|
$this->createConnection();
|
||||||
|
}
|
||||||
|
return $this->connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createConnection(): void
|
||||||
|
{
|
||||||
|
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
|
||||||
|
$this->connection = new mysqli($this->host, $this->user, $this->password, $this->db, $this->port, $this->socket);
|
||||||
|
$this->connection->set_charset('utf8mb4');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,28 +97,28 @@ class Select
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function where($column, string $relation = null, $value = null): Select
|
public function where($column, ?string $relation = null, $value = null): Select
|
||||||
{
|
{
|
||||||
$this->addWhereCondition('AND', $column, $relation, $value);
|
$this->addWhereCondition('AND', $column, $relation, $value);
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function orWhere($column, string $relation = null, $value = null): Select
|
public function orWhere($column, ?string $relation = null, $value = null): Select
|
||||||
{
|
{
|
||||||
$this->addWhereCondition('OR', $column, $relation, $value);
|
$this->addWhereCondition('OR', $column, $relation, $value);
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function having($column, string $relation = null, $value = null): Select
|
public function having($column, ?string $relation = null, $value = null): Select
|
||||||
{
|
{
|
||||||
$this->addHavingCondition('AND', $column, $relation, $value);
|
$this->addHavingCondition('AND', $column, $relation, $value);
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function orHaving($column, string $relation = null, $value = null): Select
|
public function orHaving($column, ?string $relation = null, $value = null): Select
|
||||||
{
|
{
|
||||||
$this->addHavingCondition('OR', $column, $relation, $value);
|
$this->addHavingCondition('OR', $column, $relation, $value);
|
||||||
|
|
||||||
@@ -211,12 +211,12 @@ class Select
|
|||||||
$this->joins[] = [$type, $table, $column1, $relation, $column2];
|
$this->joins[] = [$type, $table, $column1, $relation, $column2];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function addWhereCondition(string $logic, $column, string $relation, $value): void
|
private function addWhereCondition(string $logic, $column, ?string $relation, $value): void
|
||||||
{
|
{
|
||||||
$this->conditions[self::CONDITION_WHERE][] = [$logic, $column, $relation, $value];
|
$this->conditions[self::CONDITION_WHERE][] = [$logic, $column, $relation, $value];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function addHavingCondition(string $logic, $column, string $relation, $value): void
|
private function addHavingCondition(string $logic, $column, ?string $relation, $value): void
|
||||||
{
|
{
|
||||||
$this->conditions[self::CONDITION_HAVING][] = [$logic, $column, $relation, $value];
|
$this->conditions[self::CONDITION_HAVING][] = [$logic, $column, $relation, $value];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ interface IPersistentDataManager
|
|||||||
|
|
||||||
public function loadRelationsFromDb(Model $model, bool $recursive = false, array $withRelations = []): void;
|
public function loadRelationsFromDb(Model $model, bool $recursive = false, array $withRelations = []): void;
|
||||||
|
|
||||||
|
public function loadMultiRelationsFromDb(array $models, string $relation, bool $useRelations = false, array $withRelations = []): void;
|
||||||
|
|
||||||
public function saveToDb(Model $model): void;
|
public function saveToDb(Model $model): void;
|
||||||
|
|
||||||
public function deleteFromDb(Model $model): void;
|
public function deleteFromDb(Model $model): void;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ abstract class Model
|
|||||||
|
|
||||||
protected static array $relations = [];
|
protected static array $relations = [];
|
||||||
|
|
||||||
|
protected static array $multiRelations = [];
|
||||||
|
|
||||||
protected $id = null;
|
protected $id = null;
|
||||||
|
|
||||||
private array $snapshot = [];
|
private array $snapshot = [];
|
||||||
@@ -27,6 +29,11 @@ abstract class Model
|
|||||||
return static::$relations;
|
return static::$relations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function getMultiRelations(): array
|
||||||
|
{
|
||||||
|
return static::$multiRelations;
|
||||||
|
}
|
||||||
|
|
||||||
public function setId($id): void
|
public function setId($id): void
|
||||||
{
|
{
|
||||||
$this->id = $id;
|
$this->id = $id;
|
||||||
|
|||||||
@@ -92,6 +92,40 @@ class PersistentDataManager implements IPersistentDataManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function loadMultiRelationsFromDb(array $models, string $relation, bool $useRelations = false, array $withRelations = []): void
|
||||||
|
{
|
||||||
|
if (count($models) === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parentModelType = get_class($models[0]);
|
||||||
|
$relationModelsSetter = 'set' . str_replace('_', '', ucwords($relation, '_'));
|
||||||
|
[$relationModelType, $reverseRelation] = call_user_func([$parentModelType, 'getMultiRelations'])[$relation];
|
||||||
|
$reverseRelationIdGetter = 'get' . str_replace('_', '', ucwords($reverseRelation, '_')) . 'Id';
|
||||||
|
|
||||||
|
$parentModelsById = [];
|
||||||
|
foreach ($models as $model) {
|
||||||
|
$parentModelsById[$model->getId()] = $model;
|
||||||
|
}
|
||||||
|
|
||||||
|
$select = new Select($this->dbConnection);
|
||||||
|
$select->where($reverseRelation . '_id', 'IN', array_keys($parentModelsById));
|
||||||
|
|
||||||
|
$relationsByParentModelId = [];
|
||||||
|
foreach ($this->selectMultipleFromDb($select, $relationModelType, $useRelations, $withRelations) as $relationModel) {
|
||||||
|
$reverseRelationId = $relationModel->$reverseRelationIdGetter();
|
||||||
|
if (!isset($relationsByParentModelId[$reverseRelationId])) {
|
||||||
|
$relationsByParentModelId[$reverseRelationId] = [];
|
||||||
|
}
|
||||||
|
$relationsByParentModelId[$reverseRelationId][] = $relationModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($parentModelsById as $parentModelId => $parentModel) {
|
||||||
|
$relationModels = $relationsByParentModelId[$parentModelId] ?? [];
|
||||||
|
$parentModel->$relationModelsSetter($relationModels);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function saveToDb(Model $model): void
|
public function saveToDb(Model $model): void
|
||||||
{
|
{
|
||||||
$this->syncRelations($model);
|
$this->syncRelations($model);
|
||||||
|
|||||||
@@ -55,6 +55,11 @@ class HttpResponse
|
|||||||
|
|
||||||
public function render(): void
|
public function render(): void
|
||||||
{
|
{
|
||||||
|
$this->handleCors();
|
||||||
|
if ($this->method === 'options') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$match = $this->routeCollection->match($this->method, $this->parsedUrl['path']);
|
$match = $this->routeCollection->match($this->method, $this->parsedUrl['path']);
|
||||||
if ($match === null) {
|
if ($match === null) {
|
||||||
$this->render404();
|
$this->render404();
|
||||||
@@ -94,6 +99,7 @@ class HttpResponse
|
|||||||
$response = call_user_func([$controller, $handler[1]]);
|
$response = call_user_func([$controller, $handler[1]]);
|
||||||
} catch (Exception $exception) {
|
} catch (Exception $exception) {
|
||||||
$this->dbConnection->rollback();
|
$this->dbConnection->rollback();
|
||||||
|
error_log($exception);
|
||||||
$this->render500($exception);
|
$this->render500($exception);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -109,6 +115,56 @@ class HttpResponse
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function handleCors(): void
|
||||||
|
{
|
||||||
|
$origin = $this->request->header('Origin');
|
||||||
|
if (!$origin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($this->appConfig['cors']['allow_origins'])) {
|
||||||
|
if (in_array($origin, $this->appConfig['cors']['allow_origins']) || in_array('*', $this->appConfig['cors']['allow_origins'])) {
|
||||||
|
header("Access-Control-Allow-Origin: {$origin}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($this->appConfig['cors']['allow_credentials'])) {
|
||||||
|
header('Access-Control-Allow-Credentials: true');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->method !== 'options') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($this->appConfig['cors']['allow_headers'])) {
|
||||||
|
$headers = explode(',', $this->request->header('Access-Control-Request-Headers'));
|
||||||
|
if (in_array('*', $this->appConfig['cors']['allow_headers'])) {
|
||||||
|
$allow_headers = $headers;
|
||||||
|
} else {
|
||||||
|
$allow_headers = array_intersect($this->appConfig['cors']['allow_headers'], $headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($allow_headers) > 0) {
|
||||||
|
header('Access-Control-Allow-Headers: ' . join(', ', $allow_headers));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($this->appConfig['cors']['allow_methods'])) {
|
||||||
|
if (in_array('*', $this->appConfig['cors']['allow_methods'])) {
|
||||||
|
$allow_methods = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT'];
|
||||||
|
} else {
|
||||||
|
$allow_methods = $this->appConfig['cors']['allow_methods'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($allow_methods) > 0) {
|
||||||
|
header('Access-Control-Allow-Methods: ' . join(', ', $allow_methods));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$max_age = $this->appConfig['cors']['max_age'] ?? 600;
|
||||||
|
header("Access-Control-Max-Age: {$max_age}");
|
||||||
|
}
|
||||||
|
|
||||||
private function redirectToLogin(): void
|
private function redirectToLogin(): void
|
||||||
{
|
{
|
||||||
$this->request->session()->set('redirect_after_login', $this->rawUrl);
|
$this->request->session()->set('redirect_after_login', $this->rawUrl);
|
||||||
|
|||||||
@@ -86,12 +86,12 @@ class DatabaseSessionHandler implements ISessionHandler
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function gc($maxlifetime): bool
|
public function gc($maxlifetime): int|false
|
||||||
{
|
{
|
||||||
// empty on purpose
|
// empty on purpose
|
||||||
// old sessions are deleted by MaintainDatabaseCommand
|
// old sessions are deleted by MaintainDatabaseCommand
|
||||||
|
|
||||||
return true;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create_sid(): string
|
public function create_sid(): string
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ final class GoogleOAuthTest extends TestCase
|
|||||||
$redirectUrl = 'http://example.com/oauth';
|
$redirectUrl = 'http://example.com/oauth';
|
||||||
|
|
||||||
$requestMock = $this->getMockBuilder(IRequest::class)
|
$requestMock = $this->getMockBuilder(IRequest::class)
|
||||||
->setMethods(['setUrl', 'setMethod', 'setQuery', 'setHeaders', 'send'])
|
->onlyMethods(['setUrl', 'setMethod', 'setQuery', 'setHeaders', 'send'])
|
||||||
->getMock();
|
->getMock();
|
||||||
$googleOAuth = new GoogleOAuth($requestMock);
|
$googleOAuth = new GoogleOAuth($requestMock);
|
||||||
|
|
||||||
@@ -48,10 +48,10 @@ final class GoogleOAuthTest extends TestCase
|
|||||||
$redirectUrl = 'http://example.com/oauth';
|
$redirectUrl = 'http://example.com/oauth';
|
||||||
|
|
||||||
$requestMock = $this->getMockBuilder(IRequest::class)
|
$requestMock = $this->getMockBuilder(IRequest::class)
|
||||||
->setMethods(['setUrl', 'setMethod', 'setQuery', 'setHeaders', 'send'])
|
->onlyMethods(['setUrl', 'setMethod', 'setQuery', 'setHeaders', 'send'])
|
||||||
->getMock();
|
->getMock();
|
||||||
$responseMock = $this->getMockBuilder(IResponse::class)
|
$responseMock = $this->getMockBuilder(IResponse::class)
|
||||||
->setMethods(['getBody', 'getHeaders'])
|
->onlyMethods(['getBody', 'getHeaders'])
|
||||||
->getMock();
|
->getMock();
|
||||||
$googleOAuth = new GoogleOAuth($requestMock);
|
$googleOAuth = new GoogleOAuth($requestMock);
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user