still some bugs and auth issues to iron out. need to implement a new action button/bar, the card for moments in a space, creating new moments and essays, dynamically refreshing the spaces view when a new post is created.

This commit is contained in:
2025-05-20 19:46:09 -06:00
parent a93c30c40f
commit 54a01f0f1f
20 changed files with 1018 additions and 1191 deletions
+5 -1
View File
@@ -11,7 +11,7 @@ if (!empty($_FILES['upload']))
*/
function randomId(int $mode)
{
$id = rand(1000, 999999999999);
$id = rand(10000000, 999999999);
switch ($mode) {
case 0:
if (User::exists($id))
@@ -19,6 +19,10 @@ function randomId(int $mode)
else
return $id;
case 1:
if (Post::exists($id))
return randomId($mode);
else
return $id;
break;
case 2:
break;
+8 -3
View File
@@ -29,10 +29,15 @@ function exec_stmt($stmt, $param_types, ...$args)
$stmt->bind_param($param_types, ...$args);
$stmt->execute();
$result = $stmt->get_result();
if ($result)
if ($result) {
$conn->close();
return $result;
else
return mysqli_insert_id($conn);
} else {
$id = mysqli_insert_id($conn);
$conn->close();
return $id;
}
}
function get_connection()
+9 -49
View File
@@ -1,18 +1,18 @@
<?php
session_start();
header("Content-Security-Policy: default-src 'self' localhost:7477; font-src 'self' https://www.nerdfonts.com/assets/fonts/Symbols-2048-em%20Nerd%20Font%20Complete.woff2; style-src 'self' 'unsafe-inline' https://nerdfonts.com/assets/css/webfont.css https://www.nerdfonts.com/assets/css/webfont.css; script-src 'self' 'unsafe-inline'; img-src 'self';");
header("Content-Security-Policy: default-src 'self' localhost:*; font-src 'self' https://www.nerdfonts.com/assets/fonts/Symbols-2048-em%20Nerd%20Font%20Complete.woff2; style-src 'self' 'unsafe-inline' https://nerdfonts.com/assets/css/webfont.css https://www.nerdfonts.com/assets/css/webfont.css; script-src 'self' 'unsafe-inline'; img-src 'self';");
// Initialize Composer.
require_once 'vendor/autoload.php';
require_once 'db.php';
require_once 'user.php';
require_once 'post.php';
require_once 'spaces.php';
// Load environment variables.
require_once 'functions/vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable('/home/jashton/dev/me/weave.space/.env');
$dotenv->safeLoad();
require_once 'functions/db.php';
require_once 'functions/core.php';
require_once 'functions/user.php';
require_once 'functions/post.php';
require_once 'functions/space.php';
/**/
// Set SESSION variable.
if (!isset($_SESSION['initialized'])) {
$_SESSION['initialized'] = true;
@@ -25,43 +25,3 @@ if (!isset($_SESSION['initialized'])) {
define('owner', 1);
define('developer', 2);
define('user', 3);
// Update the database and cookies to keep the user logged in.
if (isset($_SESSION['user_id'])) {
$conn = get_connection();
$stmt = $conn->prepare('UPDATE user SET is_active = true WHERE user_id = ?');
$stmt->bind_param('i', $_SESSION['user_id']);
$stmt->execute();
$stmt = $conn->prepare('UPDATE user SET last_active = CURRENT_TIMESTAMP WHERE user_id = ?');
$stmt->bind_param('i', $_SESSION['user_id']);
$stmt->execute();
} else if (isset($_COOKIE['remember_user'])) {
$conn = get_connection();
$stmt = $conn->prepare('SELECT * FROM remember_user WHERE token = ?');
$stmt->bind_param('s', $_COOKIE['remember_user']);
$stmt->execute();
$remember = $stmt->get_result()->fetch_assoc();
if (hash('sha256', $_SERVER['REMOTE_ADDR']) != $remember['remote_addr'] || (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && hash('sha256', $_SERVER['HTTP_X_FORWARDED_FOR']) != $remember['forwarded_for'])) {
// TODO: Log the attempt to use a cookie from a different browser/device than the cookie was created
echo "Naughty, you are trying to use someone else's cookie...";
} else {
$stmt = $conn->prepare('SELECT user_id, username, role_id FROM user WHERE user_id = ?');
$stmt->bind_param('s', $remember['user_id']);
$stmt->execute();
$user = $stmt->get_result()->fetch_assoc();
$_SESSION['user_id'] = $user['user_id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role_id'] = $user['role_id'];
$stmt = $conn->prepare('UPDATE user SET is_active = true WHERE user_id = ?');
$stmt->bind_param('i', $_SESSION['user_id']);
$stmt->execute();
$stmt = $conn->prepare('UPDATE user SET last_active = CURRENT_TIMESTAMP WHERE user_id = ?');
$stmt->bind_param('i', $_SESSION['user_id']);
$stmt->execute();
}
}
+153 -645
View File
@@ -4,9 +4,9 @@
// namespace VintageCoding\Models;
// namespace VintageCoding\Repositories;
require_once 'db.php'; // For exec_stmt() and get_connection() (though get_connection is used by exec_stmt)
require_once 'user.php'; // For User and Author classes
require_once 'space.php';
require_once 'functions/db.php'; // For exec_stmt() and get_connection() (though get_connection is used by exec_stmt)
require_once 'functions/user.php'; // For User and Author classes
require_once 'functions/space.php';
// -- Data Transfer Object for Topics --
class Topic
@@ -34,56 +34,87 @@ class Topic
// -- Post Data Models (as previously defined, with setReadCount in Post) --
class Post
{
private Space $space;
private Author $author;
private int $postTypeId;
private ?string $title;
private ?string $slug;
private int $readCount;
private ?Space $space;
private ?int $id;
private User $creator;
private int $postType;
private ?int $status;
private int $seenCount;
private ?DateTime $createdAt;
private ?DateTime $updatedAt;
private ?DateTime $status;
private bool $published;
public function __construct($postArray)
public function __construct(?Space $space = null, ?int $id = null, User $creator, int $postType, int $status)
{
$this->postId = $postArray['postId'];
$this->author = $postArray['authorId'];
$this->postTypeId = $postArray['postType'];
$this->title = $postArray['title'];
$this->slug = $postArray['slug'];
$this->readCount = 0;
$this->status = $postArray['status'];
$this->space = $space;
$this->id = $id;
$this->creator = $creator;
$this->postType = $postType;
$this->status = $status;
$this->seenCount = 0;
}
public function getPostId(): int
public static function exists(int $id): bool
{
return $this->postId;
$stmt = 'SELECT count(id) FROM post WHERE id = ?';
$result = exec_stmt($stmt, 'i', $id)->fetch_assoc();
if ($result['count(id)'] == 1)
return true;
else if ($result['count(id)'] > 1) {
// TODO: Add additional logging here, since that means multiple posts have the same ID.
return true;
}
return false;
}
public function getAuthor(): Author
public static function retrieveFromDB(int $id): ?Post
{
return $this->author;
$stmt = 'SELECT * FROM post WHERE id = ?';
$post = exec_stmt($stmt, 'i', $id)->fetch_assoc();
if ($post) {
$creator = User::retrieveFromDB($post['creator']);
switch ($post['postType']) {
case 1:
return new Essay(null, $id, $creator, 1, $post['status'], $post['title'], $post['markdown'], $post['excerpt']);
case 3:
return new Thought(null, $post['id'], $creator, 3, $post['status'], $post['thought']);
default:
return null;
}
} else
return null;
}
public function getPostTypeId(): int
public function setId(int $id): void
{
return $this->postTypeId;
$this->id = $id;
}
public function getTitle(): ?string
public function authUser(User $user): bool
{
return $this->title;
return true;
}
public function getSlug(): ?string
public function getId(): int
{
return $this->slug;
return $this->id;
}
public function getReadCount(): int
public function getCreator(): User
{
return $this->readCount;
return $this->creator;
}
public function getPostType(): int
{
return $this->postType;
}
public function getSeenCount(): int
{
return $this->seenCount;
}
public function getCreatedAt(): ?DateTime
@@ -101,54 +132,80 @@ class Post
return $this->status;
}
public function isPublished(): bool
public function incrementSeenCount(): void
{
return $this->published;
$this->seenCount++;
}
public function incrementReadCount(): void
{
$this->readCount++;
}
public function setSlug(?string $slug): void
{
$this->slug = $slug;
}
public function setStatus(?string $status): void
public function setStatus(?int $status): void
{
$this->status = $status;
}
public function setPublished(bool $published): void
public function setSpace(Space $space): void
{
$this->published = $published;
$this->space = $space;
}
public function setReadCount(int $count): void
public function getSpace(): ?Space
{
$this->readCount = $count;
return $this->space;
}
}
class Essay extends Post
{
private ?string $markdown;
private ?string $excerpt;
private ?array $files;
private string $title;
private string $html;
private string $excerpt;
/* private array $files; */
public function __construct(int $postId, Author $author, ?string $title = null, ?string $slug = null, ?string $markdown = null, ?string $excerpt = null, ?array $files = null, ?DateTime $createdAt = null, ?DateTime $updatedAt = null, ?DateTime $publishedAt = null, bool $published = true)
public function __construct(?Space $space = null, ?int $id = null, User $creator, int $postType, int $status, string $title, string $markdown, string $excerpt)
{
parent::__construct($postId, $author, 1, $title, $slug, $createdAt, $updatedAt, $publishedAt, $published);
$this->markdown = $markdown;
parent::__construct($space, $id, $creator, 1, $postType, $status);
require_once 'vendor/autoload.php';
$parsedown = new Parsedown();
$parsedown->setSafeMode(true);
$this->title = $title;
$this->excerpt = $excerpt;
$this->files = $files;
$this->html = $parsedown->text($markdown);
}
public function getMarkdown(): ?string
public function getCardHTML(): ?string
{
return $this->markdown;
/* <a href="user.php?view=display&id=' . parent::getCreator()->getId() . '">@' . parent::getCreator()->getUsername() . '</a> */
$html = '
<div class="full_width std_border padding center top_margin bottom_margin">
<a href="spaces.php?view=essay&id=' . parent::getId() . '" class="divLink">
<div class="row space_between full_width">
<div class="column std_width">
<h4>' . $this->title . '</h4>
</div>
<div class="">
<span>Something</span>
</div>
</div>
<div class="row full_width">
<p>' . $this->excerpt . '</p>
</div>
</a>
</div>
';
return $html;
}
public function getTitle(): ?string
{
return $this->title;
}
public function getHTML(): ?string
{
return $this->html;
}
public function getExcerpt(): ?string
@@ -156,25 +213,18 @@ class Essay extends Post
return $this->excerpt;
}
public function getFiles(): ?array
{
return $this->files;
}
public function setMarkdown(?string $markdown): void
{
$this->markdown = $markdown;
require_once 'vendor/autoload.php';
$parsedown = new Parsedown();
$parsedown->setSafeMode(true);
$this->html = $parsedown->text($markdown);
}
public function setExcerpt(?string $excerpt): void
{
$this->excerpt = $excerpt;
}
public function setFiles(?array $files): void
{
$this->files = $files;
}
}
class Moment extends Post
@@ -182,9 +232,9 @@ class Moment extends Post
private ?string $caption;
private ?array $images;
public function __construct(int $postId, Author $author, ?string $caption = null, ?array $images = null, ?DateTime $createdAt = null, ?DateTime $updatedAt = null, ?DateTime $publishedAt = null, bool $published = true)
public function __construct(?Space $space = null, ?int $id = null, User $creator, int $postType, int $status, ?array $images = null, ?string $caption = null)
{
parent::__construct($postId, $author, 2, null, null, $createdAt, $updatedAt, $publishedAt, $published);
parent::__construct($space, $id, $creator, $postType, $status);
$this->caption = $caption;
$this->images = $images;
}
@@ -214,10 +264,41 @@ class Thought extends Post
{
private ?string $thought;
public function __construct(int $postId, Author $author, ?string $thought = null, ?DateTime $createdAt = null, ?DateTime $updatedAt = null, ?DateTime $publishedAt = null, bool $published = true)
public function __construct(?Space $space = null, ?int $id = null, User $creator, int $postType, int $status, string $thought)
{
parent::__construct($postId, $author, 3, null, null, $createdAt, $updatedAt, $publishedAt, $published);
parent::__construct($space, $id, $creator, $postType, $status);
$this->thought = $thought;
if ($id == null) {
$stmt = 'INSERT INTO post (id, parentSpace, creator, postType, status, thought) VALUES (?, ?, ?, ?, ?, ?)';
$newID = randomId(1);
parent::setId($newID);
$space = isset($space) ? $space->getId() : $creator->getHomeSpace()->getId();
exec_stmt($stmt, 'iiiiis', $newID, $space, $creator->getId(), 3, 2, $this->thought);
$stmt = 'INSERT INTO spacePosts (post, space) VALUES (?, ?)';
exec_stmt($stmt, 'ii', $newID, $space);
}
}
public function getCardHTML(): ?string
{
$html = '
<div class="std_border padding center full_width top_margin bottom_margin">
<div class="row space_between full_width">
<a href="user.php?view=display&id=' . parent::getCreator()->getId() . '">@' . parent::getCreator()->getUsername() . '</a>
<div class="">
<span>Something</span>
</div>
</div>
<div class="line"></div>
<div class="row full_width">
<p>' . $this->thought . '</p>
</div>
</div>
';
return $html;
}
public function getThought(): ?string
@@ -230,576 +311,3 @@ class Thought extends Post
$this->thought = $thought;
}
}
// -- Repository for Topics --
class TopicRepository
{
// No $conn property needed as exec_stmt handles connection
public function __construct()
{
// Constructor is now empty
}
public function getAllTopics(): array
{
$result = exec_stmt('SELECT topic_id, topic FROM topics ORDER BY topic ASC', ''); // No params
$topics = [];
if ($result instanceof mysqli_result) {
while ($row = $result->fetch_assoc()) {
$topics[] = new Topic((int) $row['topic_id'], $row['topic']);
}
$result->free();
} else {
// exec_stmt for SELECT should return mysqli_result. If not, it's an issue with exec_stmt or query.
error_log('TopicRepository::getAllTopics expected mysqli_result, got something else.');
}
return $topics;
}
public function findById(int $topicId): ?Topic
{
$result = exec_stmt('SELECT topic_id, topic FROM topics WHERE topic_id = ?', 'i', $topicId);
if ($result instanceof mysqli_result) {
if ($row = $result->fetch_assoc()) {
$result->free();
return new Topic((int) $row['topic_id'], $row['topic']);
}
$result->free();
}
return null;
}
public function findByName(string $name): ?Topic
{
$result = exec_stmt('SELECT topic_id, topic FROM topics WHERE topic = ?', 's', $name);
if ($result instanceof mysqli_result) {
if ($row = $result->fetch_assoc()) {
$result->free();
return new Topic((int) $row['topic_id'], $row['topic']);
}
$result->free();
}
return null;
}
public function findOrCreate(string $name): Topic
{
$topic = $this->findByName($name);
if ($topic) {
return $topic;
}
// topics.topic_id is AUTO_INCREMENT, so exec_stmt should return the new ID.
$newTopicId = exec_stmt('INSERT INTO topics (topic) VALUES (?)', 's', $name);
if (is_numeric($newTopicId) && $newTopicId > 0) {
return new Topic((int) $newTopicId, $name);
}
// If $newTopicId is 0 or not numeric, insert failed or exec_stmt behavior is unexpected.
throw new \RuntimeException('Failed to create topic or retrieve its ID: ' . $name);
}
}
// -- Repository for Posts --
class PostRepository
{
private TopicRepository $topicRepository;
private string $essayContentPath;
public function __construct(TopicRepository $topicRepository, string $essayContentPath)
{
$this->topicRepository = $topicRepository;
$this->essayContentPath = rtrim($essayContentPath, '/') . '/';
}
private function getAuthorFromDb(int $authorId): ?Author
{
$result = exec_stmt('SELECT user_id, username, profile_picture, bio, website FROM user WHERE user_id = ?', 'i', $authorId);
if ($result instanceof mysqli_result) {
$authorData = $result->fetch_assoc();
$result->free();
if ($authorData) {
return new Author(
(int) $authorData['user_id'],
$authorData['username'],
$authorData['profile_picture'],
$authorData['bio'],
$authorData['website']
);
}
} else {
error_log('PostRepository::getAuthorFromDb expected mysqli_result for author ID: ' . $authorId);
}
error_log('Author not found with ID: ' . $authorId);
return null;
}
private function hydratePost(array $row): ?Post
{
$author = $this->getAuthorFromDb((int) $row['author_id']);
if (!$author)
return null;
$postId = (int) $row['post_id'];
$postTypeId = (int) $row['post_type'];
$title = $row['title'];
$slug = $row['slug'];
$readCount = (int) $row['read_count'];
$createdAt = $row['created_at'] ? new DateTime($row['created_at']) : null;
$updatedAt = $row['updated_at'] ? new DateTime($row['updated_at']) : null;
$publishedAt = $row['published_at'] ? new DateTime($row['published_at']) : null;
$published = (bool) $row['published'];
$post = null;
switch ($postTypeId) {
case 1: // Essay
$markdown = $this->getEssayMarkdown($postId);
$post = new Essay($postId, $author, $title, $slug, $markdown, $row['excerpt'], null, $createdAt, $updatedAt, $publishedAt, $published);
break;
case 2: // Moment
$post = new Moment($postId, $author, $row['excerpt'], null, $createdAt, $updatedAt, $publishedAt, $published);
break;
case 3: // Thought
$post = new Thought($postId, $author, $row['excerpt'], $createdAt, $updatedAt, $publishedAt, $published);
break;
default:
error_log('Unknown post type ID: ' . $postTypeId);
return null;
}
if ($post)
$post->setReadCount($readCount);
return $post;
}
// File operations remain unchanged as they don't use the DB connection directly
private function getEssayMarkdown(int $postId): ?string
{ /* ... same as before ... */
$filePath = $this->essayContentPath . $postId . '/article.md';
if (file_exists($filePath) && is_readable($filePath)) {
return file_get_contents($filePath);
}
return null;
}
private function saveEssayMarkdown(int $postId, string $markdownContent): bool
{ /* ... same as before ... */
$dirPath = $this->essayContentPath . $postId . '/';
if (!is_dir($dirPath)) {
if (!mkdir($dirPath, 0755, true)) {
error_log('Failed to create directory: ' . $dirPath);
return false;
}
}
$filePath = $dirPath . 'article.md';
if (file_put_contents($filePath, $markdownContent) === false) {
error_log('Failed to write markdown file: ' . $filePath);
return false;
}
return true;
}
private function deleteEssayContentDirectory(int $postId): bool
{ /* ... same as before ... */
$dirPath = $this->essayContentPath . $postId . '/';
if (!is_dir($dirPath))
return true;
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dirPath, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($files as $fileinfo) {
$todo = ($fileinfo->isDir() ? 'rmdir' : 'unlink');
if (!@$todo($fileinfo->getRealPath())) {
error_log("Failed to {$todo} {$fileinfo->getRealPath()}");
return false;
}
}
if (!@rmdir($dirPath)) {
error_log("Failed to remove main directory {$dirPath}");
return false;
}
return true;
}
public function findById(int $postId): ?Post
{
$result = exec_stmt('SELECT * FROM posts WHERE post_id = ?', 'i', $postId);
if ($result instanceof mysqli_result) {
$row = $result->fetch_assoc();
$result->free();
return $row ? $this->hydratePost($row) : null;
}
error_log('PostRepository::findById expected mysqli_result for post ID: ' . $postId);
return null;
}
public function getPostIds(string $orderBy = 'published_at', string $direction = 'DESC'): array
{
// Basic validation for orderBy and direction
$orderBy = preg_replace('/[^a-zA-Z0-9_]/', '', $orderBy); // Basic sanitization
$direction = strtoupper($direction) === 'ASC' ? 'ASC' : 'DESC';
$sql = "SELECT post_id FROM posts ORDER BY $orderBy $direction";
$result = exec_stmt($sql, '');
$ids = [];
if ($result instanceof mysqli_result) {
while ($row = $result->fetch_assoc())
$ids[] = (int) $row['post_id'];
$result->free();
} else {
error_log('Error in getPostIds: Expected mysqli_result.');
}
return $ids;
}
public function getAllPosts(string $orderBy = 'published_at', string $direction = 'DESC'): array
{
$orderBy = preg_replace('/[^a-zA-Z0-9_]/', '', $orderBy);
$direction = strtoupper($direction) === 'ASC' ? 'ASC' : 'DESC';
$sql = "SELECT * FROM posts ORDER BY $orderBy $direction";
$result = exec_stmt($sql, '');
$posts = [];
if ($result instanceof mysqli_result) {
while ($row = $result->fetch_assoc()) {
if ($postObject = $this->hydratePost($row))
$posts[] = $postObject;
}
$result->free();
} else {
error_log('Error in getAllPosts: Expected mysqli_result.');
}
return $posts;
}
public function getPostIdsByAuthor(int $authorId, string $orderBy = 'published_at', string $direction = 'DESC'): array
{
$orderBy = preg_replace('/[^a-zA-Z0-9_]/', '', $orderBy);
$direction = strtoupper($direction) === 'ASC' ? 'ASC' : 'DESC';
$sql = "SELECT post_id FROM posts WHERE author_id = ? ORDER BY $orderBy $direction";
$result = exec_stmt($sql, 'i', $authorId);
$ids = [];
if ($result instanceof mysqli_result) {
while ($row = $result->fetch_assoc())
$ids[] = (int) $row['post_id'];
$result->free();
} else {
error_log('Error in getPostIdsByAuthor: Expected mysqli_result.');
}
return $ids;
}
public function getPostsByAuthor(int $authorId, string $orderBy = 'published_at', string $direction = 'DESC'): array
{
$orderBy = preg_replace('/[^a-zA-Z0-9_.]/', '', $orderBy); // Allow dot for aliased columns
$direction = strtoupper($direction) === 'ASC' ? 'ASC' : 'DESC';
$sql = "SELECT * FROM posts WHERE author_id = ? ORDER BY $orderBy $direction";
$result = exec_stmt($sql, 'i', $authorId);
$posts = [];
if ($result instanceof mysqli_result) {
while ($row = $result->fetch_assoc()) {
if ($postObject = $this->hydratePost($row))
$posts[] = $postObject;
}
$result->free();
} else {
error_log('Error in getPostsByAuthor: Expected mysqli_result.');
}
return $posts;
}
public function getPostIdsByTopicName(string $topicName, string $orderBy = 'p.published_at', string $direction = 'DESC'): array
{
$orderBy = preg_replace('/[^a-zA-Z0-9_.]/', '', $orderBy);
$direction = strtoupper($direction) === 'ASC' ? 'ASC' : 'DESC';
$sql = "SELECT p.post_id FROM posts p
INNER JOIN post_topics pt ON p.post_id = pt.post_id
INNER JOIN topics t ON pt.topic_id = t.topic_id
WHERE t.topic = ?
ORDER BY $orderBy $direction";
$result = exec_stmt($sql, 's', $topicName);
$ids = [];
if ($result instanceof mysqli_result) {
while ($row = $result->fetch_assoc())
$ids[] = (int) $row['post_id'];
$result->free();
} else {
error_log('Error in getPostIdsByTopicName: Expected mysqli_result.');
}
return $ids;
}
public function getPostsByTopicName(string $topicName, string $orderBy = 'p.published_at', string $direction = 'DESC'): array
{
$orderBy = preg_replace('/[^a-zA-Z0-9_.]/', '', $orderBy);
$direction = strtoupper($direction) === 'ASC' ? 'ASC' : 'DESC';
$sql = "SELECT p.* FROM posts p
INNER JOIN post_topics pt ON p.post_id = pt.post_id
INNER JOIN topics t ON pt.topic_id = t.topic_id
WHERE t.topic = ?
ORDER BY $orderBy $direction";
$result = exec_stmt($sql, 's', $topicName);
$posts = [];
if ($result instanceof mysqli_result) {
while ($row = $result->fetch_assoc()) {
if ($postObject = $this->hydratePost($row))
$posts[] = $postObject;
}
$result->free();
} else {
error_log('Error in getPostsByTopicName: Expected mysqli_result.');
}
return $posts;
}
private function updatePostTopics(int $postId, array $topicNames): void
{
// Delete existing topics for the post
// exec_stmt returns 0 for successful DELETE. No direct success check here.
exec_stmt('DELETE FROM post_topics WHERE post_id = ?', 'i', $postId);
if (empty($topicNames))
return;
// Batch insert is harder with this exec_stmt. Doing one by one.
// This is inefficient but simpler with the current exec_stmt.
foreach ($topicNames as $name) {
$topicObj = $this->topicRepository->findOrCreate(trim($name));
// post_topics has no auto-increment, exec_stmt will return 0 for success.
exec_stmt('INSERT INTO post_topics (post_id, topic_id) VALUES (?, ?)', 'ii', $postId, $topicObj->getId());
}
}
public function createPost(Post $post, array $topicNames = []): ?Post
{
// WARNING: No transaction possible with the provided exec_stmt for multi-step operations.
// Each exec_stmt is its own transaction.
try {
$sql = 'INSERT INTO posts (post_id, author_id, post_type, title, slug, read_count, created_at, updated_at, published_at, published, excerpt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
$postIdVal = $post->getPostId();
$authorId = $post->getAuthor()->getUserId();
$postTypeId = $post->getPostTypeId();
$title = $post->getTitle();
$slug = $post->getSlug();
$readCount = $post->getReadCount();
$createdAtDb = $post->getCreatedAt() ? $post->getCreatedAt()->format('Y-m-d H:i:s') : (new DateTime())->format('Y-m-d H:i:s');
$updatedAtDb = $post->getUpdatedAt() ? $post->getUpdatedAt()->format('Y-m-d H:i:s') : (new DateTime())->format('Y-m-d H:i:s');
$publishedAtDb = $post->getPublishedAt() ? $post->getPublishedAt()->format('Y-m-d H:i:s') : null;
$published = (int) $post->isPublished();
$excerpt = null;
if ($post instanceof Essay)
$excerpt = $post->getExcerpt();
elseif ($post instanceof Moment)
$excerpt = $post->getCaption();
elseif ($post instanceof Thought)
$excerpt = $post->getThought();
// For INSERT on `posts` (no auto-increment PK), exec_stmt returns 0 on success.
$insertResult = exec_stmt(
$sql,
'iiississsis',
$postIdVal,
$authorId,
$postTypeId,
$title,
$slug,
$readCount,
$createdAtDb,
$updatedAtDb,
$publishedAtDb,
$published,
$excerpt
);
// We can't reliably check $insertResult for success of this specific insert.
// Assume success if no PHP error/exception was thrown by exec_stmt (though it doesn't throw).
if ($post instanceof Essay && $post->getMarkdown() !== null) {
if (!$this->saveEssayMarkdown($postIdVal, $post->getMarkdown())) {
// Rollback is not possible. Log error.
error_log('Failed to save essay markdown for post ID: ' . $postIdVal . '. DB insert was separate.');
// Potentially delete the created post record if consistency is critical, but that's complex.
}
}
$this->updatePostTopics($postIdVal, $topicNames); // Also not part of a transaction
return $this->findById($postIdVal);
} catch (\Exception $e) { // Catch any exceptions from our code, not exec_stmt
error_log("Error creating post (ID: {$post->getPostId()}): " . $e->getMessage());
return null;
}
}
public function updatePost(Post $post, array $topicNames = []): ?Post
{
// WARNING: No transaction possible.
try {
$sql = 'UPDATE posts SET author_id = ?, post_type = ?, title = ?, slug = ?,
updated_at = CURRENT_TIMESTAMP, published_at = ?, published = ?, excerpt = ?
WHERE post_id = ?';
$authorId = $post->getAuthor()->getUserId();
$postTypeId = $post->getPostTypeId();
$title = $post->getTitle();
$slug = $post->getSlug();
$publishedAtDb = $post->getPublishedAt() ? $post->getPublishedAt()->format('Y-m-d H:i:s') : null;
$published = (int) $post->isPublished();
$postIdVal = $post->getPostId();
$excerpt = null;
if ($post instanceof Essay)
$excerpt = $post->getExcerpt();
elseif ($post instanceof Moment)
$excerpt = $post->getCaption();
elseif ($post instanceof Thought)
$excerpt = $post->getThought();
// exec_stmt returns 0 for successful UPDATE.
exec_stmt(
$sql,
'iisssisi',
$authorId,
$postTypeId,
$title,
$slug,
$publishedAtDb,
$published,
$excerpt,
$postIdVal
);
if ($post instanceof Essay && $post->getMarkdown() !== null) {
if (!$this->saveEssayMarkdown($postIdVal, $post->getMarkdown())) {
error_log('Failed to save essay markdown for post ID: ' . $postIdVal . ' during update.');
}
}
$this->updatePostTopics($postIdVal, $topicNames);
return $this->findById($postIdVal);
} catch (\Exception $e) {
error_log("Error updating post (ID: {$post->getPostId()}): " . $e->getMessage());
return null;
}
}
/**
* Deletes a post.
* WARNING: Due to exec_stmt, this is not a transactional operation.
* Success of individual DB deletions is not reliably checkable.
* This method attempts the operations and logs errors.
* Consider changing return type to void as boolean success is ambiguous.
*/
public function deletePost(int $postId): void
{
try {
$postDataResult = exec_stmt('SELECT post_type FROM posts WHERE post_id = ?', 'i', $postId);
$postType = null;
if ($postDataResult instanceof mysqli_result && $row = $postDataResult->fetch_assoc()) {
$postType = (int) $row['post_type'];
$postDataResult->free();
}
exec_stmt('DELETE FROM post_topics WHERE post_id = ?', 'i', $postId);
exec_stmt('DELETE FROM posts WHERE post_id = ?', 'i', $postId);
// Cannot reliably check if the above deletes were successful or affected rows.
if ($postType === 1) { // Essay
if (!$this->deleteEssayContentDirectory($postId)) {
error_log('Failed to delete essay content directory for post ID: ' . $postId);
}
}
} catch (\Exception $e) {
error_log("Error during deletePost for ID $postId: " . $e->getMessage());
// Depending on desired behavior, re-throw or handle
}
}
/**
* Deletes posts by author.
* WARNING: Not transactional. Returns void as count of deleted posts cannot be reliably obtained from exec_stmt.
*/
public function deletePostsByAuthor(int $authorId): void
{
try {
$result = exec_stmt('SELECT post_id, post_type FROM posts WHERE author_id = ?', 'i', $authorId);
$postsToDelete = [];
if ($result instanceof mysqli_result) {
while ($row = $result->fetch_assoc())
$postsToDelete[] = $row;
$result->free();
} else {
error_log("Failed to fetch posts for deletion by author ID: $authorId");
return; // Exit if we can't get the list of posts
}
foreach ($postsToDelete as $postInfo) {
if ((int) $postInfo['post_type'] === 1) { // Essay
if (!$this->deleteEssayContentDirectory((int) $postInfo['post_id'])) {
error_log('Failed to delete essay content for post ' . $postInfo['post_id'] . ' during mass delete by author.');
}
}
// Handle Moment image deletion if any
}
// Must delete from child table `post_topics` first if no ON DELETE CASCADE, or if ensuring order.
// This is more complex with joins if exec_stmt doesn't handle it well.
// Simpler: iterate and delete topics for each post_id, or a broader delete.
foreach ($postsToDelete as $postInfo) {
exec_stmt('DELETE FROM post_topics WHERE post_id = ?', 'i', (int) $postInfo['post_id']);
}
exec_stmt('DELETE FROM posts WHERE author_id = ?', 'i', $authorId);
// Cannot get actual deleted count.
} catch (\Exception $e) {
error_log("Error deleting posts by author (ID: $authorId): " . $e->getMessage());
}
}
/**
* Increments read count for a post.
* WARNING: Success of the UPDATE is not reliably checkable with current exec_stmt.
*/
public function incrementReadCount(int $postId): void
{
// exec_stmt returns 0 for successful UPDATE.
exec_stmt('UPDATE posts SET read_count = read_count + 1 WHERE post_id = ?', 'i', $postId);
// No easy way to confirm success with current exec_stmt.
}
}
// Example Usage (Illustrative - this would go in your controller/logic files)
/*
* $topicRepo = new TopicRepository();
* // Ensure $_ENV['ESSAY_CONTENT_PATH'] is set in your environment/config
* $essayPath = $_ENV['ESSAY_CONTENT_PATH'] ?? '/path/to/your/site/public/essays';
* $postRepo = new PostRepository($topicRepo, $essayPath);
*
* // --- Fetching Posts ---
* // $allPosts = $postRepo->getAllPosts('created_at', 'DESC');
* // $post123 = $postRepo->findById(123);
* // if ($post123 instanceof Essay) {
* // echo "Essay Title: " . $post123->getTitle() . "\n";
* // }
*
* // --- Creating a new Essay ---
* // $author = $userRepo->findAuthorById(1); // Assuming a UserRepo or similar for authors
* // if ($author) {
* // $newEssayId = 1001; // Must be unique, not auto-incremented in schema
* // $newEssay = new Essay(
* // $newEssayId, $author, "My New OOP Essay", "my-new-oop-essay",
* // "# Hello World\n\nThis is markdown content.", "An excerpt about the essay."
* // );
* // $createdPost = $postRepo->createPost($newEssay, ['PHP', 'OOP', 'Refactoring']);
* // if ($createdPost) {
* // echo "Created Post ID: " . $createdPost->getPostId() . "\n";
* // } else {
* // echo "Failed to create post.\n";
* // }
* // }
*
* // No explicit $dbConnection->close(); needed as exec_stmt handles its own connections.
*/
+71 -12
View File
@@ -6,18 +6,20 @@ require_once 'post.php';
class Space
{
private int $spaceId;
private int $id;
private string $name;
private string $description;
private int $visibility;
private array $members;
private array $posts;
private ?Space $parent;
public function __construct(?int $spaceId, string $name, string $description, array $members, $posts, ?Space $parent = null)
public function __construct(?int $id, string $name, string $description, int $visibility, array $members, array $posts, ?Space $parent = null)
{
$this->spaceId = $spaceId ?? randomId(2);
$this->id = $id ?? randomId(2);
$this->name = $name;
$this->description = $description;
$this->visibility = $visibility;
$this->members = $members;
$this->posts[] = $posts;
$this->parent = $parent;
@@ -25,9 +27,33 @@ class Space
/* $stmt = 'INSERT INTO spaces (spaceId, name, description, parentSpaceIdk)'; */
}
public static function retrieveFromDB(int $space, ?int $user): ?Space
public function getCardHTML(): string
{
if ($user) {
$html = '
<div class="full_width std_border padding center top_margin bottom_margin">
<a href="spaces.php?view=show&id=' . $this->id . '" class="divLink">
<div class="row space_between full_width">
<div class="column std_width">
<h4>' . $this->name . '</h4>
</div>
<div class="">
<span>Something</span>
</div>
</div>
<div class="row full_width">
<p>' . $this->description . '</p>
</div>
</a>
</div>
';
return $html;
}
public static function retrieveFromDB(int $space, int $user = -1): ?Space
{
if ($user != -1) {
$stmt = 'SELECT * FROM spaceMembers WHERE space = ? AND user = ?';
$isMember = exec_stmt($stmt, 'ii', $space, $user)->fetch_assoc();
if (!$isMember)
@@ -40,20 +66,48 @@ class Space
}
$stmt = 'SELECT * FROM space WHERE id = ?';
$space = exec_stmt($stmt, 'i', $space)->fetch_assoc();
$rawSpace = exec_stmt($stmt, 'i', $space)->fetch_assoc();
if (!empty($space['parentSpace']))
$parent = Space::retrieveFromDB($space['parentSpace'], $user);
if (!empty($rawSpace['parentSpace']))
$parent = Space::retrieveFromDB($rawSpace['parentSpace'], $user);
else
$parent = null;
$stmt = 'SELECT user FROM spaceMembers WHERE space = ?';
$members = exec_stmt($stmt, 'i', $space)->fetch_assoc();
$members = [];
$rawMembers = exec_stmt($stmt, 'i', $rawSpace['id']);
foreach ($rawMembers as $m) {
$members[] = User::retrieveFromDB($m['user']);
}
$stmt = 'SELECT post FROM spacePosts WHERE space = ?';
$posts = exec_stmt($stmt, 'i', $space)->fetch_assoc();
$posts = [];
$rawPosts = exec_stmt($stmt, 'i', $rawSpace['id']);
while ($row = $rawPosts->fetch_assoc()) {
$posts[] = Post::retrieveFromDB($row['post']);
}
return new Space($space['id'], $space['name'], $space['description'], $members, $posts, $parent);
$space = new Space($rawSpace['id'], $rawSpace['name'], $rawSpace['description'], $rawSpace['visibility'], $members, $posts, $parent);
foreach ($posts as $p)
$p->setSpace($space);
return $space;
}
public function allowUser(User $user)
{
$stmt = 'SELECT space FROM spaceMembers WHERE user = ?';
$result = exec_stmt($stmt, 'i', $user->getId())->fetch_assoc();
if ($result['space'] == $this->id)
return true;
else
return false;
}
public function getPosts()
{
return $this->posts;
}
public function __toString()
@@ -63,7 +117,7 @@ class Space
public function getId(): int
{
return $this->spaceId;
return $this->id;
}
public function getName(): string
@@ -75,4 +129,9 @@ class Space
{
return $this->description;
}
public function getVisibility(): int
{
return $this->visibility;
}
}
+156 -88
View File
@@ -30,7 +30,8 @@ class LoginCredentials
class User
{
private int $userId;
private int $id;
private ?array $spaces;
private string $username;
private ?DateTime $createdAt;
private ?DateTime $lastActive;
@@ -38,8 +39,9 @@ class User
private ?string $profilePicture;
private ?string $bio;
private ?string $website;
private int $role;
public function __construct(int $userId, string $username, ?string $profilePicture = null, ?string $bio = null, ?string $website = null, ?mysqli $db = null)
public function __construct(int $id = -1, ?array $spaces, string $username, ?string $profilePicture = null, ?string $bio = null, ?string $website = null, int $role = 3)
{
// Basic XSS prevention on construction (can be enhanced)
$username = htmlspecialchars($username, ENT_QUOTES, 'UTF-8');
@@ -52,7 +54,6 @@ class User
throw new InvalidArgumentException('Invalid characters in user data.');
}
$this->userId = $userId;
$this->username = $username;
$this->profilePicture = $profilePicture;
$this->bio = $bio;
@@ -60,58 +61,130 @@ class User
$this->createdAt = new DateTime();
$this->lastActive = null;
$this->isActive = false;
$this->role = $role;
$this->spaces = null;
if ($db) {
$stmt = $db->prepare('INSERT INTO user (user_id, username, profile_picture, bio, website) VALUES (?, ?, ?, ?, ?)');
if ($stmt) {
$stmt->bind_param('issss', $this->userId, $this->username, $this->profilePicture, $this->bio, $this->website);
$stmt->execute();
$stmt->close();
} else {
error_log('Error preparing statement: ' . $db->error);
}
if ($id == -1) {
$this->id = randomId(0);
$stmt = 'INSERT INTO user (userId, username, profilePicture, bio, website) VALUES (?, ?, ?, ?, ?)';
exec_stmt($stmt, 'issss', $this->id, $this->username, $this->profilePicture, $this->bio, $this->website);
} else
$this->id = $id;
}
public static function exists(int $id): bool
{
$stmt = 'SELECT count(id) FROM user WHERE id = ?';
$result = exec_stmt($stmt, 'i', $id)->fetch_assoc();
if ($result['count(id)'] == 1)
return true;
else if ($result['count(id)'] > 1)
return true;
else
return false;
}
public static function retrieveFromDB(int $userId): ?User
{
$stmt = 'SELECT id, username, profilePicture, bio, website FROM user WHERE id = ?';
$user = exec_stmt($stmt, 'i', $userId)->fetch_assoc();
if ($user) {
return new User($user['id'], null, $user['username'], $user['profilePicture'], $user['bio'], $user['website']);
} else {
// Consider logging or throwing an exception if no database connection is provided
error_log('Database connection not provided during User creation.');
}
}
public static function exists(int $id)
{
$stmt = 'SELECT count(username) FROM user WHERE id = ?';
echo pretty_dump(exec_stmt($stmt, 'i', $id)->fetch_assoc());
}
public static function login(LoginCredentials $credentials, ?mysqli $db): ?User
{
if (!$db) {
error_log('Database connection not provided for login.');
return null;
}
$username = $credentials->getUsername();
$password = $credentials->getPassword();
}
$stmt = $db->prepare('SELECT user_id, username, password_hash, profile_picture, bio, website FROM user WHERE username = ?');
if ($stmt) {
$stmt->bind_param('s', $username);
$stmt->execute();
$result = $stmt->get_result();
$user = $result->fetch_assoc();
$stmt->close();
public static function getSignupHTML()
{
$html = '
<div class="form_card">
<h3 class="underline">Sign Up</h3>
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="form_id" value="signup_form">
<input type="hidden" name="crop_x" value="" id="crop_x">
<input type="hidden" name="crop_y" value="" id="crop_y">
<input type="hidden" name="crop_width" value="" id="crop_width">
<input name="required_field" id="required_field" value="">
if ($user && hash('sha256', $password) === $user['password_hash']) {
return new User(
(int) $user['user_id'],
$user['username'],
$user['profile_picture'],
$user['bio'],
$user['website']
);
}
} else {
error_log('Error preparing statement: ' . $db->error);
}
return null;
<label for="username">Username</label>
<input id="username" name="username" required>
<label for="email">Email Address</label>
<input id="email" name="email" required>
<label for="password">Password</label>
<input id="password" name="password" type="password" required>
<label for="verify_password">Verify Password</label>
<input id="verify_password" name="verify_password" type="password" required>
<label for="profile_picture_input" class="upload_button modal_button" id="profile_cropper_button">Upload a Profile Picture</label>
<input id="profile_picture_input" class="file_input" name="profile_picture" type="file" accept="image/png, image/jpeg, image/jpg">
<small class="file_input_feedback">No file selected.</small>
<label class="form_checkbox_container">Stay Logged In?
<input name="stay_logged_in" type="checkbox" value="true">
<span class="checkmark"></span>
</label>
<input class="button" type="submit" value="Sign Up">
</form>
<div id="modal" class="modal">
<div id="display_modal"></div>
</div>
<div style="display: none;">
<div id="profile_cropper" class="modal_form">' . profile_cropper() . '</div>
</div>
</div>
';
return $html;
}
public static function getLoginHTML()
{
$html = '
<div class="form_card">
<h3 class="underline">Log In</h3>
<form method="post" id="login_form" action="director.php">
<input type="hidden" name="form_id" value="login_form">
<input type="hidden" name="password_hash" value="" id="password_hash">
<label for="username">Username / Email</label>
<input id="username" name="username" spellcheck="false" required autofocus>
<label for="password">Password</label>
<input id="password" name="password" type="password" spellcheck="false" required>
<label class="form_checkbox_container">Stay Logged In?
<input name="stay_logged_in" type="checkbox" value="true">
<span class="checkmark"></span>
</label>
<input class="button" type="submit" value="Log In">
</form>
<script src="js/login.js" defer></script>
</div>
';
return $html;
}
public static function login(LoginCredentials $credentials): ?User
{
$stmt = 'SELECT id, username, email, passwordHash, role, profilePicture, bio, website FROM user WHERE username = ?';
$user = exec_stmt($stmt, 's', $credentials->getUsername())->fetch_assoc();
if (!password_verify($credentials->getPassword(), $user['passwordHash']))
return null;
else
return new User($user['id'], null, $user['username'], $user['profilePicture'], $user['bio'], $user['website']);
}
public static function logout(): void
@@ -124,9 +197,33 @@ class User
exit();
}
public function getUserId(): int
public function getHomeSpace()
{
return $this->userId;
// TODO: Need to implement. Requires fleshing out additional space types.
return Space::retrieveFromDB(1, 1);
}
public function getSpaces()
{
$stmt = 'SELECT * FROM spaceMembers WHERE user = ?';
$spaces = [];
$rawSpaces = exec_stmt($stmt, 'i', $this->id);
while ($row = $rawSpaces->fetch_assoc()) {
$spaces[] = Space::retrieveFromDB($row['space'], $row['user']);
}
$this->spaces = $spaces;
return $spaces;
}
public function getRole(): int
{
return $this->role;
}
public function getId(): int
{
return $this->id;
}
public function getUsername(): string
@@ -183,7 +280,7 @@ class User
$this->isActive = !$this->isActive;
$stmt = $db->prepare('UPDATE user SET is_active = ? WHERE user_id = ?');
if ($stmt) {
$stmt->bind_param('ii', (int) $this->isActive, $this->userId);
$stmt->bind_param('ii', (int) $this->isActive, $this->id);
$result = $stmt->execute();
$stmt->close();
return $result;
@@ -205,7 +302,7 @@ class User
}
$stmt = $db->prepare('UPDATE user SET username = ? WHERE user_id = ?');
if ($stmt) {
$stmt->bind_param('si', $newUsername, $this->userId);
$stmt->bind_param('si', $newUsername, $this->id);
$result = $stmt->execute();
$stmt->close();
if ($result) {
@@ -227,7 +324,7 @@ class User
}
$stmt = $db->prepare('UPDATE user SET password_hash = ? WHERE user_id = ?');
if ($stmt) {
$stmt->bind_param('si', $newPasswordHash, $this->userId);
$stmt->bind_param('si', $newPasswordHash, $this->id);
$result = $stmt->execute();
$stmt->close();
return $result;
@@ -249,7 +346,7 @@ class User
}
$stmt = $db->prepare('UPDATE user SET profile_picture = ? WHERE user_id = ?');
if ($stmt) {
$stmt->bind_param('si', $newProfilePicture, $this->userId);
$stmt->bind_param('si', $newProfilePicture, $this->id);
$result = $stmt->execute();
$stmt->close();
if ($result) {
@@ -270,7 +367,7 @@ class User
}
$stmt = $db->prepare('INSERT INTO follows (follower_user_id, following_user_id, notify_user) VALUES (?, ?, ?)');
if ($stmt) {
$stmt->bind_param('iii', $this->userId, $followingUserId, (int) $notify);
$stmt->bind_param('iii', $this->id, $followingUserId, (int) $notify);
$result = $stmt->execute();
$stmt->close();
return $result;
@@ -288,7 +385,7 @@ class User
}
$stmt = $db->prepare('SELECT u.user_id, u.username, u.profile_picture, u.bio, u.website FROM follows f JOIN user u ON f.follower_user_id = u.user_id WHERE f.following_user_id = ?');
if ($stmt) {
$stmt->bind_param('i', $this->userId);
$stmt->bind_param('i', $this->id);
$stmt->execute();
$result = $stmt->get_result();
$followers = [];
@@ -317,7 +414,7 @@ class User
}
$stmt = $db->prepare('SELECT u.user_id, u.username, u.profile_picture, u.bio, u.website FROM follows f JOIN user u ON f.following_user_id = u.user_id WHERE f.follower_user_id = ?');
if ($stmt) {
$stmt->bind_param('i', $this->userId);
$stmt->bind_param('i', $this->id);
$stmt->execute();
$result = $stmt->get_result();
$following = [];
@@ -337,35 +434,6 @@ class User
return [];
}
}
// Static method to fetch a User by ID (example using mysqli)
public static function findById(int $userId, ?mysqli $db): ?User
{
if (!$db) {
error_log('Database connection not provided for findById.');
return null;
}
$stmt = $db->prepare('SELECT user_id, username, profile_picture, bio, website FROM user WHERE user_id = ?');
if ($stmt) {
$stmt->bind_param('i', $userId);
$stmt->execute();
$result = $stmt->get_result();
$user = $result->fetch_assoc();
$stmt->close();
if ($user) {
return new User(
(int) $user['user_id'],
$user['username'],
$user['profile_picture'],
$user['bio'],
$user['website']
);
}
} else {
error_log('Error preparing statement: ' . $db->error);
}
return null;
}
}
class Author extends User {}
class Creator extends User {}