From 086e2d2668784469ec114f6e6fd2b3dace3d7c3b Mon Sep 17 00:00:00 2001
From: Jonas Kohl
Date: Thu, 12 Sep 2024 19:49:17 +0200
Subject: Way more progress on forum
---
.env.example | 3 +
.gitignore | 1 +
compose.yml | 4 +-
src/application/mystic/forum/Database.php | 194 +-
src/application/mystic/forum/Messaging.php | 6 +-
.../mystic/forum/attributes/DefaultValue.php | 10 +
.../mystic/forum/attributes/References.php | 14 +-
src/application/mystic/forum/attributes/Unique.php | 6 +
src/application/mystic/forum/orm/Attachment.php | 18 +
src/application/mystic/forum/orm/Post.php | 6 +-
src/application/mystic/forum/orm/Topic.php | 5 +-
src/application/mystic/forum/orm/User.php | 16 +-
.../mystic/forum/orm/UserPermissions.php | 59 +
.../mystic/forum/utils/RequestUtils.php | 77 +-
.../mystic/forum/utils/ValidationUtils.php | 22 +
src/application/views/alert_error.php | 3 +
src/application/views/form_addpost.php | 27 +
src/application/views/form_delete_post_confirm.php | 25 +
src/application/views/form_login.php | 40 +
src/application/views/form_newtopic.php | 24 +
src/application/views/form_register.php | 55 +
src/application/views/nav_guest.php | 6 +
src/application/views/nav_logged_in.php | 14 +
src/application/views/template_end.php | 22 +
src/application/views/template_navigation.php | 6 +
src/application/views/template_navigation_end.php | 4 +
.../views/template_navigation_start.php | 12 +
src/application/views/template_start.php | 22 +
src/application/views/view_post.php | 86 +
src/application/views/view_topic_end.php | 0
src/application/views/view_topic_start.php | 55 +
src/application/views/view_topics.php | 19 +
src/application/views/view_user.php | 1 +
src/index.php | 509 +-
src/ui/dist/css/bootstrap-theme.css | 587 ++
src/ui/dist/css/bootstrap-theme.css.map | 1 +
src/ui/dist/css/bootstrap-theme.min.css | 6 +
src/ui/dist/css/bootstrap-theme.min.css.map | 1 +
src/ui/dist/css/bootstrap.css | 6834 ++++++++++++++++++++
src/ui/dist/css/bootstrap.css.map | 1 +
src/ui/dist/css/bootstrap.min.css | 6 +
src/ui/dist/css/bootstrap.min.css.map | 1 +
src/ui/dist/fonts/glyphicons-halflings-regular.eot | Bin 0 -> 20127 bytes
src/ui/dist/fonts/glyphicons-halflings-regular.svg | 288 +
src/ui/dist/fonts/glyphicons-halflings-regular.ttf | Bin 0 -> 45404 bytes
.../dist/fonts/glyphicons-halflings-regular.woff | Bin 0 -> 23424 bytes
.../dist/fonts/glyphicons-halflings-regular.woff2 | Bin 0 -> 18028 bytes
src/ui/dist/js/bootstrap.js | 2580 ++++++++
src/ui/dist/js/bootstrap.min.js | 6 +
src/ui/dist/js/npm.js | 13 +
src/ui/html5shiv.min.js | 4 +
src/ui/jquery-1.12.4.min.js | 5 +
src/ui/modernizr-2.6.2.min.js | 4 +
src/ui/placeholder.svg | 6 +
src/ui/respond.min.js | 6 +
src/ui/site.css | 37 +
56 files changed, 11697 insertions(+), 60 deletions(-)
create mode 100644 .env.example
create mode 100644 src/application/mystic/forum/attributes/DefaultValue.php
create mode 100644 src/application/mystic/forum/attributes/Unique.php
create mode 100644 src/application/mystic/forum/orm/Attachment.php
create mode 100644 src/application/mystic/forum/orm/UserPermissions.php
create mode 100644 src/application/mystic/forum/utils/ValidationUtils.php
create mode 100644 src/application/views/alert_error.php
create mode 100644 src/application/views/form_addpost.php
create mode 100644 src/application/views/form_delete_post_confirm.php
create mode 100644 src/application/views/form_login.php
create mode 100644 src/application/views/form_newtopic.php
create mode 100644 src/application/views/form_register.php
create mode 100644 src/application/views/nav_guest.php
create mode 100644 src/application/views/nav_logged_in.php
create mode 100644 src/application/views/template_end.php
create mode 100644 src/application/views/template_navigation.php
create mode 100644 src/application/views/template_navigation_end.php
create mode 100644 src/application/views/template_navigation_start.php
create mode 100644 src/application/views/template_start.php
create mode 100644 src/application/views/view_post.php
create mode 100644 src/application/views/view_topic_end.php
create mode 100644 src/application/views/view_topic_start.php
create mode 100644 src/application/views/view_topics.php
create mode 100644 src/application/views/view_user.php
create mode 100644 src/ui/dist/css/bootstrap-theme.css
create mode 100644 src/ui/dist/css/bootstrap-theme.css.map
create mode 100644 src/ui/dist/css/bootstrap-theme.min.css
create mode 100644 src/ui/dist/css/bootstrap-theme.min.css.map
create mode 100644 src/ui/dist/css/bootstrap.css
create mode 100644 src/ui/dist/css/bootstrap.css.map
create mode 100644 src/ui/dist/css/bootstrap.min.css
create mode 100644 src/ui/dist/css/bootstrap.min.css.map
create mode 100644 src/ui/dist/fonts/glyphicons-halflings-regular.eot
create mode 100644 src/ui/dist/fonts/glyphicons-halflings-regular.svg
create mode 100644 src/ui/dist/fonts/glyphicons-halflings-regular.ttf
create mode 100644 src/ui/dist/fonts/glyphicons-halflings-regular.woff
create mode 100644 src/ui/dist/fonts/glyphicons-halflings-regular.woff2
create mode 100644 src/ui/dist/js/bootstrap.js
create mode 100644 src/ui/dist/js/bootstrap.min.js
create mode 100644 src/ui/dist/js/npm.js
create mode 100644 src/ui/html5shiv.min.js
create mode 100644 src/ui/jquery-1.12.4.min.js
create mode 100644 src/ui/modernizr-2.6.2.min.js
create mode 100644 src/ui/placeholder.svg
create mode 100644 src/ui/respond.min.js
create mode 100644 src/ui/site.css
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..4b62cb4
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,3 @@
+POSTGRES_USER=postgres
+POSTGRES_DB=postgres
+POSTGRES_PASSWORD=postgres
diff --git a/.gitignore b/.gitignore
index a5a14c3..5b26652 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@
.DS_Store
*.DS_Store
vendor/
+/.env
diff --git a/compose.yml b/compose.yml
index eb4f8c7..ebaab06 100644
--- a/compose.yml
+++ b/compose.yml
@@ -2,6 +2,7 @@ services:
app:
build: .
restart: unless-stopped
+ env_file: ./.env
depends_on:
- db
ports:
@@ -11,9 +12,8 @@ services:
db:
image: postgres
restart: unless-stopped
+ env_file: ./.env
ports:
- 5432:5432
- environment:
- POSTGRES_PASSWORD: "postgres"
volumes:
- ./data/db:/var/lib/postgresql/data
diff --git a/src/application/mystic/forum/Database.php b/src/application/mystic/forum/Database.php
index 9b9cf55..7c9ac7a 100644
--- a/src/application/mystic/forum/Database.php
+++ b/src/application/mystic/forum/Database.php
@@ -6,10 +6,12 @@ namespace mystic\forum;
use DateTime;
use DateTimeImmutable;
use mystic\forum\attributes\Column;
+use mystic\forum\attributes\DefaultValue;
use mystic\forum\attributes\NotNull;
use mystic\forum\attributes\PrimaryKey;
use mystic\forum\attributes\References;
use mystic\forum\attributes\Table;
+use mystic\forum\attributes\Unique;
use mystic\forum\exceptions\DatabaseConnectionException;
use mystic\forum\orm\Entity;
use mystic\forum\utils\ArrayUtils;
@@ -25,6 +27,25 @@ class Database {
private const PRIMARY_KEY = 0b0000_0001;
private const NOT_NULL = 0b0000_0010;
private const REFERENCES = 0b0000_0100;
+ private const UNIQUE = 0b0000_1000;
+ private const DEFAULT = 0b0001_0000;
+
+ protected static function encodeBinary(string $bin): string {
+ return "\\x" . bin2hex($bin);
+ }
+
+ protected static function decodeBinary(string $enc): string {
+ return hex2bin(substr($enc, 2));
+ }
+
+ public static function generateId(int $length = 64): string {
+ static $charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
+ static $charsetLength = strlen($charset);
+ $buf = "";
+ for ($i = 0; $i < $length; ++$i)
+ $buf .= $charset[random_int(0, $charsetLength - 1)];
+ return $buf;
+ }
public function __construct(string $connectionString) {
try {
@@ -136,6 +157,14 @@ class Database {
$colType = self::getColumnType($rflProp);
$statement = "$colName $colType";
+ if (($colInfo["flags"] & self::DEFAULT) !== 0) {
+ $statement .= " DEFAULT " . $colInfo["defaultValue"];
+ }
+
+ if (($colInfo["flags"] & self::UNIQUE) !== 0) {
+ $statement .= " UNIQUE";
+ }
+
if (($colInfo["flags"] & self::NOT_NULL) !== 0) {
$statement .= " NOT NULL";
}
@@ -159,15 +188,21 @@ class Database {
$flags = 0;
$attrs = $prop->getAttributes();
$reference = "";
+ $defaultValue = null;
foreach ($attrs as $attr) {
$attrName = $attr->getName();
if ($attrName === PrimaryKey::class) {
$flags |= self::PRIMARY_KEY;
} elseif ($attrName === NotNull::class) {
$flags |= self::NOT_NULL;
+ } elseif ($attrName === Unique::class) {
+ $flags |= self::UNIQUE;
} elseif ($attrName === References::class) {
$flags |= self::REFERENCES;
$reference = $attr->newInstance()->__toString();
+ } elseif ($attrName === DefaultValue::class) {
+ $flags |= self::DEFAULT;
+ $defaultValue = $attr->newInstance()->defaultValue;
}
}
@@ -175,6 +210,8 @@ class Database {
"propertyName" => $prop->getName(),
"flags" => $flags,
"reference" => $reference,
+ "defaultValue" => $defaultValue,
+ "columnType" => self::getColumnType($prop),
]];
}, $rflEntity->getProperties(ReflectionProperty::IS_PUBLIC)));
}
@@ -186,8 +223,10 @@ class Database {
return null;
}
- private static function stringifyValue(mixed $value): ?string {
- if (is_null($value))
+ private static function stringifyValue(mixed $value, string $columnType): ?string {
+ if ($columnType === "bytea" && is_string($value))
+ return self::encodeBinary($value);
+ elseif (is_null($value))
return null;
elseif (is_bool($value))
return $value ? "true" : "false";
@@ -201,6 +240,7 @@ class Database {
private static function assignValue(Entity &$entity, array $colProps, ?string $value): void {
$propName = $colProps["propertyName"];
+ $colType = $colProps["columnType"];
$prop = new \ReflectionProperty($entity, $propName);
$type = $prop->getType();
@@ -210,38 +250,43 @@ class Database {
$typeName = $type->getName();
if (!$type->isBuiltin() && $typeName !== \DateTime::class && $typeName !== \DateTimeImmutable::class)
throw new \RuntimeException("User-defined types cannot be converted to from a database type.");
- switch ($typeName) {
- case \DateTime::class:
- $typedValue = new \DateTime($value);
- break;
- case \DateTimeImmutable::class:
- $typedValue = new \DateTimeImmutable($value);
- break;
- case "true":
- case "false":
- case "bool":
- $typedValue = in_array(strtolower($value), ["true","yes","on","1"]) ? true : (in_array(strtolower($value), ["false","no","off","0"]) ? false : null);
- break;
- case "float":
- $typedValue = floatval($value);
- break;
- case "int":
- $typedValue = intval($value);
- break;
- case "string":
- case "null":
- $typedValue = strval($value);
- break;
- case "object":
- case "array":
- case "iterable":
- case "callable":
- case "mixed":
- case "never":
- case "void":
- default:
- throw new \RuntimeException("The type \"$typeName\" cannot be restored from the database");
- }
+ if ($value !== null)
+ switch ($typeName) {
+ case \DateTime::class:
+ $typedValue = new \DateTime($value);
+ break;
+ case \DateTimeImmutable::class:
+ $typedValue = new \DateTimeImmutable($value);
+ break;
+ case "true":
+ case "false":
+ case "bool":
+ $typedValue = in_array(strtolower($value), ["true","t","yes","on","1"]) ? true : (in_array(strtolower($value), ["false","f","no","off","0"]) ? false : null);
+ break;
+ case "float":
+ $typedValue = floatval($value);
+ break;
+ case "int":
+ $typedValue = intval($value);
+ break;
+ case "string":
+ case "null":
+ if ($colType === "bytea") {
+ $typedValue = self::decodeBinary(strval($value));
+ } else {
+ $typedValue = strval($value);
+ }
+ break;
+ case "object":
+ case "array":
+ case "iterable":
+ case "callable":
+ case "mixed":
+ case "never":
+ case "void":
+ default:
+ throw new \RuntimeException("The type \"$typeName\" cannot be restored from the database");
+ }
$entity->{$propName} = $typedValue;
}
@@ -249,7 +294,7 @@ class Database {
$values = [];
foreach ($cols as $colInfo) {
- $values []= self::stringifyValue($entity->{$colInfo["propertyName"]} ?? null);
+ $values []= self::stringifyValue($entity->{$colInfo["propertyName"]} ?? null, $colInfo["columnType"]);
}
return $values;
@@ -288,6 +333,67 @@ class Database {
return true;
}
+ public function fetchWhere(Entity &$entity, string $columnName): bool {
+ $entityClassName = get_class($entity);
+ $tableName = self::getTableName($entityClassName);
+ $reflClass = new ReflectionClass($entityClassName);
+ $cols = self::getColumns($reflClass);
+ if (!isset($cols[$columnName]))
+ throw new \RuntimeException("Column $columnName does not exist!");
+ $query = "SELECT * FROM $tableName WHERE $columnName = \$1 LIMIT 1;";
+ $result = \pg_query_params($this->connection, $query, [ $entity->{$cols[$columnName]["propertyName"]} ]);
+ if ($result === false)
+ throw new \RuntimeException("Fetch failed: " . \pg_last_error($this->connection));
+ $row = \pg_fetch_assoc($result);
+ \pg_free_result($result);
+ if ($row === false)
+ return false;
+ foreach ($cols as $colName => $colProps)
+ self::assignValue($entity, $colProps, $row[$colName]);
+ return true;
+ }
+
+ public function fetchAll(string $entityClassName): array {
+ $tableName = self::getTableName($entityClassName);
+ $reflClass = new ReflectionClass($entityClassName);
+ $cols = self::getColumns($reflClass);
+ $query = "SELECT * FROM $tableName;";
+ $result = \pg_query($this->connection, $query);
+ if ($result === false)
+ throw new \RuntimeException("Fetch failed: " . \pg_last_error($this->connection));
+ $items = [];
+ while (($row = \pg_fetch_assoc($result)) !== false) {
+ $entity = new $entityClassName();
+ foreach ($cols as $colName => $colProps)
+ self::assignValue($entity, $colProps, $row[$colName]);
+ $items []= $entity;
+ }
+ \pg_free_result($result);
+ return $items;
+ }
+
+ public function fetchCustom(string $entityClassName, string $customQuery, ?array $customQueryParams = null): array {
+ $tableName = self::getTableName($entityClassName);
+ $reflClass = new ReflectionClass($entityClassName);
+ $cols = self::getColumns($reflClass);
+ $query = "SELECT * FROM $tableName $customQuery;";
+ if ($customQueryParams === null)
+ $result = \pg_query($this->connection, $query);
+ else
+ $result = \pg_query_params($this->connection, $query, $customQueryParams);
+ if ($result === false)
+ throw new \RuntimeException("Fetch failed: " . \pg_last_error($this->connection));
+ $items = [];
+ while (($row = \pg_fetch_assoc($result)) !== false) {
+ $entity = new $entityClassName();
+ foreach ($cols as $colName => $colProps)
+ self::assignValue($entity, $colProps, $row[$colName]);
+ $items []= $entity;
+ }
+ \pg_free_result($result);
+ return $items;
+ }
+
public function delete(Entity &$entity): bool {
$entityClassName = get_class($entity);
$tableName = self::getTableName($entityClassName);
@@ -313,14 +419,20 @@ class Database {
if ($primaryCol === null)
throw new \RuntimeException("Updating an entity requires a primary key column to be specified");
- $filteredCols = array_filter(ArrayUtils::asPairs($cols), function($pair) {
+ $filteredCols = array_values(array_filter(ArrayUtils::asPairs($cols), function($pair) {
return ($pair[1]["flags"] & self::PRIMARY_KEY) === 0;
- });
- $colDef = implode(", ", array_map(function(array $pair): string {
- return implode("=", [ $pair[0], '$' . $pair[1] ]);
- }, $filteredCols));
+ }));
+ // echo "
";
+ // var_dump($filteredCols);
+ // exit;
+ $qCols = [];
+ for ($i = 0; $i < count($filteredCols); ++$i) {
+ $qCols []= implode("=", [ $filteredCols[$i][0], '$' . ($i + 2) ]);
+ }
+ $colDef = implode(", ", $qCols);
$query = "UPDATE $tableName SET $colDef WHERE $primaryCol = \$1;";
- $result = \pg_query_params($this->connection, $query, array_map(fn($c) => $entity->{$c["propertyName"]}, $filteredCols));
+ //$theCols = ArrayUtils::assocFromPairs($filteredCols);
+ $result = \pg_query_params($this->connection, $query, self::getColumnValues($entity, $cols));
if ($result === false)
throw new \RuntimeException("Update failed: " . \pg_last_error($this->connection));
$num_affected_rows = \pg_affected_rows($result);
diff --git a/src/application/mystic/forum/Messaging.php b/src/application/mystic/forum/Messaging.php
index 2f5c80d..3530d5e 100644
--- a/src/application/mystic/forum/Messaging.php
+++ b/src/application/mystic/forum/Messaging.php
@@ -11,7 +11,7 @@ final class Messaging {
const ENT_FLAGS = \ENT_COMPAT | \ENT_HTML401 | \ENT_SUBSTITUTE;
protected static function message(array $items, string $headerText, string $headerColor, string $headerTextColor): void {
- echo "\n";
+ //echo "\n";
echo " \n";
echo "\n";
@@ -20,7 +20,7 @@ final class Messaging {
echo " \n";
- echo "\n";
+ echo "\n";
foreach ($items as $item) {
if (is_scalar($item)) {
@@ -65,7 +65,7 @@ final class Messaging {
echo "
\n";
echo "
\n";
- echo "\n";
+ //echo "\n";
}
public static function bold(string $contents): array {
diff --git a/src/application/mystic/forum/attributes/DefaultValue.php b/src/application/mystic/forum/attributes/DefaultValue.php
new file mode 100644
index 0000000..e3dd1a5
--- /dev/null
+++ b/src/application/mystic/forum/attributes/DefaultValue.php
@@ -0,0 +1,10 @@
+foreignTableName
. ($this->foreignColumnName !== null ? " ({$this->foreignColumnName})" : "")
- . ($this->cascadeOnDelete ? " ON DELETE CASCADE" : "");
+ . ([
+ self::NO_ACTION => "",
+ self::CASCADE => " ON DELETE CASCADE",
+ self::SET_NULL => " ON DELETE SET NULL",
+ self::SET_DEFAULT => " ON DELETE SET DEFAULT",
+ ][$this->onDelete] ?? "");
}
}
diff --git a/src/application/mystic/forum/attributes/Unique.php b/src/application/mystic/forum/attributes/Unique.php
new file mode 100644
index 0000000..7c32255
--- /dev/null
+++ b/src/application/mystic/forum/attributes/Unique.php
@@ -0,0 +1,6 @@
+id === self::SUPERUSER_ID)
+ return true;
+ return ($this->permissionMask & $perm) === $perm;
+ }
}
diff --git a/src/application/mystic/forum/orm/UserPermissions.php b/src/application/mystic/forum/orm/UserPermissions.php
new file mode 100644
index 0000000..cd2fdf4
--- /dev/null
+++ b/src/application/mystic/forum/orm/UserPermissions.php
@@ -0,0 +1,59 @@
+id = $userId;
+ if (!$db->fetch($user))
+ return null;
+ return $user;
+ }
+
+ public static function setAuthorizedUser(User &$user): void {
+ $_SESSION["authedUser"] = $user->id;
+ }
+
+ public static function unsetAuthorizedUser(): void {
+ unset($_SESSION["authedUser"]);
}
}
diff --git a/src/application/mystic/forum/utils/ValidationUtils.php b/src/application/mystic/forum/utils/ValidationUtils.php
new file mode 100644
index 0000000..df97914
--- /dev/null
+++ b/src/application/mystic/forum/utils/ValidationUtils.php
@@ -0,0 +1,22 @@
+name = $name;
+ return !$db->fetchWhere($user, "name");
+ }
+}
+
diff --git a/src/application/views/alert_error.php b/src/application/views/alert_error.php
new file mode 100644
index 0000000..c18546b
--- /dev/null
+++ b/src/application/views/alert_error.php
@@ -0,0 +1,3 @@
+
+= htmlentities($message); ?>
+
diff --git a/src/application/views/form_addpost.php b/src/application/views/form_addpost.php
new file mode 100644
index 0000000..b3cd6ca
--- /dev/null
+++ b/src/application/views/form_addpost.php
@@ -0,0 +1,27 @@
+
+
+ $_formError]);
+}
+?>
+
diff --git a/src/application/views/form_delete_post_confirm.php b/src/application/views/form_delete_post_confirm.php
new file mode 100644
index 0000000..9d04095
--- /dev/null
+++ b/src/application/views/form_delete_post_confirm.php
@@ -0,0 +1,25 @@
+
+
+
Do you want to delete your post?
+
+
+ Are you sure you want to delete the following post:
+
+ = renderPost($post->content); ?>
+
+
+
+
diff --git a/src/application/views/form_login.php b/src/application/views/form_login.php
new file mode 100644
index 0000000..0e98a24
--- /dev/null
+++ b/src/application/views/form_login.php
@@ -0,0 +1,40 @@
+
+Log in
+
+
+
diff --git a/src/application/views/form_newtopic.php b/src/application/views/form_newtopic.php
new file mode 100644
index 0000000..d5cbfbd
--- /dev/null
+++ b/src/application/views/form_newtopic.php
@@ -0,0 +1,24 @@
+
+" method="post">
+Topic title:
+ " required>
+
+Message:
+
+
+Create topic
+
diff --git a/src/application/views/form_register.php b/src/application/views/form_register.php
new file mode 100644
index 0000000..221f37d
--- /dev/null
+++ b/src/application/views/form_register.php
@@ -0,0 +1,55 @@
+
+Register
+
+
+
diff --git a/src/application/views/nav_guest.php b/src/application/views/nav_guest.php
new file mode 100644
index 0000000..433c487
--- /dev/null
+++ b/src/application/views/nav_guest.php
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/src/application/views/nav_logged_in.php b/src/application/views/nav_logged_in.php
new file mode 100644
index 0000000..fd46d6e
--- /dev/null
+++ b/src/application/views/nav_logged_in.php
@@ -0,0 +1,14 @@
+
+
diff --git a/src/application/views/template_end.php b/src/application/views/template_end.php
new file mode 100644
index 0000000..f7f70a9
--- /dev/null
+++ b/src/application/views/template_end.php
@@ -0,0 +1,22 @@
+
+
+
+
+
+